이번주 스터디는 지난 React에 점진적으로 TypeScript 적용하기 #1에 이어서 #2 입니다.
이번에는 contextAPI + useReducer를 활용할 때 type을 적용하는 방법 위주로 정리합니다.
context
API를 활용할 때 주로 사용하는 함수와 요소는 다음과 같습니다.
- createContext()
- useContext()
- <Context.Provider value={value}></Context.Provider>
이전에 context API는 전역으로 상태를 관리하기도 할 때 또는 재사용성이 높은 컴포넌트를 만들 때 사용하곤 하는데, 이 때 useReducer
hook과 같이 종종 사용한다고 말했었습니다.
그러면, 본격적으로 type 지정을 어떻게 하는 지 살펴보면 다음과 같습니다.
interface TodoType {
id: number
text: string
isChecked: boolean
}
interface TodoStateType {
todos: TodoType[]
}
const TodoStateContext = createContext<TodoStateType | null>(null)
TodoList App에서 활용한 context 코드의 일부입니다. createContext
를 통해 Context를 생성할 텐데요.
이 때, Provider
컴포넌트를 통해 전달한 value
의 type을 generic
과 union type
을 이용하여 TodoStateType | null
로 타입을 지정합니다.
// TodoProvider.tsx
function TodoProvider({children}: {children : React.ReactNode}) {
const [todoState, todosDispatch] = useReducer(todoReducer, { todos: loadTodos() })
return (
<TodoStateContext.Provider value={todoState}>
{children}
</TodoStateContext.Provider>
)
}
위의 createContext
에서 인자로 받는 값 또는 참조값의 타입을 지정해주었기 때문에, Provider
에서 value를 이용해 전역으로 상태를 공유하는 값 todoState
은 타입 오류 없이 잘 작동하게 됩니다.
useContext
는 전역으로 관리하는 상태 (여기서는 useReducer
가 반환하는todoState
)를 사용할 컴포넌트에서 import하여 사용해도 상관없지만, hook 함수로 만들면 상태를 사용하길 원하는 컴포넌트에서 활용할 때 유용하게 사용 가능합니다.
다음과 같이 사용합니다.
export const useTodoState = () => {
const value = useContext(TodoStateContext)
if (!value) {
throw new Error('cannot find useTodoState')
}
return value
}
if 조건문이 존재하는 이유는 위에서 보았듯, createContext
를 통해 context 생성 시 초기값을 null
로 주었습니다. 이후에 Provider
로 value
를 전달하여 useContext
를 사용할 때, 잘못된 값을 전달할 때 에러를 잡아내기 위해서, value가 null
이면 error를 확인할 수 있도록, 조건문을 추가하였습니다.
관리해야 할 상태가 참조값, 즉 배열 또는 객체인 경우 내부 필드(key 또는 Property)가 많아지면, useState
를 여러 개로 상태를 관리하는 경우들이 발생합니다.
이 때 useReducer
를 활용하면, useReducer
가 인자로 받는 순수 함수인 reducer
함수에서 조건문 switch-case
를 활용해서 dispatch
가 받는 action
객체가 다양하여도, 가독성이 늘어난다는 장점이 있습니다.
//todoReducer.ts
interface TodoInputStateType {
text: string
}
type TodoInputActionType =
| {
type: 'change'
payload: string
}
| { type: 'clear' }
export function todoInputReducer(state: TodoInputStateType, action: TodoInputActionType) {
switch (action.type) {
case 'change':
return {
text: action.payload,
}
case 'clear':
return {
text: '',
}
default:
throw new Error('this is not action type')
}
}
useReducer
에 사용되는 reducer
를 선언할 때 type은 인자로 받는 state
와 action
만 지정하면 됩니다.
state
는 text
를 필드로 갖는 객체이며, action
객체는 type
과 payload
라는 필드가 지정된 객체입니다. 위에서는 switch-case 조건문을 활용하기 때문에, 우리가 지정한 type에서만 action.type
, 즉 case
가 사용되도록 사전에 에러를 방지할 수 있습니다.
이 reducer
함수가 사용되는 useReducer
와 useReducer
가 반환하는 Disptach 함수inputDispatch
를 전역 상태로 전달하는 creatContext
함수를 살펴보면 다음과 같습니다.
// TodoProvider.tsx
type TodoInputActionType =
| {
type: 'change'
payload: string
}
| { type: 'clear' }
const InputTodoDispatchContext = createContext<React.Dispatch<TodoInputActionType> | null>(null)
function TodoProvider(){
const [inputState, inputDispatch] = useReducer(todoInputReducer, { text: '' })
return (
<InputTodoDispatchContext.Provider value={inputDispatch}>
{props.children}
</InputTodoDispatchContext.Provider>
)
}
이번에는 value
로 전달하는 전역 상태를 inputDispatch
로 활용해 봤습니다.
코드의 상단을 보면, Dispatch
함수의 타입을 createContext()
로 컨텍스트 생성 시 <React.Dispatch<TodoInputActionType> | null>
로 지정한 것을 볼 수 있습니다.
해당 type은 Dispatch
라는 함수를 받는 것이고, Dispatch
함수는 인자로 받는 Action
객체의 타입을 generic
으로 받기 때문에 위와 같이 type을 지정합니다.
이렇게 React에 Typescript를 적용할 때, context
API와 useReducer
사용시 타입을 지정하는 방법에 대해서 정리해 보았습니다. 다음에는 또 다른 전역상태 라이브러리인 redux
와 이 redux
를 쉽게 사용할 수 있게 도와주는 redux-toolkit
을 어떻게 사용하는 지 익히고, 정리하는 시간을 가져보겠습니다.
감사합니다.