Context Hook - useContext

Doozuu·2025년 3월 29일
0

React

목록 보기
29/30
post-thumbnail

context

context는 전역적인 데이터를 컴포넌트 트리 내에서 임의의 깊이를 가진 컴포넌트들에게 전달하는 방법이다. (쉽게 말해서 깊이에 관계없이 타겟 컴포넌트에 필요한 데이터를 전달할 수 있는 방법)

일반적으로 props를 사용해서 데이터를 전달할 때는 부모 컴포넌트에서 자식 컴포넌트로만 데이터를 전달할 수 있지만, context를 사용하면 컴포넌트 트리의 깊은 곳에 있는 컴포넌트들에도 간편하게 데이터를 전달할 수 있다.


useContext

컴포넌트에서 context를 읽고 사용할 수 있도록 도와주는 React Hook이다.

형태

const value = useContext(SomeContext)

Reference

useContext(SomeContext)

매개변수

someContext : createContext로 생성한 context이다.
컴포넌트에서 제공하거나 읽을 수 있는 정보의 종류를 나타낸다.

반환값

useContext는 호출하는 컴포넌트에 대한 context 값을 반환한다.
이 값은 트리에서 호출하는 컴포넌트 상위의 가장 가까운 SomeContext.Provider에 전달된 값으로 결정된다.
Provider가 없으면 반환된 값은 해당 context에 대해 createContext에 전달한 defaultValue가 된다.
Context가 변경되면 React는 자동으로 해당 Context를 읽는 컴포넌트를 다시 렌더링한다.

주의사항

  • useContext를 사용할 때는 항상 해당 context의 Provider가 그 컴포넌트보다 상위에 있어야 한다.
  • Provider의 값이 바뀌면 그 값을 사용하는 자식 컴포넌트들이 자동으로 리렌더링된다. 즉, Provider가 새 값을 전달하면 그 값을 사용하는 모든 컴포넌트가 다시 렌더링된다. 이때 렌더링 최적화를 위해 React.memo를 사용해도 컨텍스트 값 변경에 따른 리렌더링은 막을 수 없다.
  • 만약 빌드 시스템에서 동일한 모듈을 두 번 이상 불러온다면 컨텍스트가 제대로 동작하지 않을 수 있다. (SomeContext가 두 개의 다른 객체처럼 취급되기 때문)

Usage

트리의 깊은 곳에 데이터 전달하기

컴포넌트의 최상위 레벨에서 useContext를 호출하여 Context를 읽고 사용한다.

import { useContext } from 'react';

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

useContext를 전달한 context에 대한 context value를 반환하다.
Context 값을 결정하기 위해 React는 컴포넌트 트리를 탐색하고 특정 Context에 대해 상위에서 가장 가까운 Context Provider를 찾는다.

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

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

function Form() {
  // ... 내부에서 버튼을 렌더링합니다. ...
}

주의사항

useContext는 현재 컴포넌트에서 호출된 위치보다 상위에 있는 Context.Provider를 찾는다. 쉽게 말해서, useContext는 자신이 호출된 컴포넌트보다 위쪽에 있는 Provider에서 값을 가져오고, 자신의 컴포넌트 내부에 있는 Provider는 무시한다.

사용 예시

  1. createContext를 이용해 테마 관련 context를 생성한다.
  2. context provider로 Form을 감싸고, 값을 제공한다.(다크 모드)
  3. Form 내부 패널과 버튼에서 useContext를 통해 테마 값을 사용한다.
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 value로 provider에 전달한다.
  • Provider에 전달된 theme 값을 업데이트 하기 위해 setTheme을 호출하면, 모든 Button 컴포넌트가 새로운 'light' 값으로 다시 렌더링된다.
function MyPage() {
  const [theme, setTheme] = useState('dark');
  return (
    <ThemeContext.Provider value={theme}>
      <Form />
      <Button onClick={() => {
        setTheme('light');
      }}>
        Switch to light theme
      </Button>
    </ThemeContext.Provider>
  );
}

예시

  • 다크 모드/ 라이트 모드 구현
    value="dark"는 "dark" 문자열을 전달하지만, value={theme}는 JSX 중괄호를 사용하여 자바스크립트 theme 변수 값을 전달한다.
    중괄호를 사용하면 문자열이 아닌 Context 값도 전달할 수 있다.
import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={theme}>
      <Form />
      <label>
        <input
          type="checkbox"
          checked={theme === 'dark'}
          onChange={(e) => {
            setTheme(e.target.checked ? 'dark' : 'light')
          }}
        />
        Use dark mode
      </label>
    </ThemeContext.Provider>
  )
}

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 }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className}>
      {children}
    </button>
  );
}

