카테고리 없음

useContext()에 대한 정리

불곰자리 2023. 11. 29. 10:16

useContext라는 리액트 기본 훅에 대해서 배웠는데, 여러 개념에 대해 헷갈리는 부분이 있어 정리하려고 한다.

다음은 리액트 공식 문서를 번역한 글이다.

 

useContext는 컴포넌트에서 context를 구독할 수 있고, 조회할 수 있게 해주는 리액트 훅이다.

 

참조

useContext(SomeContext)

컴포넌트가 읽고 구독하기 위해선, 사용하는 컴포넌트의 상위 레벨에서 useContext를 호출한다.

import { useContext } from 'react';

function MyComponent() {
  const theme = useContext(ThemeContext);
  // ...

 

구독(subscribe)라는 단어의 의미가 감이 안 왔었는데, 컴포넌트를 구독한다는 것은 구독당한 컴포넌트가 변화했을 때 구독한 컴포넌트가 변화에 대해 알 수 있다는 것을 뜻하는 것 같다. (우리가 아는 구독의 의미와 비슷한 것 같다.) 

Redux를 예로 들어 설명하면 '구독' 시에 변화 시 실행할 이벤트 리스너를 생성하고, 상태를 관찰하고 상태가 변화했을 때마다 유저에게 알린다. (Redux의 핵심은 거대한 이벤트 생성기라는 점에서) 유저는 dispatch를 사용해 상태의 변화를 store에 알린다. dispatch 함수는 상태가 어떻게 변화할지 보여주는 순수함수(항상 실행 결과 값이 동일한)로, action을 파라미터로 취한다. dispatch가 실행되면 store를 생성하는 reducer 함수는 현재 상태 트리와 action과 함께 호출되고 트리에 다음 상태를 반환, 리스너를 변경한다. 

 

또, React에서 사용하는 Context에 대한 개념에 대해서도 헷갈렸다. 이에 대해서 공식 문서를 찾아보니, 자식 컴포넌트에 props를 넘겨주기 위해 (해당 props가 필요 없는) 중간 컴포넌트에까지 props를 넘겨주는 prop drilling 문제에 대한 대안으로 사용할 수 있다고 한다. (컴포넌트의 깊이에 관계 없이 Context로 감싼 컴포넌트는 Context의 데이터를 사용할 수 있음)

Context를 생성하는 법은 다음과 같다.

1. Context를 생성한다.
2. 데이터가 필요한 컴포넌트에서 Context를 사용한다. 
3. 명시한 데이터에 대해 Context에서 제공한다. 

 

파라미터

SomeContext

createContext로 생성한 Context. Context 자체는 정보를 지니고 있지 않으며, 컴포넌트에서 제공하거나 읽을 수 있는 정보의 종류를 제공한다. 

 

반환 값

컴포넌트라고 불리는 Context 값을 반환한다. 트리 내에서 SomeContext.Provider 컴포넌트를 호출할 때 value로써 전달된다. Provider가 없다면 반환하는 값은 해당 Context를 위해 createContext로 넘긴 defaultValue를 반환한다. 반환된 값은 항상 최신 상태를 유지한다. ReactContext 변경 시 그것을 읽고 자동으로 리렌더링을 수행한다.

 

주의 사항

  • 컴포넌트 내에서 호출되는 useContext()는 같은 컴포넌트에서 반환되는 provider에는 영향을 주지 않는다. 일치하는 Context.ProvideruseContext 호출 하는 컴포넌트의 위에 있어야 한다.
  • React는 다른 값을 받는 Provider에서부터 시작해 특정 Context를 사용하는 모든 자식 컴포넌트를 자동으로 리렌더링한다. 이전 값과 다음 값은 Object.is로 비교할 수 있다. memo로 리렌더링 시 자식 컴포넌트가 새로운 Context 값을 받는 것을 방지할 수는 없다.
  • 빌드 시스템이 결과값으로 중복된 모듈을 생성한다면(symlinks에서 일어날 수 있음), Context를 파괴할 수 있다. Context를 통해 무언가를 넘겨주는 것은 Provide Context 사용을 위한 SomeContext를 사용했을 때나, === 연산자로 결정이 되는 같은 객체를 읽고자 SomeContext를 사용할 때만 작용한다.

 

사용

1. 트리의 깊은 곳에 데이터를 전달하고자 할 때

useContext는 전달한 Context를 위한 Context 값을 반환한다. Context 값을 판별하기 위해, React는 컴포넌트 트리를 탐색하고, 특정 Context의 가장 가까운 상위 계층에서 제공하는 Context Provider를 찾아낸다. 참고로 Providerstore에 접금할 수 있도록 해주는 역할을 맡고 있다.

 

아래는 공식 문서에서 제공하는 예시다. 

import { createContext, useContext } from 'react';

const ThemeContext = createContext(null);

// 제일 상위 컴포넌트
export default function MyApp() { 
  return (
    <ThemeContext.Provider value="dark">
      <Form />
    </ThemeContext.Provider>
  )
}

// 두 번째 컴포넌트
function Form() {
  return (
    <Panel title="Welcome">
      <Button>Sign up</Button>
      <Button>Log in</Button>
    </Panel>
  );
}

// 세 번째 컴포넌트
function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      <h1>{title}</h1>
      {children}
    </section>
  )
}

