치춘짱베리굿나이스

React의 useContext 본문

ClientSide/React

React의 useContext

치춘 2023. 5. 6. 17:08

useContext

개요

이번에 쪼끄만 자기소개 페이지를 SSR과 SSG를 섞어서 만들어보고 있는데 아무래도 외부 라이브러리를 가져다가 쓸 만큼 프로젝트 규모가 크지 않다 보니… 라이브러리 설치가 조금 꺼려지는 상황에서 상태값을 자식의 자식의 자식한테 내려줘야 하는 일이 생겼다

3~4 depth 정도이니 아예 Prop Drilling 으로 처리할까 했는데, 그보다 먼저 useContext 를 이용하여 상태값의 활동 범위를 넓혀줄 수 있겠다는 생각이 들어서 여태껏 한번도 써 보지 않은 useContext를 써 보기로 했다

useContext란?

각 컴포넌트에서 context를 읽고 구독할 수 있도록 도와주는 React Hook 이라고 한다

쉽게 말해 어떠한 상태값을 context 형태로 감싼 뒤, 이를 다른 컴포넌트가 구독하게끔 하여 상태값의 변화를 감지하고 사용할 수 있게끔 도와주는 것이다

전역 상태관리 라이브러리와 역할이 비슷하다고 보면 된다

useContext 훅 단독으로 사용되진 않고, createContext 와 함께 사용된다

  • createContext : 편지봉투를 만드는 공장
  • context: 편지봉투
  • context.Provider: 집배원의 활동 반경을 정의하는 역할
  • useContext: 도착한 편지 개봉하는 역할

맞는 비유인진 잘 모르겠지만 이렇게 생각하면 조금 쉽진 않을까…

사용 방법

context 생성

export const TextContext = createContext('');

다른 함수가 구독할 수 있는 context를 하나 생성해 보자

createContext는 인자로 context의 기본값을 받고, context 객체를 반환한다

만약 기본값으로 설정하고픈 값이 딱히 없을 경우, null로 설정하면 된다

 

주의할 점은, context 객체 자체는 값을 갖고 있지 않고, 컴포넌트가 어떤 값을 읽거나 구독해야 하는지 알려주는 객체일 뿐이다

뒤에서 사용할 <context 객체>.Provider 를 이용하여 context가 가리킬 상태값을 정의하고, useContext로 읽어들이도록 할 것이다

context 를 다른 컴포넌트에 제공

const App = () => {
    const [text, setText] = useState('');
    return (
        <TextContext.Provider value={text}>
            <Page />
        </TextContext>
    );
};

context 값을 다른 컴포넌트에 전달할 때는 Provider 로 컴포넌트를 감싼다

여러 컴포넌트가 context를 구독하게끔 하고 싶다면 상위 컴포넌트를 감싸거나, 한번에 여러 컴포넌트를 Fragment를 이용하여 감싸주면 된다

위의 text 처럼 상위 컴포넌트에서 context로 사용할 상태값을 정의하고 value로 상태값을 넘겨주면 하위 컴포넌트는 이 context를 구독함으로서 안에 있는 상태값을 꺼내 사용할 수 있다

제공받은 context 읽어들이기

const Title = () => {
    const text = useContext(TextContext);    

    return <h1>{text}</h1>;
};

자식 컴포넌트에서 context를 읽어들이기 위해서는 useContext를 호출한다

useContextcontext에서 상태값을 읽어들이며, 읽은 상태값을 반환한다

 

useContext는 조상 컴포넌트 중 가장 가까이 있는 <context 객체>.Provider 를 찾아 그 value를 가져오는데, 만약 적절한 Provider 가 없을 경우 createContext 함수에 인자로 넘겼던 기본값 (default value) 을 사용한다

<context 객체>.Provider는 반드시 useContext를 호출하는 컴포넌트의 상위에 존재해야 한다

useContext의 문제점

const Test = () => {
    return (
    <TestProvider>
      <>
        <TestComponent2 />
        <TestComponent3 />
      </>
    </TestProvider>
  );
};

const TestProvider = ({ children }: { children: JSX.Element }) => {
  const [text, setText] = useState("");

  return (
    <TestContext.Provider value={{ text, setText }}>
      {children}
    </TestContext.Provider>
  );
};

const TestComponent1 = () => {
    const { text, setText } = useContext(TestContext);

  const handleChange = useCallback(
    (e: ChangeEvent<HTMLInputElement>) => {
      setText(e.target.value);
    },
    [setText]
  );

  return (
    <div>
      {text} in TestComponent1
      <input value={text} onChange={handleChange} />
    </div>
  );
};

