보통 ContextAPI와 전역 상태관리 라이브러리를 많이 비교한다. 사실이 두 개는 같은 비교군이 아니다.
ContextAPI를 전역 상태 관리 툴이라고 오해할 수 있지만 Context는 실제로 아무것도 관리하지 않는다. 단순히 값을 전달하는 파이프다.
React는 부모 컴포넌트에서 자식 컴포넌트로 props를 전달하는 하향식이다. 이 때 전달 되는 props가 많아지고 컴포너트 깊이가 깊어지면 관리가 번거로워 진다. 그래서 ContextAPI를 통해 Provider로 감싸져있는 컴포넌트 내부에서 어디서든 상태값에 접근할 수 있는 방법을 제공한다. 그러므로 prop-drilling이 해소되어 코드가 단순해진다.
그럼 Context는 단순 값을 전달하는 파이프이고 값은 어디에서 가져오는가?
값, 즉 상태관리는 useState나 useReducer를 통해 일어난다. Context는 이 상태들을 전달하는 것뿐이다. 그래서 전역 상태관리 라이브러리와 ContextAPI는 같은 비교군이 아닌 것이다.
위에서 ContextAPI를 사용하면 Provider로 감싸져있는 컴포넌트에서 상태값에 접근할 수 있다고 했다. 그래서 prop-drilling을 해소할 수 있는 것이다. 코드로 한번 살펴보자.
스낵바를 구현한 코드이다.
우선 스낵바의 상태값을 useReducer에서 관리한다.
const reducer = (prevState: State, action: Action): string[] => {
switch (action.type) {
case 'add':
return [...prevState, action.message];
case 'delete':
return prevState.slice(1, prevState.length);
default:
throw new Error();
}
};
export const SnackbarContext = createContext<State>(initialState);
export const SnackbarDispatchContext = createContext<SnackbarDispatchType>(() => {});
export const SnackbarProvider = ({ children }: SnackbarProviderProps) => {
const [snackbars, dispatch] = useReducer<Reducer<State, Action>>(reducer, initialState);
return (
<SnackbarContext.Provider value={snackbars}>
<SnackbarDispatchContext.Provider value={dispatch}>
{children}
</SnackbarDispatchContext.Provider>
</SnackbarContext.Provider>
);
};
그리고 스낵바는 하나의 상태만 존재해야 하기 때문에 최상위 컴포넌트인 App 파일에서 스낵바를 보여준다.
const App = () => {
const content = useRoutes(routes);
const snackbars = useContext(SnackbarContext);
return (
<Container>
<GlobalStyles />
<Header />
<PageContainer>{content}</PageContainer>
<SnackbarContainer>
{snackbars.map((snackbar, index) => (
<Snackbar key={index}>{snackbar}</Snackbar>
))}
</SnackbarContainer>
<Footer />
</Container>
);
};
ContextAPI를 사용하지 않고 App 파일에서 스낵바의 상태를 관리하는 useReducer를 만든다면 스낵바의 상태값를 변경하는 dispatch 함수를 자식 컴포넌트로 내려주어야 한다.
스낵바에서 dispatch를 감싼 함수를 반환해주는 훅을 만들어준다.
여기서 스낵바를 useReducer로 관리하는 이유는 스낵바를 추가하고 삭제하는 로직이 명시적이기 때문이다.
useReducer로 관리하지 않고 useState로 관리하면 dispatch(..이 부분..)
에 스낵바의 상태를 제어하는 코드가 들어간다. 그것보다는 명시적으로 type
이라는 인자를 받아 add
or delete
로 스낵바의 상태 변경을 명시적으로 제어할 수 있다.
import { useContext } from 'react';
import { SnackbarDispatchContext } from 'contexts/snackbarContext';
const useSnackbar = () => {
const dispatch = useContext(SnackbarDispatchContext);
const showSnackbar = ({ message }: ShowSnackbarProps) => {
dispatch({ type: 'add', message });
removeSnackbar();
};
const removeSnackbar = () => {
let showSnackbarTime = new Date().getTime();
const callback = () => {
const currentTime = new Date().getTime();
if (currentTime - 2000 > showSnackbarTime) {
dispatch({ type: 'delete' });
} else {
requestAnimationFrame(callback);
}
};
requestAnimationFrame(callback);
};
return { showSnackbar };
};
export interface ShowSnackbarProps {
message: string;
}
export default useSnackbar;
그리고 이 훅을 필요한 컴포넌트, Provider로 감싸져있는 컴포넌트나 다른 훅에서 호출해서 스낵바 상태를 조작하는 함수를 실행할 수 있다.
import { useState } from 'react';
import usePreQuestion from 'hooks/usePreQuestion';
import useSnackbar from 'hooks/useSnackbar';
import { MESSAGE } from 'constants/constants';
import { PreQuestionCustomHookType, PreQuestionParticipantType } from 'types/preQuestion';
import { ParticipantType } from 'types/team';
const usePreQuestionModal = () => {
const { preQuestion, getPreQuestion, deletePreQuestion } = usePreQuestion();
const { showSnackbar } = useSnackbar();
const [isPreQuestionModalOpen, setIsPreQuestionModalOpen] = useState(false);
const onClickDeletePreQuestion = async ({
levellogId,
preQuestionId,
}: Pick<PreQuestionCustomHookType, 'levellogId' | 'preQuestionId'>) => {
await deletePreQuestion({
levellogId,
preQuestionId,
});
setIsPreQuestionModalOpen(false);
showSnackbar({ message: MESSAGE.PREQUESTION_DELETE_CONFIRM });
};
return {
preQuestion,
isPreQuestionModalOpen,
onClickDeletePreQuestion,
};
};
export default usePreQuestionModal;
import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import styled from 'styled-components';
import useLevellog from 'hooks/useLevellog';
import useSnackbar from 'hooks/useSnackbar';
import { MESSAGE, ROUTES_PATH } from 'constants/constants';
import BottomBar from 'components/@commons/BottomBar';
import UiEditor from 'components/@commons/UiEditor';
const LevellogEdit = () => {
const { levellogRef, getLevellogOnRef, onClickLevellogEditButton } = useLevellog();
const { showSnackbar } = useSnackbar();
const { teamId, levellogId } = useParams();
const navigate = useNavigate();
const handleClickLevellogEditButton = () => {
if (typeof teamId === 'string' && typeof levellogId === 'string') {
onClickLevellogEditButton({ teamId, levellogId });
return;
}
showSnackbar({ message: MESSAGE.WRONG_ACCESS });
};
return (
<S.Container>
<UiEditor
needToolbar={true}
autoFocus={true}
contentRef={levellogRef}
initialEditType={'markdown'}
/>
<BottomBar
buttonText={'수정하기'}
handleClickRightButton={handleClickLevellogEditButton}
></BottomBar>
</S.Container>
);
};
export default LevellogEdit;
이렇게 스낵바의 상태값은 최상위 컴포넌트에서 보여주지만 상태를 조작하는 함수는 자식 컴포넌트에서 prop-drilling없이 호출할 수 있다.
모든 상태값을 Context를 통해 컴포넌트에 주입하는 것이 좋아보이는데 막상 그렇지 않다.
Provider로 감싼 컴포넌트가 재렌더링이 된다. 이는 Profiler에서 확인할 수 있다.
스낵바를 상태값을 하나 추가한 경우이다.
하위 컴포넌트가 전부 재렌더링된 것을 확인할 수 있다. 그래서 꼭 필요한 경우에만 ContextAPI를 사용하는 것이 좋다.