들어가며 ..
다음은 이 포스팅에서 다루는 내용이다.
- React에서 전역 상태를 관리하는 방법 소개 및 비교: Context API와 Recoil을 중심으로
- Context API의 성능 이슈에 관하여
- Context API로 작성한 코드를 Recoil로 마이그레이션 해 본 과정
props
로 데이터를 전달하지 않고도, 여러 컴포넌트 간 상태를 손쉽게 공유할 수 있다.크게 3가지 요소로 구성된다.
일일이 컴포넌트 트리 저 아래에 있는 곳까지 props를 전해 주지 않아도 되므로 보다 쉬운 상태 관리가 가능해지지만,
리코일에 대한 기본적인 내용은 본 블로그의 Recoil을 이용한 React 상태 관리 포스팅에서도 정리되어 있다.
atomFamily
)주로 2가지 주요 개념으로 구성된다.
상태 관리 로직을 보다 명확하게 분리하고 효과적으로 관리할 수 있지만,
🔎 어느 도구를 선택하든, 해결하고자 하는 문제에 가장 적합한 도구는 있어도, 항상 모든 경우에 좋은 단 하나의 도구가 정해져 있지는 않을 것이다. 고유의 특성과 한계점을 먼저 따져 보는게 중요하지 않을까.
React 진영에서 널리 사용되고 있는 여러 상태 관리 도구들이 있다.
예를 들면 Redux, MobX, Recoil, Jotai, Zustand, React Query, SWR 등... 상당히 많은 종류의 도구가 있다.
각각의 라이브러리는 특화되어 있는 부분이 서로 다르다. 이 중에서도 마지막 2가지는 비동기 데이터 처리에 특화되어 있는 라이브러리들이라 치고, 나머지는 상태를 중앙 집중식으로 관리하거나 보다 간단하고 단순하게 상태 관리를 할 수 있게 해준다.
물론 모든 종류의 도구를 전부 사용해 본 것은 아니지만, 이들을 소개하는 문서에는 공통점이 있었는데
바로 'React의 컨텍스트 지옥(Context Hell) 문제를 해결하고자 등장하였다' 라는 문구를 심심치 않게 볼 수 있다는 것이었다.
그래서 자연스레 '🤔 과연 Context API를 다른 상태 관리 라이브러리로 대체해야만 할까?'와 같은 물음이 떠오르곤 했다.
그렇다면 일반적으로 대두되는 Context API의 성능 관련 이슈에 적절히 대응하면서도 장점을 최대한 살릴 수 있도록 활용하는 방법에 대해 정리해 보았다.
React.memo
, useMemo
, useCallback
등의 최적화 기능을 활용해 개선 가능하다.성능 최적화에서 중요한 것은 '컴포넌트가 렌더링 될 때 마다 Context 값이 재생성되지 않도록 관리'하는 것이다.
이렇게 하면 Context를 사용하는 모든 소비자 컴포넌트가 불필요하게 렌더링 되는 것을 방지할 수 있다.
‣ useMemo
활용하여 value가 변경될 때만 재계산 하는 코드:
// ExamProvider.js
const ExamContext = createContext(null)
const ExamProvider = ({ children }) => {
const [value, setValue] = useState('init value')
const contextValue = useMemo(() => ({ value, setValue }), [value])
return (
<ExamContext.Provider value={contextValue}>
{children}
</ExamContext.Provider>
)
}
‣ React.memo
활용하여 props 변화가 존재할 때만 렌더링 하는 코드:
// ExamComponent.js
const ExamComponent = React.memo(({ onClick }) => {
const { value } = useContext(ExamContext)
console.log('ExamComponent Rendered.')
return (
<>
<p>Value: {value}</p>
<button onClick={onClick}>Click!</button>
</>
)
})
‣ useCallback
활용하여 필요 시에만 함수를 재생성 하는 코드:
// App.js
const App = () => {
const { setValue } = useContext(ExamContext)
const handleClick = useCallback((newValue) => {
setValue(newValue)
}, [setValue])
return (
<ExamProvider>
<ExamComponent onClick={handleClick} />
</ExamProvider>
)
}
이번에 Recoil을 학습해보게 되면서 코드에 익숙해지고자 기존 프로젝트에 useContext
를 사용해 만들어진 Alert 공통 컴포넌트를 Recoil 방식으로 한 번 변경해 보았다.
먼저 아래는 원래 있던 코드이다.
‣ AlertDialogProvider.js
const AlertDialogContext = createContext()
export const useAlertDialog = () => {
return useContext(AlertDialogContext)
}
export const AlertDialogProvider = ({ children }) => {
const [isOpen, setIsOpen] = useState(false)
const [title, setTitle] = useState("")
const [desc, setDesc] = useState("")
const [action, setAction] = useState({})
const hasButtonAction = () => {
return action?.label && typeof action?.handler === 'function'
}
const openAlert = (title, desc, action) => {
setTitle(title)
setDesc(desc)
setIsOpen(true)
if (action.lable && typeof action.handler === "function") {
setAction(action)
} else {
setAction({})
}
}
const closeAlert = () => {
setIsOpen(false)
}
return (
<AlertDialogContext.Provider value={{ openAlert, closeAlert }}>
{children}
<AlertOverlay
isOpen={isOpen}
onClose={closeAlert}
title={title}
description={desc}
action={action}
/>
</AlertDialogContext.Provider>
)
}
‣ AlertOverlay.js
// Chakra-UI로 작성한 공통 얼럿 컴포넌트
const AlertOverlay = ({ isOpen, onClose, title, desc, action }) => {
return (
<AlertDialog
isOpen={isOpen}
onClose={onClose}
>
<AlertDialogContent>{title}</AlertDialogContent>
<AlertDialogHeader>{desc}</AlertDialogHeader>
<AlertDialogFooter>
<Button onClick={onClose}>Close</Button>
{action?.label && typeof action?.handler === 'function' && (
<Button onClick={action.handler}>{action.label}</Button>
)}
</AlertDialogFooter>
</AlertDialog>
)
}
‣ main.js
<>
<AlertDialogProvider>
{/* ... 그 외 다른 Provider 등 */}
<App />
</AlertDialogProvider>
</>
변경한 과정은 다음과 같다.
isOpen
, title
, desc
, action
속성을 모두 Atom 안에 기본값과 함께 정의한다.openAlert
, closeAlert
, hasButtonAction
)<AlertOverlay />
를 가져다 놓는다.‣ atoms.js
export const alertDialogState = atom({
key: 'alertDialogState',
default: {
isOpen: false,
title: '',
desc: '',
action: {}
}
})
‣ useAlertDialog.js
const useAlertDialog = () => {
const [alertDialog, setAlertDialog] = useRecoilState(alertDialogState)
const openAlert = (title, desc, action) => {
setAlertDialog({
isOpen: true,
title,
desc,
action: action && hasButtonAction() ? action : {}
})
}
const closeAlert = () => {
setAlertDialog((prev) => ({
...prev,
isOpen: false
}))
}
const hasButtonAction = () => {
return (
alertDialog.action?.label &&
typeof alertDialog.action?.handler === 'function'
)
}
return { openAlert, closeAlert, hasButtonAction }
}
export default useAlertDialog
‣ AlertOverlay.js
const AlertOverlay = () => {
const alertDialog = useRecoilValue(alertDialogState)
const { closeAlert, hasButtonAction } = useAlertDialog()
return (
<AlertDialog
isOpen={alertDialog.isOpen}
onClose={closeAlert}
>
<AlertDialogContent>
<AlertDialogHeader>{alertDialog.title}</AlertDialogHeader>
<AlertDialogBody>{alertDialog.desc}</AlertDialogBody>
<AlertDialogFooter>
<Button onClick={closeAlert}>Close</Button>
{hasButtonAction() && (
<Button onClick={alertDialog.action.handler}>
{alertDialog.action.label}
</Button>
)}
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
‣ App.js
<>
{/* .. 기존 내용들 .. */}
<AlertOverlay />
</>
이후 얼럿을 표시하고자 하는 컴포넌트에서 const { openAlert } = useAlertDialog()
와 같이 불러와 호출하면 어디서나 얼럿을 띄우고 닫을 수 있게 된다.
어느 기술을 도입하든 각 프로젝트에 맞는 최적의 도구를 선택하는 것이 효율성과 유지보수성에 있어서 큰 도움이 될 것이다. 이러한 선택은 프로젝트의 규모, 구조, 개발자의 선호도 등의 요소에 따라 이루어질 수 있다. 선택에 있어 도움이 될 수 있도록 지금까지 Context API와 Recoil을 중심으로 각 방식의 특성과 그들이 사용되기 적절한 프로젝트에 대해 알아보았다.
마지막으로 정리해보자면,