Skip to content
This page has been auto-translated and may contain errors.View in English

Context

state 끌어올리기는 값을 공유하는 컴포넌트들이 가까이 있을 때 잘 작동합니다. 떨어져 있으면 힘들어집니다. 트리의 상단 근처에 있는 값이 6단계 아래에서 필요하면, 그 사이의 모든 컴포넌트를 거쳐야 하고, 각 컴포넌트는 자신이 쓰지도 않는 prop을 받아서 전달해야 합니다. 이런 중계 과정을 **prop drilling**이라고 하는데, Compound components에서 마주친 문제이자 Children.map으로도 해결할 수 없던 문제입니다. Context는 이 문제를 푸는 React의 기본 방법입니다. 한 컴포넌트가 값을 제공하면, 그 아래의 어떤 컴포넌트든 직접 그 값을 읽을 수 있습니다. 사이에 몇 개의 계층이 있든 상관없습니다.

데이터를 순간이동시킨다고 생각하면 됩니다. 중간의 컴포넌트들은 그것을 보지도 전달하지도 않으므로, prop 목록은 자신의 일에만 집중할 수 있습니다.

context 만들고 값 제공하기

Context는 세 부분으로 이루어집니다. 만들기, 값 제공하기, 값 읽기입니다. 만드는 것은 한 번만 일어나고, 파일의 최상단에서, 어떤 컴포넌트의 바깥에서 합니다. 보통 export도 함께 하는데, 그 값을 읽는 컴포넌트들이 다른 파일에 있을 수 있기 때문입니다.

jsx
import { createContext } from 'react'

export const ThemeContext = createContext()

export default function App() {
  return (
    <ThemeContext value="light">
      <Header />
      <Button />
    </ThemeContext>
  )
}

context 객체를 컴포넌트로 렌더링하면, 그 안의 모든 것이 소비자가 됩니다. value prop이 페이로드인데, 여기서는 문자열 "light"이지만, 어떤 JavaScript 값이든 될 수 있습니다. 객체, 배열, 심지어 함수도 가능합니다. 거기에 넣는 것이 아래의 읽는 쪽이 받게 될 것입니다. prop은 반드시 value라고 이름 붙여야 합니다. 이것이 API의 일부입니다.

배치가 중요합니다. provider가 전체 앱을 감싸야 할 필요는 없고, 보통 그렇게 해서는 안 됩니다. 그 값이 필요한 모든 컴포넌트를 포함하는 가장 작은 서브트리 주위에 놓으세요. UI의 한 구석만 테마에 신경 쓴다면, 그 구석의 공통 조상에서 테마를 제공하고 나머지 트리는 빼세요.

Version note

React 19 이전에는 context를 직접 렌더링할 수 없었습니다. <ThemeContext.Provider value="light"> 형태로 렌더링했습니다. React 19에서 context 객체 자체를 provider로 렌더링 가능하게 했습니다. .Provider 형태는 여전히 작동하고 대부분의 기존 코드베이스와 강의 레슨에서 볼 수 있지만, React는 향후 버전에서 이것을 deprecate할 계획이고 마이그레이션용 codemod를 제공하므로, 새 코드에서는 기본 <ThemeContext> 형태를 사용하세요.

useContext로 값 읽기

provider 아래의 어떤 컴포넌트든 useContext로 값을 읽습니다. useContextHooks에서 처음 소개된 hook인데, context 객체를 전달하면 React가 어떤 context를 읽을지 알 수 있습니다. 이것이 context를 export하는 이유입니다.

jsx
import { useContext } from 'react'
import { ThemeContext } from './App'

function Header() {
  const theme = useContext(ThemeContext)

  return (
    <header className={`${theme}-theme`}>
      <h1>{theme === 'light' ? 'Light' : 'Dark'} Theme</h1>
    </header>
  )
}

useContext(ThemeContext)는 이 컴포넌트 위의 가장 가까운 ThemeContext provider가 현재 가진 것을 반환합니다. 여기서는 문자열 "light"입니다. AppHeader 사이에 prop을 연결할 필요가 없고, 더 큰 트리에서 그들 사이의 컴포넌트는 테마를 언급할 필요가 없습니다. 한 앱은 여러 context를 동시에 가질 수 있습니다. 각각은 따로 만들어지고, 각각의 객체를 useContext에 전달해서 읽습니다.

