HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
👻
개발 기록
/
🏢
회사 개발 노트
/
왓챠 노트
왓챠 노트
/
🪛
context는 drilling 때문에만 쓰는 건 아니야
🪛

context는 drilling 때문에만 쓰는 건 아니야

 
👉
실험에 쓰인 코드는 여기
 

고민의 시작점

  • context는 상태 관리.. 그리고 상태 관리는 drilling을 방지하기 위해 사용한다고 배웠다.
  • 하지만 막상 회사에서 코드를 짤 땐 여러 페이지 중 하나의 페이지를 맡아 작업하는 경우가 많았고, 이럴 때 과연 전역 상태를 쓰는 게 맞는 것인가라는 고민을 항상 해왔고, 그럴 때마다 내린 결론은 props로 내리자였다.
  • react를 쓰면 useState를 필연적으로 사용하게 되는 데 useState의 setState를 props로 내려서 해결되는 경우가 생긴다. (state는 다른 컴포넌트의 props로 전달)
  • 근데 코드가 그럴 경우 코드가 너무 이상하다. 이유는 다음과 같다.
    • react가 권장하는 문법이 useState로 하나의 컴포넌트 내의 상태를 자유롭게 관리하는 것으로 알고 있는데 이 관리 방법을 다른 컴포넌트로 위임하는 게 별로다. useState의 state는 호출한 컴포넌트만을 위한 것이지 setState를 다른 컴포넌트로 전달해 이리저리 해당 state를 컨트롤하게 하고 싶지 않다.
    • props으로 전달하면서 생기는 불필요한 코드들. 예를 들어 setState를 전달한 컴포넌트에서 가장 상위에 있는 scope에서 사용하려 하면 오류가 난다.
      • 왜냐하면 setState가 아직 전달되기 전에 불렸기 때문이다. 이럴 때 쓰는 방법이 useEffect!
        지금은 예시 코드라 괜찮아보일지 몰라도 코드가 복잡해지면서 상태도 더 추가되고 점점 더 지저분해지는 걸 느낀다.
 

내 경우에는

  • tab에 따라 컴포넌트를 부르고 해당 컴포넌트에서 필요한 api 데이터를 불러야 했다.
  • 그리고 tab를 컨트롤하는 header가 존재하는데, header는 디자인이 같아 재활용하고자 했다.
  • 이 때 tabOne, tabTwo에서 호출한 데이터의 길이를 header가 알아야 하는 상황이 왔다.
  • 내가 생각한 방법은 3가지이다.
      1. header와 tab 상위 컴포넌트에서 데이터를 불러 props로 내려줌.
      1. useState로 dataLen, setDataLen을 만들어 header에는 dataLen을, tab 컴포넌트들에는 setDataLen을 전달함.
      1. context를 사용해 data와 len을 반환하여 필요한 곳에서 사용.
  • 결론부터 말하자면 c가 제일 좋다. 왜냐하면 코드도 깔끔하고 효율성도 좋기 때문이다.
방법 b; tab을 전환할 때마다 데이터 호출
방법 b; tab을 전환할 때마다 데이터 호출
방법 b; 최초 렌더시 데이터 호출
방법 b; 최초 렌더시 데이터 호출
 
  • 우선 a 방법을 선택하지 않은 이유는 상위 컴포넌트가 복잡해질 것 같아서였다. 그리고 props로 데이터를 내려주면 len이 변할 때마다 header와 tab component 모두 렌더링 시킨다는 것도 별로였다.
  • 방법 b와 c의 코드를 비교해보자.

방법 b

 

방법 c

 

결론

  • 방법 c로 했을 때 컴포넌트마다 관심사를 분리하고 관리할 수 있고 코드도 깔끔해졌고 이상한 props를 내려줌에 따라 처리해야 하는 코드도 모두 없앴다.
  • 이렇게 context는 drilling을 방지하기 위해서도 있지만 위의 예시와 같이 상위 컴포넌트로부터 하위 컴포넌트에 데이터를 효율적으로 관리하게 만들어준다.
  • 앞에도 말했다시피 오히려 회사에선 여러 명과 협업하기 때문에 전역 상태를 건드리는 경우를 많이 못봤는데 몇 개의 컴포넌트를 관리함에 있어 효율적으로 사용할 수 있기에 이번 예시가 context의 더 큰 장점이라 생각한다.
// 예를 들면 이렇게 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> ); };
// 써보면 더 가관이다 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> ); };
// 이런 느낌 return ( <Header tab={tab} /> <div> <button onClick={() => setTab("tab1")}>tab1</button> <button onClick={() => setTab("tab2")}>tab2</button> </div> {tab === "tab1" && <TabOne />} {tab === "tab2" && <TabTwo />} )
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} />} </> ); };
방법 b의 상위 컴포넌트, dataLength, setdataLength를 선언해야 하고 이를 props로 필요한 곳에 내려주는 방식이다.
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> ); };
setState를 props로 받으면 불필요한 type 선언과 useEffect 선언이 수반된다.
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> ); };
context를 사용하여 각 상태를 등록.
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> ); };
불필요한 선언 없이 필요한 props만 전달.
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> ); };
useEffect나 setState 관련 코드 없이 동작 가능.