예시 2

  • 객체 업데이트
    state가 객체인 경우, { currentUser, setCurrentUser }를 하나의 객체로 결합하여 value={} 내부의 Context를 통해 전달한다.
    이렇게 하면 LoginButton과 같은 하위의 모든 컴포넌트가 currentUser와 setCurrentUser를 모두 읽은 다음 필요할 때 setCurrentUser를 호출할 수 있다.
import { createContext, useContext, useState } from 'react';

const CurrentUserContext = createContext(null);

export default function MyApp() {
  const [currentUser, setCurrentUser] = useState(null);
  return (
    <CurrentUserContext.Provider
      value={{
        currentUser,
        setCurrentUser
      }}
    >
      <Form />
    </CurrentUserContext.Provider>
  );
}

function Form({ children }) {
  return (
    <Panel title="Welcome">
      <LoginButton />
    </Panel>
  );
}

function LoginButton() {
  const {
    currentUser,
    setCurrentUser
  } = useContext(CurrentUserContext);

  if (currentUser !== null) {
    return <p>You logged in as {currentUser.name}.</p>;
  }

  return (
    <Button onClick={() => {
      setCurrentUser({ name: 'Advika' })
    }}>Log in as Advika</Button>
  );
}

function Panel({ title, children }) {
  return (
    <section className="panel">
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Button({ children, onClick }) {
  return (
    <button className="button" onClick={onClick}>
      {children}
    </button>
  );
}

예시 3

  • Context와 Reducer를 통한 확장
    규모가 큰 앱에서는 컨텍스트와 Reducer를 결합하여 컴포넌트에서 특정 State와 관련된 로직을 분리하는 것이 일반적이다. 이 예시에서는 모든 “Wiring”이 Reducer와 두 개의 개별 Context가 포함된 TasksContext.js에 숨겨져 있다.

context, reducer 정의

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

const TasksContext = createContext(null);

const TasksDispatchContext = createContext(null);

export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

export function useTasks() {
  return useContext(TasksContext);
}

export function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

const initialTasks = [
  { id: 0, text: 'Philosopher’s Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink matcha', done: false }
];

state쓰고 dispatch하기

import { useState, useContext } from 'react';
import { useTasks, useTasksDispatch } from './TasksContext.js';

export default function TaskList() {
  const tasks = useTasks();
  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          <Task task={task} />
        </li>
      ))}
    </ul>
  );
}

function Task({ task }) {
  const [isEditing, setIsEditing] = useState(false);
  const dispatch = useTasksDispatch();
  let taskContent;
  if (isEditing) {
    taskContent = (
      <>
        <input
          value={task.text}
          onChange={e => {
            dispatch({
              type: 'changed',
              task: {
                ...task,
                text: e.target.value
              }
            });
          }} />
        <button onClick={() => setIsEditing(false)}>
          Save
        </button>
      </>
    );
  } else {
    taskContent = (
      <>
        {task.text}
        <button onClick={() => setIsEditing(true)}>
          Edit
        </button>
      </>
    );
  }
  return (
    <label>
      <input
        type="checkbox"
        checked={task.done}
        onChange={e => {
          dispatch({
            type: 'changed',
            task: {
              ...task,
              done: e.target.checked
            }
          });
        }}
      />
      {taskContent}
      <button onClick={() => {
        dispatch({
          type: 'deleted',
          id: task.id
        });
      }}>
        Delete
      </button>
    </label>
  );
}

Fallback 기본값 지정

React가 부모 트리에서 특정 Context Provider를 찾을 수 없는 경우, useContext()가 반환하는 Context 값은 해당 Context를 생성할 때 지정한 기본값과 동일하다.
따라서 아래와 같은 경우 null이 반환된다.

const ThemeContext = createContext(null);

기본값은 변경할 수 없으므로 context를 업데이트하려면 state와 함께 사용해야 한다.
null 대신 의미있는 기본값을 하는 경우는 아래와 같다.

const ThemeContext = createContext('light');

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


트리의 일부 Context 오버라이딩 하기

트리의 일부분을 다른 값의 Provider로 감싸서 해당 부분에 대한 Context를 오버라이딩 할 수 있다.
아래의 경우 Footer 부분은 light로 나온다.

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

예시1

여기서 Footer 내부의 버튼은 외부의 버튼("dark")과 다른 Context 값("light")을 받는다.

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>
      <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>
  );
}

예시2

context Provider를 중첩할 때 정보를 “누적”할 수 있다.

