Kotlin/JS 상태관리 3 와 npm 라이브러리 래핑

2로 올린걸 사용하려는데 시발 제대로 작동 안하는 경우가 있었다. 그래서 그냥 다음과 같이 전역 메서드로만 구성하는게 해답일 것 같다.

import js.array.JsTuple2
import js.array.tupleOf
import react.*

typealias SuspendDispatch<TAction> = suspend (action: TAction) -> Unit
typealias SuspendReducerInstance<TState, TAction> = JsTuple2<TState, SuspendDispatch<TAction>>
typealias Middleware<TState, TActionType> = (state: TState, action: ReducerAction<TActionType>) -> TState

external interface ReducerAction<TActionType : Enum<TActionType>> {
    var type: TActionType
    var payload: Any?
}

fun <TState : State, TActionType : Enum<TActionType>> createContext(): RequiredContext<SuspendReducerInstance<TState, ReducerAction<TActionType>>> =
    createRequiredContext()

fun <TState : State, TActionType : Enum<TActionType>> RequiredContext<SuspendReducerInstance<TState, ReducerAction<TActionType>>>.useState() =
    useRequiredContext(this).component1()

fun <TState : State, TActionType : Enum<TActionType>, U> RequiredContext<SuspendReducerInstance<TState, ReducerAction<TActionType>>>.useSelector(
    selector: (TState) -> U
): U =
    useState().let(selector)

fun <TState : State, TActionType : Enum<TActionType>> RequiredContext<SuspendReducerInstance<TState, ReducerAction<TActionType>>>.useDispatch() =
    useRequiredContext(this).component2()

fun <TState : State, TActionType : Enum<TActionType>> useReducer(
    initialState: TState,
    reducer: Reducer<TState, ReducerAction<TActionType>>,
    vararg middlewares: Middleware<TState, TActionType>
): SuspendReducerInstance<TState, ReducerAction<TActionType>> {
    val targetReducer: Reducer<TState, ReducerAction<TActionType>> = { state, action ->
        reducer(middlewares.fold(state) { accumulator, middleware ->
            middleware(accumulator, action)
        }, action)
    }
    val (state, syncDispatch) = useReducer(targetReducer, initialState)
    return tupleOf(state) { action ->
        syncDispatch(action)
    }
}

미들웨어는 리덕스 미들웨어처럼 다음 상태도 알 수 있는 건 아니고(store를 구현한게 아니라서;) 그냥 받은 액션과 현재 상태만 로깅하는 용도다.

예시 카운터

import js.objects.jso
import react.State

external interface CounterState : State {
    var count: Int
}

enum class CounterAction {
    INCREMENT,
    DECREMENT,
    ADD,
    SUBTRACT
}

val CounterContext = createContext<CounterState, CounterAction>()

@Suppress("FunctionName")
fun CounterReducer(state: CounterState, action: ReducerAction<CounterAction>): CounterState {
    return when (action.type) {
        CounterAction.INCREMENT -> jso {
            count = state.count + 1
        }

        CounterAction.DECREMENT -> jso {
            count = state.count - 1
        }

        CounterAction.ADD -> jso {
            count = state.count + requireNotNull(action.payload as? Int)
        }

        CounterAction.SUBTRACT -> jso {
            count = state.count - requireNotNull(action.payload as? Int)
        }
    }
}

사용법은 CounterContext.Provider 로 적절히 감싸고 CounterContext.useSelector나 Countercontext.useDispatch 를 쓰면 된다.


다음에 알아볼 것은 npm 라이브러리를 사용할 때 dynamic 천지면 매우 불편하므로 어느정도 래핑을 해야하는데 거기서 기억해야할 것이다.

가끔 라이브러리들을 보면 클래스로 정의 -> 인스턴스 생성한걸 export

하는 경우가 있다. 그리고 아래처럼 interface에 함수들을 정의해서 인스턴스가 함수처럼 작동할 수 있게 하는 경우가 있다.

예 : notistack #Link

이걸 코틀린으로 가져올 때 하나 간과한게 있었다.

함수 호출은 기본적으로 call 로 하는데, call의 맨 앞 파라미터는 인스턴스의 메서드라면 this, 정적 메서드라면 null이다. (null인지 undefined인지는 사실 모름)

그러므로 가져올 때는 this가 무조건 있도록 해야한다.

kotlin에서는 definedExternally 를 쓰면 대충 JS에서는 정의되어 있으니 그렇게 알고 있어 정도다.

끗.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다