본문 바로가기
React-study/dil

[DIL] useContext

by 어느새벽 2024. 5. 29.

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

const value = useContext(SomeContext)

 

컴포넌트의 최상위 레벨에서 useContext를 호출하여 context를 읽고 구독한다.

 

import { useContext } from 'react';

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

 

매개변수 

context : 이전에 createContext로 생성한 context 이다. context자체는 정보를 보유하지 않으며, 컴포넌트에서 제공하거나 읽을 수 있는 정보의 종류를 나타낼 뿐이다.

 

반환값

useContext는 호출하는 컴포넌트에 대한 context 값을 반환한다. 이 값은 호출한 컴포넌트에 트리상 위에 있는 가장 가까운 SomeContenxt.Provider에 전달된 value이다. 이러한 provider가 없는 경우 반환되는 값은 해당 contenxt에 대해 createContext에 전달한 defaultValue가 된다. 반환된 값은 항상 최신 값이다. 리액트는 context가 변경되면 context를 읽는 컴포넌트를 자동으로 리렌더링 한다.

 

주의사항

  • 컴포넌트의 useContext() 호출은 동일한 컴포넌트에서 반환된 provider의 영향을 받지 않는다. 해당 <Context.Provider>는 반드시 useContext() 호출을 수행하는 컴포넌트의 위에 있어야 한다.
  • 리액트는 변경된 value를 받는 provider부터 시작해서 해당 context를 사용하는 자식들에 대해서까지 전부 자동으로 리렌더링한다. 이전 값과 다음 값은 Object.is로 비교한다. memo로 리렌더링을 건너뛰어도 새로운 context 값을 수신하는 자식들을 막지는 못한다.
  • 빌드 시스템이 출력 결과에 중복 모듈을 생성하는 경우(심볼릭 링크를 사용하는 경우 발생할 수 있음) context가 손상될 수 있다. context를 통해 무언가를 전달하는 것은 === 비교에 의해 결정되는 것처럼 context를 제공하는 데 사용하는 SomeContext와 context를 읽는 데 사용하는 SomeContext가 정확하게 동일한 객체인 경우에만 작동한다.

사용법

트리 깊숙이 데이터 전달하기

컴포넌트의 최상위 레벨에서 useContext를 호출하여 context를 읽고 구독한다.

import { useContext } from 'react';

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

 

useContext는 전달한 context에 대한 context값을 반환한다. context 값을 결정하기 위해 리액트는 컴포넌트 트리를 검색하고 특정 context에 대해 위에서 가장 가까운 context provider를 찾는다.

context를 button에 전달하려면 해당 버튼 또는 상위 컴포넌트 중 하나를 해당 context provider로 감싼다.

 

function MyPage() {
  return (
    <ThemeContext.Provider value="dark">
      <Form />
    </ThemeContext.Provider>
  );
}

function Form() {
  // ... renders buttons inside ...
}

 

provider와 Button 사이에 얼마나 많은 컴포넌트 레이어가 있는지는 중요하지 않다. Form내부의 Button이 useContext(ThemeContext)를 호출하면 "dark"를 값으로 받는다.

 

useContext()는 항상 그것을 호출하는 컴포넌트 의 가장 가까운 provider를 찾는다.

useContext()를 호출하는 컴포넌트 내의 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>
    </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 }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className}>
      {children}
    </button>
  );
}

 

context를 통해 전달된 데이터 업데이트하기

시간이 지남에 따라 context가 변경되기를 원하는 경우가 종종 있다. context를 업데이트하려면 state와 결합해야 한다. 부모 컴포넌트에 state 변수를 선언하고 현재 state를 context 값으로 provider에 전달한다.

function MyPage() {
  const [theme, setTheme] = useState('dark');
  return (
    <ThemeContext.Provider value={theme}>
      <Form />
      <Button onClick={() => {
        setTheme('light');
      }}>
        Switch to light theme
      </Button>
    </ThemeContext.Provider>
  );
}

 

이제 provider 내부의 모든 Button은 현재 theme 값을 받게 된다. provider에게 전달한 theme 값을 업데이트 하기 위해 setTheme를 호출하면 모든 Button 컴포넌트가 새로운 light 값으로 리렌더링된다.

 

fallback 기본값 지정하기

리액트가 부모 트리에서 특정 context의 provider들을 찾을 수 없는 경우, useContext()가 반환하는 context값은 해당 context를 생성할 때 지정한 기본값과 동일하다. 

const ThemeContext = createContext(null);

 

기본값은 절대 변경되지 않는다. context를 업데이트하려면 앞서 설명된 대로 state를 사용한다.

null 대신 기본값으로 사용할 수 있는 더 의미 있는 값이 있는 경우가 많다. 예를들어

const ThemeContext = createContext('light');

 

이렇게 하면 실수로 해당 provider없이 일부 컴포넌트를 렌더링해도 중단되지 않는다. 또한 테스트 환경에서 많은 provider를 설정하지 않고도 컴포넌트가 테스트 환경에서 잘 작동하는 데 도움이 된다.

 

아래 예시에서 '테마 전환' 버튼은 테마 context provider 외부에 있고 기본 context 테마 값이 'light'이므로 항상 밝게 표시된다. 기본 테마를 'dark'으로 편집하면

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

const ThemeContext = createContext('light');

export default function MyApp() {
  const [theme, setTheme] = useState('light');
  return (
    <>
      <ThemeContext.Provider value={theme}>
        <Form />
      </ThemeContext.Provider>
      <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 재정의하기

트리의 일부분을 다른 값의 provider로 감싸 해당 부분에 대한 context를 재정의할 수 있다.

<ThemeContext.Provider value="dark">
  ...
  <ThemeContext.Provider value="light">
    <Footer />
  </ThemeContext.Provider>
  ...
</ThemeContext.Provider>

 

필요한 만큼 provider들을 중첩하고 재정의할 수 있다.

 

객체 및 함수 전달 시 리렌더링 최적화 

context를 통해 객체와 함수를 포함한 모든 값을 전달할 수 있다.

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 값은 두 개의 프로퍼티를 가진 JavaScript 객체이며, 그 중 하나는 함수이다. MyApp이 리렌더링할 때마다 (ex.라우트 업데이트), 이것은 다른 함수를 가리키는 다른 객체가 될 것이므로 리액트는 useContext(AuthContext)를 호출하는 트리 깊숙한 곳의 모든 컴포넌트도 리렌더링해야 한다.

 

소규모 앱에서는 문제가 되지 않는다. 그러나 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이 리렌더링해야 하는 경우에도 currentUser가 변경되지 않는 한 useContext(AuthProvider)를 호출하는 컴포넌트는 리렌더링할 필요가 없다.

 

'React-study > dil' 카테고리의 다른 글

[DIL] useEffect  (0) 2024.06.04
[DIL] useRef  (0) 2024.05.30
[DIL] useReducer(2)  (0) 2024.05.28
[DIL] useReducer(1)  (0) 2024.05.27
[DIL] useState 사용법  (0) 2024.05.20