실험에 쓰인 코드는 여기
고민의 시작점
- context는 상태 관리.. 그리고 상태 관리는 drilling을 방지하기 위해 사용한다고 배웠다.
- 하지만 막상 회사에서 코드를 짤 땐 여러 페이지 중 하나의 페이지를 맡아 작업하는 경우가 많았고, 이럴 때
과연 전역 상태를 쓰는 게 맞는 것인가
라는 고민을 항상 해왔고, 그럴 때마다 내린 결론은props로 내리자
였다.
- react를 쓰면 useState를 필연적으로 사용하게 되는 데 useState의 setState를 props로 내려서 해결되는 경우가 생긴다. (state는 다른 컴포넌트의 props로 전달)
- 근데 코드가 그럴 경우 코드가 너무 이상하다. 이유는 다음과 같다.
- react가 권장하는 문법이 useState로 하나의 컴포넌트 내의 상태를 자유롭게 관리하는 것으로 알고 있는데 이 관리 방법을 다른 컴포넌트로 위임하는 게 별로다. useState의 state는 호출한 컴포넌트만을 위한 것이지 setState를 다른 컴포넌트로 전달해 이리저리 해당 state를 컨트롤하게 하고 싶지 않다.
- props으로 전달하면서 생기는 불필요한 코드들. 예를 들어 setState를 전달한 컴포넌트에서 가장 상위에 있는 scope에서 사용하려 하면 오류가 난다.
// 예를 들면 이렇게 export const TypeOne = ({ setDataLength }: Props) => { if(!typeOneData.length) { setDataLength(0); } return ( <ul> {typeOneData.length && typeOneData.map((d) => ( <div key={d.id}> <li>{d.content}</li> <li>{d.isLiked ? "like" : "hate"}</li> <li>{d.userName}</li> </div> ))} {!typeOneData.length && <Empty />} </ul> ); };
왜냐하면 setState가 아직 전달되기 전에 불렸기 때문이다. 이럴 때 쓰는 방법이 useEffect!
// 써보면 더 가관이다 export const TypeOne = ({ setDataLength }: Props) => { useEffect(() => { setDataLength(typeOneData.length); }, [typeOneData, setDataLength]); return ( <ul> {typeOneData.length && typeOneData.map((d) => ( <div key={d.id}> <li>{d.content}</li> <li>{d.isLiked ? "like" : "hate"}</li> <li>{d.userName}</li> </div> ))} {!typeOneData.length && <Empty />} </ul> ); };
지금은 예시 코드라 괜찮아보일지 몰라도 코드가 복잡해지면서 상태도 더 추가되고 점점 더 지저분해지는 걸 느낀다.
내 경우에는
- tab에 따라 컴포넌트를 부르고 해당 컴포넌트에서 필요한 api 데이터를 불러야 했다.
- 그리고 tab를 컨트롤하는 header가 존재하는데, header는 디자인이 같아 재활용하고자 했다.
// 이런 느낌 return ( <Header tab={tab} /> <div> <button onClick={() => setTab("tab1")}>tab1</button> <button onClick={() => setTab("tab2")}>tab2</button> </div> {tab === "tab1" && <TabOne />} {tab === "tab2" && <TabTwo />} )
- 이 때 tabOne, tabTwo에서 호출한 데이터의 길이를 header가 알아야 하는 상황이 왔다.
- 내가 생각한 방법은 3가지이다.
- header와 tab 상위 컴포넌트에서 데이터를 불러 props로 내려줌.
- useState로 dataLen, setDataLen을 만들어 header에는 dataLen을, tab 컴포넌트들에는 setDataLen을 전달함.
- context를 사용해 data와 len을 반환하여 필요한 곳에서 사용.
- 결론부터 말하자면 c가 제일 좋다. 왜냐하면 코드도 깔끔하고 효율성도 좋기 때문이다.
.gif?table=block&id=964042a1-fba9-4964-af9e-2f2388a21035&cache=v2)

