이번 편에서는 Reacct Context를 소개하고 작성하는 방법을 알아보겠습니다.

  • 아래에서 나오는 Context.Provider는 다음 편인 useContext를 이용해 더 쉽고 편하게 작성할 수 있기 때문에, React Context 개념과 createContext, Context.Provider 컴포넌트의 작성방법만 익혀도 괜찮습니다.

Props Drilling

React Context를 소개하기 전에 Props Drilling 패턴을 살펴보겠습니다.

import React from 'react'

const App = () => {
  const user = {
    nickname: 'Danuel',
    isAdmin: true
  }

  return (
    <div>
      <Main user={user} />
    </div>
  )
}

const Main = ({ user }) => (
  <main>
    <Avatar user={user} />
  </main>
)

const Avatar = ({ user }) => (
  <div>
    <User user={user} />
  </div>
)

const User = ({ user }) => {
  let label = 'user'
  if (user.isAdmin) {
    label = 'admin'
  }

  return (
    <div>
      <div>{label}</div>
      <div>{user.nickname}</div>
    </div>
  )
}

컴포넌트를 작성하고 사용하다 보면 해당 컴포넌트에게는 필요가 없지만 하위 컴포넌트에게 전달하기 위해 Props를 받아야 할 때가 있습니다. 이 예시로 보면 Main 컴포넌트와 Avatar 컴포넌트는 user에 대한 상태를 알 필요가 없지만, User 컴포넌트에게 user 상태를 전달하기 위해서 상위 컴포넌트로부터 전달을 받습니다.

이러한 패턴을 Props Drilling이라고 합니다.

Props Drilling을 이용해 작성한 컴포넌트는 서로가 서로에게 의존을 하는 형태로 발전하기 시작하고, 재활용이 불가능하지만 분리만 해놓은 컴포넌트를 작성하게 만드는 좋지 않은 패턴입니다. 재활용 가능한 컴포넌트를 작성하기 위해서 무엇인가 대책이 필요합니다.

React Context

Props_Drilling_vs_React_Context@16x.png

이미지에서 알 수 있듯이 React Context를 이용하면 Main 컴포넌트와 Avatar 컴포넌트를 건너뛰고 User 컴포넌트에 Props를 바로 전달할 수 있습니다. 이를 위해서는 약간의 추상화를 해야 합니다.

import React, { createContext } from 'react'

// 0. Context를 생성합니다.
const AppContext = createContext()

// 1. 상위 컴포넌트 안에서 AppContext.Provider 컴포넌트로 user를 전달합니다.
// 하위 컴포넌트는 AppContext.Provider 태그 안에 위치하게 합니다.
const App = () => {
  const user = {
    nickname: 'Danuel',
    isAdmin: true
  }

  return (
    <AppContext.Provider value={user}>
      <div>
        <Main />
      </div>
    </AppContext.Provider>
  )
}

const Main = () => (
  <main>
    <Avatar />
  </main>
)

const Avatar = () => (
  <div>
    <User />
  </div>
)

// 2. AppContext.Consumer 컴포넌트를 이용해 Context에서 전달한 user를 사용합니다.
const User = () => (
  <AppContext.Consumer>
    {user => {
      let label = 'user'
      if (user.isAdmin) {
        label = 'admin'
      }

      return (
        <div>
          <div>{label}</div>
          <div>{user.nickname}</div>
        </div>
      )
    }}
  </AppContext.Consumer>
)

React Hooks까지와는 조금씩 다른 부분이 있어서 복잡하게 보일 수 있지만, 크게 3단계로 나누어 살펴보면 쉽게 이해할 수 있습니다.

  1. React 라이브러리에서 제공하는 createContext 함수를 불러와서 Context라는 이름의 State를 만들어준다.

  2. 해당 Context가 가지고 있는 Context.Provider 컴포넌트를 상위 컴포넌트를 감싸는 형태로 작성한다.

  3. 해당 Context를 사용하고자 하는 하위 컴포넌트에서 Context.Consumer로 감싸는 형태로 작성한다.

    • Context.Consumer 태그 안에는 JSX를 return 하는 함수를 작성한다.

React Context를 이용하면 재활용 가능한 컴포넌트를 작성할 수 있다고 소개했습니다. 그 소개처럼 하위 컴포넌트에게 전달하기 위해서 상위 컴포넌트에게 Props를 받아들이는 부분이 없는 형태로 바뀌었습니다. 즉, Props Drilling 패턴을 떨쳐냈습니다.

import React, { createContext } from 'react'

// ...

const Avatar = () => (
  <div>
    <AppContext.Consumer>{User}</AppContext.Consumer>
  </div>
)

const User = user => {
  let label = 'user'
  if (user.isAdmin) {
    label = 'admin'
  }

  return (
    <div>
      <div>{label}</div>
      <div>{user.nickname}</div>
    </div>
  )
}

추가적으로 개선한 부분만을 확인하기 위해 Avatar 컴포넌트와 User 컴포넌트만 따로 보여주는 예시 코드입니다.

이전 예시 코드와 다른 점은 Avatar 컴포넌트 안에서 Context.Consumer 컴포넌트를 사용했고, 그 컴포넌트 안에는 User 컴포넌트를 JSX 형태가 아니라 문자열, 숫자를 표시할 때와 같이 함수 형태로 바로 위치하게 작성했습니다. 컴포넌트는 함수의 형태를 하고 있기에 가능한 기법입니다.

이렇게 작성하는 경우는 많지 않지만, '범용적으로 사용하고자 하는 컴포넌트라면 이렇게도 작성할 수 있다' 는 것을 이해한다면 더욱 유연하고 깔끔한 컴포넌트를 작성할 수 있습니다.

import React, { createContext } from 'react'

const Context = createContext()

const App = () => {
  const user = {
    nickname: 'Danuel',
    isAdmin: true
  }

  return (
    <Context.Provider value={user}>
      {/* ... */}
    </Context.Provider>
  )
}

App 컴포넌트가 지금은 user 데이터만 전달하는 로직을 가지고 있지만, Header 컴포넌트, Main 컴포넌트, Navigation 컴포넌트 등 하위 컴포넌트가 하나씩 늘어나다 보면 무엇을 하는 컴포넌트인지 알기 어려울 정도로 복잡하게 변합니다.

import React, { createContext } from 'react'

const AppContext = createContext()

const AppProvider = ({ children }) => {
  const user = {
    nickname: 'Danuel',
    isAdmin: true
  }

  return (
    <AppContext.Provider value={user}>
      {children}
    </AppContext.Provider>
  )
}

const App = () => (
  <AppProvider>
    {/* ... */}
  </AppProvider>
)

이 예시와 같이 AppProvider라는 별도의 컴포넌트를 작성해서 사용하면 보다 더 읽기 쉬운 컴포넌트를 작성할 수 있습니다.