velopert님의 벨로그 포스트를 보면서 이번 장에서는 타입스크립로 리액트의 Context API를 활용하는 방법에 대해서 알아보겠습니다. 프로젝트의 Hello, World!라고 할 수 있는 투두리스트를 만들어보면서 배웠던 내용을 정리해보죠.
먼저, 타입스크립트가 적용된 리액트 프로젝트를 만들어야겠죠?
$ yarn create react-app ts-context-api --template typescript
리액트의 자체 상태관리인 context API를 만들때는 크게 4가지로 구분할 수 있습니다.
import React, {createContext, Dispatch, useReducer, useContext} from "react";
export type Todo = {
id: number;
text: string;
done: boolean;
};
type TodosState = Todo[];
const TodosStateContext = createContext<TodosState | undefined>(undefined);
여기서 초기 상태는 Todo[]
혹은 아직 정의되지 않았을 수도 있기 때문에 undefined
를 타입으로 추가해줍니다.
import React, {createContext, Dispatch, useReducer, useContext} from "react";
type Action =
| {type: "CREATE"; text: string}
| {type: "TOGGLE"; id: number}
| {type: "REMOVE"; id: number};
type TodosDispatch = Dispatch<Action>; // 이 부분!
const TodosDispatchContext = createContext<TodosDispatch | undefined>(
undefined
);
여기서 Dispatch<Action>
와 같이 제네릭으로 액션들의 타입을 넣으면 나중에 컴포넌트에서 액션을 디스패치할 때 타입 검사를 할 수 있습니다. 정확히 동작하는 방식은 잘 모르겠지만 일단은 알아둡시다!
function todosReducer(state: TodosState, action: Action): TodosState {
switch (action.type) {
case "CREATE":
const nextId = Math.max(...state.map((todo) => todo.id)) + 1;
const newState = state.concat({
id: nextId,
text: action.text,
done: false,
});
return newState;
case "TOGGLE":
return state.map((todo) =>
todo.id === action.id ? {...todo, done: !todo.done} : todo
);
case "REMOVE":
return state.filter((todo) => todo.id !== action.id);
default:
throw new Error("Unahdled action");
}
}
이 부분에서 눈 여겨봐야할 부분은 state.map
이 아니라 ...state.map
을 사용했다는 점입니다. 콘솔에 찍어본 결과 배열을 리턴하는 전자와는 다르게 ...state.map
을 사용했을 경우 배열을 벗겨서 값 자체를 리턴해줍니다. 알아두면 나중에 요긴하게 써먹을 것 같죠?
위에서 만든 상태 context와 dispatch context 각각의 provider를 합쳐보죠.
export function TodosContextProvider({children}: {children: React.ReactNode}) {
const [todos, dispatch] = useReducer(todosReducer, [
{
id: 1,
text: "Context API 배우기",
done: true,
},
{
id: 2,
text: "TypeScript 배우기",
done: true,
},
{
id: 3,
text: "TypeScript 와 Context API 함께 사용하기",
done: false,
},
]);
return (
<TodosDispatchContext.Provider value={dispatch}>
<TodosStateContext.Provider value={todos}>
{children}
</TodosStateContext.Provider>
</TodosDispatchContext.Provider>
);
}
children
은 App.js혹은 Index.js에서 렌더링할 컴포넌트를 말합니다. Provider
로 감싸야되기 때문이죠.
이렇게 context API에 대해서 알아봤는데 커스텀 hooks을 사용하면 나중에 컴포넌트에서 사용할 때 더 편하게 상태관리를 할 수 있습니다. 특히, todos
는 타입이 두 개였죠? TodosState | undefined
이렇게 두 개였는데 이를 사용하기 전에 꼭 유효성 검사를 해줘야합니다.
const todos = useContext(TodosStateContext);
if (!todos) return null;
그래서 이런 부분때문에 커스텀 hooks를 사용합니다.
export function useTodosState() {
const state = useContext(TodosStateContext);
if (!state) throw new Error("TodosProvider not found");
return state;
}
export function useTodosDispatch() {
const dispatch = useContext(TodosDispatchContext);
if (!dispatch) throw new Error("TodosProvider not found");
return dispatch;
}
나중에 컴포넌트에서 사용할 때는 그냥 아래와 같이 사용하면 되겠죠.
import {useTodosDispatch} from "../contexts/TodosContext";
function TodoItem({todo}: TodoItemProps) {
const dispatch = useTodosDispatch();
const onToggle = () => {
dispatch({type: "TOGGLE", id: todo.id});
};
const onRemove = () => {
dispatch({type: "REMOVE", id: todo.id});
};
(...)
https://velog.io/@velopert/typescript-context-api | TypeScript 환경에서 리액트 Context API 제대로 활용하기
안녕하세요 :) 질문이 하나 있어 댓글 남깁니다! 혹시 context를 정의할 때 undefined를 빼는 방법은 없을까요? undefined가 있으니 context를 쓰는 컴포넌트에서 항상 undefined를 확인하는 전처리를 해주어야해서 불편한 듯 합니다!