// Sign up과 Log in 버튼을 생성하는 컴포넌트
function Button({ children }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className}>
      {children}
    </button>
  );
}

 

시간이 지남에 따라 Context를 변경하고 싶을 때, 상태와 결합하여 Context를 업데이트할 수 있다. 부모 컴포넌트에 상태 변수를 선언하고, Provider에 현재 값을 Context 값으로써 전달한다.

 

이 외에도 여러 예시가 있으니 보면 좋을 것 같다.

 

참고로 Provider에 값을 넘겨줄 때 theme과 같이 다른 이름으로 넘겨주어서는 안 되고, value로 써주어야 한다.

// 🚩 작동하지 않음: prop은 "value" 값으로 넘겨져야 함
<ThemeContext.Provider theme={theme}>
   <Button />
</ThemeContext.Provider>

 

Fallback의 기본 값 지정

부모 트리에서 특정 ContextProvider가 없다면 Context 값은 Context 생성 시 기본 값으로 전달된다.

const ThemeContext = createContext(null);

 

기본 값은 절대 변하지 않는다. Context를 업데이트하고 싶다면 위의 (useState를 사용한) 예시를 참고한다. null 이외에, 기본 값으로는 여러 값이 들어갈 수 있다. 예를 들어,

const ThemeContext = createContext('light');

 

이 경우엔, 일치하는 Provider 없이 컴포넌트를 렌더링하게되는 상황에서도 오류가 생기지 않는다. 테스트 시에도 Provider 값을 따로 설정할 필요가 없어 편하다.

 

import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext('light');

export default function MyApp() {
  const [theme, setTheme] = useState('dark');
  return (
    <>
      <ThemeContext.Provider value={theme}>
        <Form />
      </ThemeContext.Provider>
      {/* Provider 바깥에 Button이 있어서 클릭해도 색이 변경되지 않는다. */}
      <Button onClick={() => {
        setTheme(theme === 'dark' ? 'light' : 'dark');
      }}>
        Toggle theme
      </Button>
    </>
  )
}

function Form({ children }) {
  return (
    <Panel title="Welcome">
      <Button>Sign up</Button>
      <Button>Log in</Button>
    </Panel>
  );
}

function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Button({ children, onClick }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className} onClick={onClick}>
      {children}
    </button>
  );
}

 

Context Overriding 

같은 Provider를 다른 값으로 감싸서 오버라이드 할 수 있다.

import { createContext, useContext } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
  return (
    <ThemeContext.Provider value="dark">
      <Form />
    </ThemeContext.Provider>
  )
}

function Form() {
  return (
    <Panel title="Welcome">
      <Button>Sign up</Button>
      <Button>Log in</Button>
      {/* Footer만 light 테마를 적용시킬 수 있다. */}
      <ThemeContext.Provider value="light">
        <Footer />
      </ThemeContext.Provider>
    </Panel>
  );
}

function Footer() {
  return (
    <footer>
      <Button>Settings</Button>
    </footer>
  );
}

function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      {title && <h1>{title}</h1>}
      {children}
    </section>
  )
}

function Button({ children }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className}>
      {children}
    </button>
  );
}

 

객체나 함수를 넘겨줄 때 리렌더링 최적화하기

function MyApp() {
  const [currentUser, setCurrentUser] = useState(null);

  function login(response) {
    storeCredentials(response.credentials);
    setCurrentUser(response.user);
  }

  return (
    <AuthContext.Provider value={{ currentUser, login }}>
      <Page />
    </AuthContext.Provider>
  );
}

 

Context에 넘겨주는 값이 함수나 객체가 될 수 있는데, 리렌더링 시 Context 값은 다른 함수를 가리키는 다른 객체 포인터가 될 수 있는데, 그 때 ReactuseContext를 호출한 깊이까지의 모든 컴포넌트를 리렌더링 할 수도 있다. 그러나 currentUser와 같은 값이 변하지 않았는데도 리렌더링 되는 것은 성능상 좋지 않을 수도 있기 때문에 login 함수useCallback 훅을 통해 감싸고, 객체useMemo를 통해 감싸면 이득을 취할 수 있을 것이다.

 

import { useCallback, useMemo } from 'react';

function MyApp() {
  const [currentUser, setCurrentUser] = useState(null);

  const login = useCallback((response) => {
    storeCredentials(response.credentials);
    setCurrentUser(response.user);
  }, []);

  const contextValue = useMemo(() => ({
    currentUser,
    login
  }), [currentUser, login]);

  return (
    <AuthContext.Provider value={contextValue}>
      <Page />
    </AuthContext.Provider>
  );
}

 

이 변화의 결과로, MyApp이 리렌더링 되더라도, useContext를 호출한 컴포넌트는 currentUser가 변경되지도 않았는데 다시 불필요하게 리렌더링 하지 않을 것이다.