무지성 Context API 업보, Zustand로 청산
싸늘하다. 사이트에 리렌더링이 날아와 박힌다.
0. Context API 그만해
이 블로그는 제 기술스택 실습실이자 샌드박스입니다. 단순히 구현하고 싶은 기능이 있으면 구현하고 있는 중입니다. 그런데 다크 모드 및 라이트 모드 기능을 넣고, 랜덤 배경 사진 기능을 넣으니 내비게이션의 폰트 색깔을 동적으로 조정하고 싶어졌습니다. 즉 특정 DOM의 평균 명도를 알아내야 하는 상황이 됐습니다.
export const BackgroundProvider = ({ children }: { children: ReactNode }) => {
...//로직
return (
<BackgroundContext.Provider
value={{ src: imgSrc, brightness, DARK_TEXT_PREFERED, backgroundColor }}
>
<canvas
id="background_image"
width={500}
height={400}
className={HIDE}
ref={canvasRef}
/>
{children}
</BackgroundContext.Provider>
)
}
여기에 구현되어 있는 DARK_TEXT_PREFERED
는 당시 임시로 구현되어 있던 것으로, 의도했던 기능은 특정 DOM의 refObject
값을 전달하면 그 DOM에 어두운 색의 텍스트가 적절한지를 boolean
으로 알려주는 것입니다. 당시에는 imgSrc의 평균 픽셀 명도값을 사용하고 있었습니다. 이 기능을 잘 구현하려면 레이아웃 컴포넌트에 또 Provider를 씌워야 했고, Context를 호출해야 하는데 이 구조대로라면 Provider Hell
과 더불어 향후 무슨 일이 벌어질지 모르겠다는 생각이 들었습니다.
어떻게 구현해야 하는지는 감이 왔는데, Context만으로 관리하던 상태들을 다 뜯어야 한다는 걸 깨달았습니다. 왜냐하면 당시에 인터페이스를 너무 생각 없이(…) 설계했기도 하고, Context만으로 전역 상태를 계속 관리하다가는 앞으로 복잡한 기능을 추가할 때 불필요한 렌더링이 너무 많아질 것 같다는 생각이 들었기 때문입니다. 이 기회에 상태 관리 도구를 추가하면서, 전역 상태들을 쪼개서 관리해야겠다는 생각이 들었습니다.
1. 상태 관리 도구 선정: Zustand
제가 최근에 사용했던 상태 관리 도구는 Jotai
와 Zustand
입니다. 같은 개발자가 개발한 것으로 둘의 가장 큰 차이는, 제가 느끼기에는 상태를 atomic하게 다루느냐와 아니냐였습니다. 복잡한 어플리케이션을 개발해본 경험이 없지만 Jotai의 경우 작은 상태들과 거기에서 조합된 상태들을 다루는 데 용이한 편이고(상태 자체만 남기는 편), Zustand는 상태와 상태를 다루는 로직이 좀더 결합된 편이라고 생각합니다.
Zustand를 선택한 이유는 현재 블로그의 전역 상태들이 비동기나 폼 처리(즉 상태끼리의 결합)보다는 특정 기능 하나 또는 특정 인터페이스의 조합에 모두 묶여 있기 때문입니다. 앞으로도 그럴 것이라는 보장은 없지만 Jotai를 사용해 보았을 때 상태가 많아지고, 흩어졌을 때 코드를 정리하기가 좀 난감하다는 생각을 했었기 때문에 이번에는 Zustand를 사용해 보면서 문제점이 있다면 체감해 볼 요량입니다.
2. Provider 해체하기
2—1. 디렉토리 옮기기
우선 이번에 도입한 랜덤 배경 사진 기능을 위해 내비게이션으로 뭉뚱그려 전달하던 컨텍스트 인터페이스를 쪼개, 상태에 직접적으로 연관되어 있는 컴포넌트로 모두 넣었습니다. 기존에는 아래처럼 providers
를 한 디렉토리에서 모두 관리하고 있었는데(일종의 store
), 이것을

아래처럼 연관된 컴포넌트의 디렉토리에서 관리하는 것으로 변경했습니다. (이 블로그의 디렉토리 구조는 점진적으로 FSD로 옮겨가려고 합니다)

2—2. 리팩토링과 기능 구현
export const BackgroundProvider = ({ children }: { children: ReactNode }) => {
...//로직
return (
<BackgroundContext.Provider
value={{ src: imgSrc, brightness, DARK_TEXT_PREFERED, backgroundColor }}
>
<canvas
id="background_image"
width={500}
height={400}
className={HIDE}
ref={canvasRef}
/>
{children}
</BackgroundContext.Provider>
)
}
Provider 내부에 있던 로직은 그대로 캔버스 컴포넌트와 함께 옮기고, 해당 캔버스 컴포넌트에서 필요한 기능을 구현하면서 아래와 같은 원칙을 세웠습니다.
캔버스와 관련된 로직은 캔버스 컴포넌트와 함께 두기
상태 조작과 관련된 로직은 따로 빼서 상태와 함께 두기
이게 무슨 말인가 하면, 캔버스 컴포넌트에는 캔버스 조작 및 이를 통한 결과값(raw data)만 남기고, 나머지 계산은 slice에서 처리하는 것입니다. 예를 들어 DARK_TEXT_PREFERED
는 imgSrc의 계산된 명도값(brightness, raw data)을 받아 186 초과라면 true를 반환하는데, 이렇게 raw data에서 파생된 상태들을 react에 의존할 필요 없이 연산할 때 slice에서 처리하겠다는 뜻입니다.
구현한 기능은 다음과 같습니다.
특정 DOM의 (key, RefObject) 쌍를 등록해서
중복 체크하고
있으면 해당 DOM 영역 범위의
PREFERS_BLACK_TEXT
값을 모두 제공함
이때 DOM을 등록할 때의 key
로 찾거나, DOM 으로 key
를 찾을 수 있습니다. (코드)
3. 마무리
이렇게 상태 관리 구조를 먼저 개편하고 기능을 구현하니 앓던 이가 빠진 기분이었고요... 나머지 필요한 Context들도 Zustand로 마이그레이션하려고 하는데, 이전에 상태 관리에 대한 생각을 글로 쓴 적이 있었는데 일단 이걸 기준으로 한 번 더 생각해 보려고 합니다.