const TestComponent2 = () => {
    console.log("TestComponent2 Rerendered");
  return <TestComponent1 />;
};

const TestComponent3 = () => {
    useContext(TestContext);
  console.log("TestComponent3 Rerendered");
  return <div>This is TestComponent3</div>;
};

이런 코드가 있다고 해 보자

TestComponent1에서는 text 조작과 출력을 모두 하므로, text가 변경될 때마다 리렌더링이 발생한다

TestComponent2TestComponent1의 부모일 뿐, textsetText를 사용하지 않는다

부모 컴포넌트가 리렌더링될 때 자식 컴포넌트가 리렌더링되는 건 맞지만, 자식 컴포넌트가 리렌더링된다고 해서 부모 컴포넌트가 리렌더링 되진 않는다

또한 TestComponent3은 useContext를 호출만 할 뿐, 아무런 값도 사용하지 않으므로 (리렌더링에 관여하는 상태값이 아예 없으므로) 리렌더링이 발생해서는 안 된다

따라서 두 컴포넌트 모두 text 상태값이 변경되어도 리렌더링이 되지 않아야만 할 것 같다

 

타자를 칠 때마다 TestComponent3이 리렌더링된다 (ㅋㅋ)

 

Provider 아래에서 useContext를 구독하는 모든 컴포넌트들은 Providervalue가 변경될 때마다 자동으로 리렌더링된다고 한다

TestComponent3은 리렌더링 될 이유가 없는데 리렌더링이 일어나고 있는 것이다 (????)

컴포넌트의 리렌더링 조건

컴포넌트는 다음과 같은 조건에서 리렌더링 된다

  1. 컴포넌트 내부의 상태값, 또는 외부에서 받는 Props에 변화가 있을 때
  2. 부모가 리렌더링되었을 때
  3. context 구독 중에 (useContext 훅 호출됨), context에 변화가 발생하였을 때
  4. Force Update (클래스 컴포넌트 시절에 있던 메서드)

해결법

...
const TestComponent2 = React.memo(() => {
  console.log('TestComponent2 Rerendered');
  return <TestComponent1 />;
});

const TestComponent3 = React.memo(() => {
  console.log('TestComponent3 Rerendered');
  return <div>This is TestComponent3</div>;
});

 

위와 같이 TestComponent3을 메모이제이션 하면 더이상TestComponent3에서는 리렌더링이 발생하지 않기는 한다

하지만 위처럼 간단한 상황이 아니라 엄청 많은 컴포넌트가 엮어져 있는 상황이라면 모든 컴포넌트에 React.memo를 붙여줘야 할까…?

return (
    <>
    <TestContext.Provider value={{ text }}>
      <TestComponent1 />
    </TestContext.Provider>
      <SetTestContext.Provider value={{ setText }}>
            <TestComponent3 />
      </SetTestContext.Provider>
    </>
);

또 하나의 방법으로는 상태값을 분리해서 Provider를 여러 개 분리하고, context가 필요한 컴포넌트 트리에만 Provider로 감싸주는 방법이 있으나, 이는 상태값 구조가 복잡해질 수록 오히려 골치아파질 수도 있다

언제 useContext를 사용할까?

내가 생각하기에 useContext를 사용하기 적합한 타이밍은 다음과 같다 (주관적)

  • Props Drilling은 일어나지만, useContext에 의한 리렌더링이 발생해도 다른 컴포넌트에 영향이 적을 정도로 간단한 구조일 때 (Depth가 비교적 얕을 때)
  • 프로젝트 전체에 전역 상태관리를 할만한 값이 그렇게 많지 않을 때

아무래도 프로젝트가 커지면 커질 수록 context 하나에 영향받는 컴포넌트가 늘어날 테니 그럴 때는 Redux나 Recoil을 찾아가는 게 낫지 않을까 하는 생각이 든다…


참고 자료

https://yrnana.dev/post/2021-08-21-context-api-redux/

https://leewarrick.com/blog/the-problem-with-context/

https://react.dev/reference/react/useContext

https://react.dev/reference/react/createContext

'ClientSide > React' 카테고리의 다른 글

useMemo, useCallback  (0) 2023.07.24
마운트와 렌더링  (0) 2023.07.24
Suspense와 Error Boundary를 이용한 로딩과 예외처리  (0) 2022.12.10
React를 클론코딩 #1 가상 돔  (0) 2022.10.05
React  (0) 2022.10.01
Comments