아래 예시에서 Section 컴포넌트는 섹션 중첩의 깊이를 지정하는 LevelContext를 추적한다.
이 컴포넌트는 부모 섹션에서 LevelContext를 읽은 다음 1씩 증가한 LevelContext 숫자를 자식에게 제공한다.
그 결과 Heading 컴포넌트는 얼마나 많은 Section 컴포넌트가 중첩되어 있는지에 따라 <h1>, <h2>, <h3>, …, 태그 중 어떤 태그를 사용할지 자동으로 결정할 수 있다.

import { createContext } from 'react';

export const LevelContext = createContext(0);
import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading>Title</Heading>
      <Section>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Section>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Section({ children }) {
  const level = useContext(LevelContext);
  return (
    <section className="section">
      <LevelContext.Provider value={level + 1}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Heading({ children }) {
  const level = useContext(LevelContext);
  switch (level) {
    case 0:
      throw Error('Heading must be inside a Section!');
    case 1:
      return <h1>{children}</h1>;
    case 2:
      return <h2>{children}</h2>;
    case 3:
      return <h3>{children}</h3>;
    case 4:
      return <h4>{children}</h4>;
    case 5:
      return <h5>{children}</h5>;
    case 6:
      return <h6>{children}</h6>;
    default:
      throw Error('Unknown level: ' + level);
  }
}

객체와 함수를 전달할 때 리렌더링 최적화하기

다음과 같이 객체와 함수를 context value로 전달할 때 MyApp이 리렌더링 될 때마다(ex. 경로 이동 시) 함수와 객체가 업데이트되어 React는 useContext(AuthContext)를 호출하는 트리 깊숙한 곳에 있는 모든 컴포넌트들을 다시 렌더링해야 한다.

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>
  );
}

이때 currentUser가 변경되지 않았다면 리렌더링을 할 필요가 없으므로 React가 이 사실을 알게끔 객체 생성을 useMemo로 최적화하고, 함수를 useCallback으로 최적화할 수 있다.

이렇게 변경하면 MyApp이 다시 렌더링해야 하는 경우에도 currentUser가 변경되지 않는 한 useContext(AuthContext)를 호출하는 컴포넌트는 다시 렌더링할 필요가 없다.

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>
  );
}

Trobleshooting

컴포넌트가 Provider에서 값을 인식하지 못할 때

이런 일이 발생하는 케이스는 다음과 같다.

  1. useContext를 호출하는 컴포넌트와 동일한 컴포넌트(또는 그 아래)에서 provider를 제공하는 경우 : provider를 useContext를 호출하는 컴포넌트의 상단으로 이동시켜야 한다.
  2. 컴포넌트를 prodiver로 감싸는 것을 잊었거나 다른 트리의 다른 부분에 배치했을 경우 : React DevTools로 계층 구조 확인하기
  3. 사용 중인 도구에서 발생하는 빌드 문제로 인해 제공하는 컴포넌트에서의 someContext와 값을 읽는 컴포넌트에서의 someContext가 서로 다른 객체로 처리되는 경우 : 이를 확인하기 위해서는 window.SomeContext1과 window.SomeContext2를 전역에 할당하고 콘솔에서 window.SomeContext1 === window.SomeContext2인지 확인한다.

기본값이 다른데도 Context가 undefined를 반환할 때

  1. provider에서 value를 지정하는 것을 잊어버린 경우
    이 경우 value={undefined}를 전달하는 것과 같다.
// 🚩 Doesn't work: no value prop
<ThemeContext.Provider>
   <Button />
</ThemeContext.Provider>

이때 createContext(defaultValue)의 기본값이 쓰이지 않고 undefined가 전달되는 이유

createContext(defaultValue) 호출의 기본값은 위에 일치하는 Provider가 전혀 없는 경우에만 사용된다. <SomeContext.Provider value={undefined}> 컴포넌트가 있는 경우, useContext(SomeContext)를 호출하는 컴포넌트는 undefined를 Context 값으로 받는다.

  1. 실수로 다른 Prop의 이름을 사용한 경우
// 🚩 Doesn't work: prop should be called "value"
<ThemeContext.Provider theme={theme}>
   <Button />
</ThemeContext.Provider>

다음과 같이 수정한다.

// ✅ Passing the value prop
<ThemeContext.Provider value={theme}>
   <Button />
</ThemeContext.Provider>

createContext

createContext를 사용하면 컴포넌트가 Context를 제공하거나 읽을 수 있다.

const SomeContext = createContext(defaultValue)

Reference

createContext(defaultValue)

컴포넌트 외부에서 createContext를 호출하여 컨텍스트를 생성한다.