살아있게 만들기: state와 context

하드코딩된 value="light"는 절대 바뀌지 않습니다. context를 유용하게 만드는 패턴은 state와 페어링하는 것입니다. state가 값을 소유하고 업데이트하고, context가 그것을 전달합니다. 현재 값과 그것을 바꾸는 함수를 모두 가진 객체를 전달하면, 모든 소비자는 provider 아래 어디서든 테마를 읽거나 업데이트할 수 있습니다.

jsx
export const ThemeContext = createContext()

export default function App() {
  const [theme, setTheme] = useState('light')

  function toggleTheme() {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light')
  }

  return (
    <ThemeContext value={{ theme, toggleTheme }}>
      <Header />
      <Button />
    </ThemeContext>
  )
}

function Button() {
  const { theme, toggleTheme } = useContext(ThemeContext)

  return (
    <button className={`${theme}-theme`} onClick={toggleTheme}>
      Switch theme
    </button>
  )
}

버튼을 클릭하면, toggleTheme이 실행되고, App의 state가 업데이트되고, App이 다시 렌더링되고, provider가 새 객체를 아래로 전달합니다. context를 읽는 모든 컴포넌트는 새 값으로 다시 렌더링됩니다. 소비자는 필요한 속성만 구조 분해합니다.

이 범위 지정 트릭은 작은 규모에서도 작동합니다. 드롭다운 메뉴 같은 컴포넌트는 자신의 자식 주위에 provider를 렌더링하고, 그 안의 부분들에게 페이지의 나머지로 새지 않는 공유 state를 줍니다. 다음은 Compound components의 메뉴를 완성한 것입니다:

jsx
const MenuContext = createContext()

function Menu({ children }) {
  const [open, setOpen] = useState(false)
  const toggle = () => setOpen(prevOpen => !prevOpen)

  return <MenuContext value={{ open, toggle }}>{children}</MenuContext>
}

function MenuButton({ children }) {
  const { toggle } = useContext(MenuContext)

  return <button onClick={toggle}>{children}</button>
}

Menuopen을 소유하고 그것과 toggle 둘 다를 context에 넣습니다. MenuButtonuseContext로 toggle을 가져오고, 같은 방식으로 작성된 MenuDropdownopen을 읽어서 렌더링할지 말지 결정합니다. 그 사이에 중계하는 것이 없으므로, 호출하는 쪽에서 버튼을 아무리 많은 레이아웃 div로 감싸도 메뉴는 계속 작동합니다.

raw context를 export하는 것은 작동하지만, 대부분의 코드베이스는 이 패턴을 두 부분으로 감싸집니다. state를 소유한 provider 컴포넌트와, context를 읽고 잘못된 사용을 크게 실패하게 하는 커스텀 hook입니다.

jsx
const ThemeContext = createContext(null)

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  function toggleTheme() {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light')
  }

  return (
    <ThemeContext value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext>
  )
}

export function useTheme() {
  const context = useContext(ThemeContext)
  if (context === null) {
    throw new Error('useTheme must be used within a ThemeProvider')
  }
  return context
}

이제 소비자는 const { theme } = useTheme()를 쓰고 context 객체를 전혀 import 하지 않습니다. 모듈의 공개 표면이 provider와 hook 정확히 두 가지만 유지됩니다. createContext에 전달한 인자는 provider가 없을 때 React가 반환할 폴백이므로, 여기서 null은 "아무도 이것을 제공하지 않았다"는 뜻입니다. 이것이 확인하는 것입니다. 없으면 소비자는 null 값을 구조 분해하면서 충돌하고, 스택 트레이스가 missing provider가 아닌 소비자를 가리킵니다.