- 우선 a 방법을 선택하지 않은 이유는 상위 컴포넌트가 복잡해질 것 같아서였다. 그리고 props로 데이터를 내려주면 len이 변할 때마다 header와 tab component 모두 렌더링 시킨다는 것도 별로였다.
- 방법 b와 c의 코드를 비교해보자.
방법 b
export const TypeContainer = () => { const [type, setType] = useState<Type>("type1"); const [dataLength, setDataLength] = useState(0); return ( <> <TypeHeader type={type} dataLength={dataLength} /> <div> <button onClick={() => setType("type1")}>type1</button> <button onClick={() => setType("type2")}>type2</button> </div> {type === "type1" && <TypeOne setDataLength={setDataLength} />} {type === "type2" && <TypeTwo setDataLength={setDataLength} />} </> ); };
interface Props { setDataLength: Dispatch<SetStateAction<number>>; // 타입 정의도 별로... } export const TypeOne = ({ setDataLength }: Props) => { const fetchTypeOneData = () => { console.log("fetchTypeOneData"); return typeOneDataRaw; }; const typeOneData = useMemo(fetchTypeOneData, []); useEffect(() => { setDataLength(typeOneData.length); }, [typeOneData, setDataLength]); return ( <ul> {typeOneData.length && typeOneData.map((d) => ( <div key={d.id}> <li>{d.content}</li> <li>{d.isLiked ? "like" : "hate"}</li> <li>{d.userName}</li> </div> ))} {!typeOneData.length && <Empty />} </ul> ); };
방법 c
import { createContext, PropsWithChildren, useMemo } from "react"; import tabOneDataRaw from "../json/one.json"; import tabTwoDataRaw from "../json/two.json"; export const TabOneContext = createContext<TabOneContextType | null>(null); export const TabTwoContext = createContext<TabTwoContextType | null>(null); interface Props {} export const TabContext = ({ children }: PropsWithChildren<Props>) => { const fetchTabOneData = () => { console.log("fetchTabOneData"); return tabOneDataRaw; }; const fetchTabTwoData = () => { console.log("fetchTabTwoData"); return tabTwoDataRaw; }; const tabOneData = useMemo(fetchTabOneData, []); const tabOneLength = tabOneData.length; const tabTwoData = useMemo(fetchTabTwoData, []); const tabTwoLength = tabTwoData.length; return ( <TabOneContext.Provider value={{ tabOneData, tabOneLength }}> <TabTwoContext.Provider value={{ tabTwoData, tabTwoLength }}> {children} </TabTwoContext.Provider> </TabOneContext.Provider> ); };
export const TabContainer = () => { const [tab, setTab] = useState<Tab>("tab1"); return ( <TabContext> <Header tab={tab} /> <div> <button onClick={() => setTab("tab1")}>tab1</button> <button onClick={() => setTab("tab2")}>tab2</button> </div> {tab === "tab1" && <TabOne />} {tab === "tab2" && <TabTwo />} </TabContext> ); };
export const TabOne = () => { return ( <TabOneContext.Consumer> {(value) => value?.tabOneLength ? ( value?.tabOneData.map((v) => ( <ul key={v.id}> <li>{v.content}</li> <li>{v.isLiked ? "like" : "hate"}</li> <li>{v.userName}</li> </ul> )) ) : ( <Empty /> ) } </TabOneContext.Consumer> ); };
결론
- 방법 c로 했을 때 컴포넌트마다 관심사를 분리하고 관리할 수 있고 코드도 깔끔해졌고 이상한 props를 내려줌에 따라 처리해야 하는 코드도 모두 없앴다.
- 이렇게 context는 drilling을 방지하기 위해서도 있지만 위의 예시와 같이 상위 컴포넌트로부터 하위 컴포넌트에 데이터를 효율적으로 관리하게 만들어준다.
- 앞에도 말했다시피 오히려 회사에선 여러 명과 협업하기 때문에 전역 상태를 건드리는 경우를 많이 못봤는데 몇 개의 컴포넌트를 관리함에 있어 효율적으로 사용할 수 있기에 이번 예시가 context의 더 큰 장점이라 생각한다.