import { createContext } from 'react';

const ThemeContext = createContext('light');

매개변수

defaultValue: 컴포넌트가 context를 읽을 때 상위에 일치하는 provider가 없는 경우 컨텍스트가 가져야 할 값이다. 의미 있는 기본값이 없으면 null을 지정한다. 기본값은 “최후의 수단”으로 사용된다. 이 값은 정적이며 시간이 지나도 변경되지 않는다.

반환값

context 객체를 반환한다. (context 객체 자체는 어떠한 정보도 갖고 있지 않다.)
일반적으로 상위 컴포넌트에서 컨텍스트 값을 지정하기 위해 SomeContext.Provider를 사용하고, 하위 컴포넌트에서 읽기 위해 useContext(SomeContext)를 호출한다.

context 객체가 가진 속성은 다음과 같다.

  • SomeContext.Provider : 컴포넌트에 컨텍스트 값을 제공한다.
  • SomeContext.Consumer : 컨텍스트 값을 읽는 대안책이고 거의 안 쓴다. (예전에 썼던 기능이고 요즘은 useContext로 읽음)

Props

value: prodiver 내부의 컨텍스트를 읽는 모든 컴포넌트에 전달하려는 값이다. 컨텍스트 값은 어떤 타입이든 상관 없다. provider 내부에서 useContext(SomeContext)를 호출하는 컴포넌트는 그 위의 가장 가까운 컨텍스트 prodiver의 value를 받게 된다.

SomeContext.Provider

컴포넌트를 컨텍스트 provider로 감싸서 이 컨텍스트의 값을 모든 내부 컴포넌트에 지정한다.

function App() {
  const [theme, setTheme] = useState('light');
  // ...
  return (
    <ThemeContext.Provider value={theme}>
      <Page />
    </ThemeContext.Provider>
  );
}

SomeContext.Consumer

useContext가 등장하기 전에 컨텍스트를 읽는 방식으로 쓰였다.

function Button() {
  // 🟡 이전 방식 (권장하지 않음)
  return (
    <ThemeContext.Consumer>
      {theme => (
        <button className={theme} />
      )}
    </ThemeContext.Consumer>
  );
}

위의 방식은 여전히 동작하긴 하지만 아래 방식을 권장한다.

function Button() {
  // ✅ 권장하는 방법
  const theme = useContext(ThemeContext);
  return <button className={theme} />;
}

Usage

context 생성하기

컴포넌트 외부에서 createContext를 호출하여 하나 이상의 컨텍스트를 생성한다.

import { createContext } from 'react';

const ThemeContext = createContext('light');
const AuthContext = createContext(null);

createContext는 컨텍스트 객체를 반환한다.
컴포넌트는 이를 useContext()에 전달하여 컨텍스트를 읽을 수 있다.

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

function Profile() {
  const currentUser = useContext(AuthContext);
  // ...
}

기본적으로 value는 context를 생성할 때 지정한 기본값이 되지만 기본값은 변경할 수 없기 때문에 유용하지 않다.
context를 변경하기 위해서는 다음과 같이 state와 결합해주면 된다.