provider의 value가 바뀌면, React는 그 context를 읽는 모든 컴포넌트를 다시 렌더링합니다. 아무리 깊게 있어도요. "바뀐다"는 것은 Object.is 비교가 실패한다는 의미인데, 바로 여기서 객체 리터럴 value가 문제가 됩니다. provider의 소유자가 렌더링될 때마다 value에 전달된 객체가 새로 생성되므로, 그 소유자가 렌더링될 때마다 모든 소비자가 다시 렌더링됩니다. theme이 실제로 바뀌지 않았어도요.

소유자가 value 변경 때문에만 다시 렌더링될 때, 예를 들어 여기서 App이 그럴 때, memoizing은 효과가 없습니다. 소유자가 관련 없는 이유로 다시 렌더링되고 모든 소비자를 함께 끌어갈 때 효과가 있습니다. 객체를 useMemo로 감싸는 것이 절반의 해결책입니다. toggleTheme은 매번 렌더링할 때마다 새로운 함수이므로, 그 함수도 useCallback으로 안정화하거나 이미 안정한 setTheme을 wrapper 대신 전달하지 않으면 memo가 매번 다시 계산합니다.

구조적으로 두 가지 도구가 memoization보다 더 중요합니다. 첫째, update 빈도에 따라 context를 나눕니다. 세션당 한 번 바뀌는 값과 매 keystroke마다 바뀌는 값이 같은 provider에 있으면 안 됩니다. 빠른 것이 느린 것의 소비자를 함께 끌어가니까요.

둘째, context는 state 관리자라기보다 전달 메커니즘이라는 것을 기억하세요. "이 값이 멀리 필요하다"는 것을 풉니다. update를 batch 처리하거나, 파생하거나, persist 하는 방법에 대해서는 아무것도 하지 않습니다. 앱의 공유 state가 테마와 사용자 객체를 넘어서 커질 때, Redux Toolkit이나 Zustand 같은 전용 store가 제 역할을 하는 시점입니다.

각 store가 컴포넌트에 도달하는 방법

둘은 다르게 전달합니다. Redux Toolkit 앱은 react-redux<Provider store={store}>로 트리를 감싸고, 이것이 context를 통해 아래의 hook에 store를 전달합니다. Zustand의 기본 설정은 아무 provider도 없이 어떤 컴포넌트든 직접 호출하는 모듈 레벨 hook을 export합니다.

JunoContext는 중간 계층을 건너뜁니다 값이 트리 깊숙이 여행해야 할 때, 모든 컴포넌트를 거쳐 전달하는 것은 금방 지쳐집니다.

Context는 상단 근처의 한 컴포넌트가 "여기 값이 있다"고 말할 수 있게 하고, 아래의 어떤 컴포넌트든 useContext로 직접 집어들 수 있게 합니다. 중간의 컴포넌트들은 그것을 건드리지 않습니다.

State와 페어링하면 값도 바뀔 수 있습니다. 값과 업데이트 함수를 provider에 함께 유지하면, 모든 소비자는 어디서든 그것을 읽거나 바꿀 수 있습니다.

JunoContext는 중간 계층을 건너뜁니다 module 레벨에서 context를 만들고, value를 가지고 provider로 렌더링하고, 아래에서 useContext로 읽습니다. state와 그 updater를 하나의 객체로 함께 전달해서 살아있게 합니다.

실제 코드에서는 provider를 자신의 컴포넌트로 감싸고 useTheme 스타일의 hook을 expose합니다. provider 밖에서는 던져집니다. 소비자는 깔끔한 API를 얻고 실수는 읽기 좋은 에러로 나타납니다.

JunoContext는 중간 계층을 건너뜁니다Object.is가 제공된 값에 실패할 때마다 모든 소비자가 다시 렌더링되고, 인라인 객체 리터럴은 매번 provider 렌더링에 실패하므로, 업데이트 빈도가 중요한 곳에서 value를 memoize합니다. 다른 속도로 업데이트되는 context를 나누고, provider의 범위를 필요한 가장 작은 서브트리로 정합니다.

Context는 먼 값의 전달로 취급합니다. 공유 state가 진짜 관리가 필요하면, Redux Toolkit처럼 context를 타는 store든, Zustand처럼 건너뛰든, store를 선택하세요.

다음: Render props와 headless 컴포넌트. 컴포넌트가 동작을 제공하고 마크업은 당신이 공급합니다.