function App() {
  const [theme, setTheme] = useState('dark');
  const [currentUser, setCurrentUser] = useState({ name: 'Taylor' });

  // ...

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

context 가져오기 & 내보내기

context는 보통 별도 파일에서 선언하는데, 그 이유는 서로 다른 파일에 있는 컴포넌트에서 컨텍스트에 접근해야 하는 경우가 발생하기 때문이다.

// Contexts.js
import { createContext } from 'react';

export const ThemeContext = createContext('light');
export const AuthContext = createContext(null);

이렇게 별도 파일에서 선언된 context를 읽기 위해서는 import를 하면 된다.

// Button.js
import { ThemeContext } from './Contexts.js';

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

Context를 사용하기 전 고려할 것

depth가 깊어진다고 해서 무조건 context를 사용해야 하는 것은 아님.
context를 사용하기 전 다음을 고려해보아야 한다.

  1. props로 전달하기 : 사소한 컴포넌트가 아닌 이상 컴포넌트에 여러 props를 전달하는 것은 이상한게 아니다. 어떤 컴포넌트가 어떤 데이터를 사용하는지 명확히 나타낼 수 있고, 유지보수에도 좋다.
  2. 컴포넌트를 추출하고 JSX를 children으로 전달하기 : 데이터를 사용하지 않는 중간 컴포넌트 층을 통해 데이터를 보내기만 하는 경우에는 컴포넌트를 추출하는 것이 낫다.
    예를 들어 Layout이라는 컴포넌트에서 post 데이터를 사용하지 않고 단순히 하위 컴포넌트에 전달만 한다면 <Layout posts={posts}/>로 쓰는 것 보다 <Layout><Posts posts={posts}/></Layout>과 같이 children을 prop으로 받아서 전달하는 것이 낫다. 이렇게 하면 데이터를 지정하는 컴포넌트와 데이터가 필요한 컴포넌트 사이의 계층이 줄어든다.

위의 방법을 먼저 시도해보고 잘 해결되지 않는다면 context를 고려해보는 것이 좋다.


Context 사용 예시

  • 테마 : 다크 모드/라이트 모드와 같이 사용자가 테마를 지정할 수 있는 경우에 context provider를 앱 최상단에 두고 시각적으로 조정이 필요한 곳에서 context를 사용할 수 있다.
  • 현재 로그인된 계정 정보 : 현재 로그인 한 유저의 정보를 알아야 하는 컴포넌트가 많은 경우에 현재 계정 정보를 context에 저장해두면 좋다.
  • 라우팅 : 대부분의 라우팅 솔루션은 현재 라우트를 유지하기 위해 context를 사용한다.
  • 상태 관리 : 애플리케이션 규모가 커지면 수많은 state가 생기게 되고 다양한 하위 컴포넌트에서 이 값을 변경하고 싶은 니즈가 있을 수 있다. reducer를 context와 함께 사용하면 복잡한 state를 관리하며 멀리 있는 컴포넌트까지 값을 전달할 수 있다.

예제 문제

Challenge 1 of 1: Replace prop drilling with context

useContext를 이용해 prop drilling 제거하기

주어진 코드

App.js

import { useState } from 'react';
import { places } from './data.js';
import { getImageUrl } from './utils.js';

export default function App() {
  const [isLarge, setIsLarge] = useState(false);
  const imageSize = isLarge ? 150 : 100;
  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={isLarge}
          onChange={e => {
            setIsLarge(e.target.checked);
          }}
        />
        Use large images
      </label>
      <hr />
      <List imageSize={imageSize} />
    </>
  )
}

function List({ imageSize }) {
  const listItems = places.map(place =>
    <li key={place.id}>
      <Place
        place={place}
        imageSize={imageSize}
      />
    </li>
  );
  return <ul>{listItems}</ul>;
}

function Place({ place, imageSize }) {
  return (
    <>
      <PlaceImage
        place={place}
        imageSize={imageSize}
      />
      <p>
        <b>{place.name}</b>
        {': ' + place.description}
      </p>
    </>
  );
}

function PlaceImage({ place, imageSize }) {
  return (
    <img
      src={getImageUrl(place)}
      alt={place.name}
      width={imageSize}
      height={imageSize}
    />
  );
}

직접 수정해보기

  1. context를 새로 만든다.

Context.js

import { createContext } from 'react';

export const ImageContext = createContext(null);
  1. context provider로 하위 컴포넌트들을 감싸고, value로 imageSize를 전달한다. List와 Place에서 불필요한 props를 제거하고, PlaceImage에서 useContext를 이용해 imageSize를 사용한다.

App.js

import { useState, useContext } from 'react';
import { places } from './data.js';
import { getImageUrl } from './utils.js';
import { ImageContext } from './Context.js'

export default function App() {
  const [isLarge, setIsLarge] = useState(false);
  const imageSize = isLarge ? 150 : 100;
  return (
    <ImageContext.Provider value={imageSize}>
      <label>
        <input
          type="checkbox"
          checked={isLarge}
          onChange={e => {
            setIsLarge(e.target.checked);
          }}
        />
        Use large images
      </label>
      <hr />
      <List />
    </ImageContext.Provider>
  )
}

function List() {
  const listItems = places.map(place =>
    <li key={place.id}>
      <Place
        place={place}
      />
    </li>
  );
  return <ul>{listItems}</ul>;
}

function Place({ place }) {
  return (
    <>
      <PlaceImage
        place={place}
      />
      <p>
        <b>{place.name}</b>
        {': ' + place.description}
      </p>
    </>
  );
}

function PlaceImage({ place }) {
  const imageSize = useContext(ImageContext);
  
  return (
    <img
      src={getImageUrl(place)}
      alt={place.name}
      width={imageSize}
      height={imageSize}
    />
  );
}

자료
https://react.dev/reference/react/useContext
https://react.dev/reference/react/createContext
https://react.dev/learn/passing-data-deeply-with-context

profile
모든게 새롭고 재밌는 프론트엔드 새싹

0개의 댓글