React Context를 대충 알고 사용하는 사람들을 위한 주의글

김태현·2023년 12월 13일
3

React

목록 보기
5/5
post-thumbnail

이 글은 블로그 답변시리즈 글을 번역한 글을 보고 작성된 글입니다.
https://velog.io/@superlipbalm/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior

context가 무엇인가

context는 React 컴포넌트 트리 안에서 전역적(global)이라고 볼 수 있는 데이터를 공유할 수 있도록 고안된 방법입니다 (리액트 공식문서에서 발췌)

리액트의 Context API는 단일 사용자 제공 값을 컴포넌트의 하위 트리에서 사용할 수 있도록 하는 메커니즘입니다. 주어진 <MyContext.Provider> 내부의 모든 컴포넌트는 사이의 모든 컴포넌트를 통해 해당 값을 명시적으로 전달할 필요 없이 해당 컨텍스트 인스턴스에서 값을 읽을 수 있습니다.

컨텍스트는 "상태 관리"도구가 아닙니다. 컨텍스트에 전달되는 값은 직접 관리해야 합니다. 이는 일반적으로 데이터를 리액트 컴포넌트 상태로 유지하고 해당 데이터를 기반으로 컨텍스트 값을 구성함으로써 수행됩니다.

Context를 마치 상태 관리 대안으로 생각하고, 상태 관리 툴인 Redux, Jotai, Zustand와 비교하는 사람들이 있다.

이렇게 처음에 상태 관리 도구라고 생각하고 Context api를 사용해보면 불편한 것이 한둘이 아닐것이다. 당연하다, 이 context는 상태 관리를 편하게 하기 위한 도구가 아니기 때문이다.

위 글의 개념 그대로 이해하면 되겠다.
복잡한 컴포넌트 트리에서 prop을 트리 하위 계층으로 전달하고 전달하는 prop drilling 현상을 막기 위해, 전역적 데이터를 컴포넌트 트리안에서 공유하기 위해 리액트 팀이 고안한 방법이라 한다.

전역적 데이터를 다루기 위한 방법이라 하지만 다른 상황에서도 context를 유용하게 사용할 수 있는데 이는 아래에서 설명하도록 하겠다.

사용법

리액트 공식문서 참고

주로 사용되는 상황

1. 전역적 데이터를 다룰 때

개념에서 설명한 그대로이다. 컴포넌트 트리 전체에서 사용되는 데이터가 있다면 복잡하게 prop drilling 현상을 일으킬 필요없이 전체 컴포넌트를 context provider를 감싸주어 provider에서 전체 컴포넌트 트리에서 다루는 데이터를 생성한 후, value값에 데이터를 전달해주면, 아래 컴포넌트 트리 어디에서든 useContext를 사용하여 원하는 데이터를 받아올 수 있다. prop drilling를 해결할 수 있는 훌륭한 방법이다.

2. 일부 컴포넌트 트리에서 자주 공유되는 데이터를 다룰 때

무조건 context를 전역적인 데이터를 다룰때만 사용해야한다고 생각할 수 있는데, 그것은 context를 반만 활용하는 방법이다. context는 전체 컴포넌트 트리 많은 곳에 사용하지 않더라도 일부 컴포넌트 트리에서 자주 공유해야한 데이터가 있다면 그 컴포넌트 트리를 역시 context provider로 감싸주어 데이터를 편리하게 사용할 수 있다.
예를 들어 SideBar 컴포넌트를 만들어 줄때, SideBar의 열고 닫음을 나타내는 상태 isOpenSideBar가 존재하고, 이를 SideBar 컴포넌트가 감싸고 있는 하위 컴포넌트들이 이 상태를 사용해야 할때, prop을 통해 전달해 줄수도 있지만, 트리가 조금이라도 복잡해진다면 context를 도입해볼만하다.

2-1. Compound Pattern

컴파운드 패턴 관련 참고글:

유용한 리액트 패턴중 한 가지인 compound pattern에도 이 context api를 적용해볼만 하다.

import * as React from 'react'
// this switch implements a checkbox input and is not relevant for this example
import {Switch} from '../switch'

const ToggleContext = React.createContext()

function useEffectAfterMount(cb, dependencies) {
  const justMounted = React.useRef(true)
  React.useEffect(() => {
    if (!justMounted.current) {
      return cb()
    }
    justMounted.current = false
  }, dependencies)
}

function Toggle(props) {
  const [on, setOn] = React.useState(false)
  const toggle = React.useCallback(() => setOn(oldOn => !oldOn), [])
  useEffectAfterMount(() => {
    props.onToggle(on)
  }, [on])
  const value = React.useMemo(() => ({on, toggle}), [on])
  return (
    <ToggleContext.Provider value={value}>
      {props.children}
    </ToggleContext.Provider>
  )
}

function useToggleContext() {
  const context = React.useContext(ToggleContext)
  if (!context) {
    throw new Error(
      `Toggle compound components cannot be rendered outside the Toggle component`,
    )
  }
  return context
}

function ToggleOn({children}) {
  const {on} = useToggleContext()
  return on ? children : null
}

function ToggleOff({children}) {
  const {on} = useToggleContext()
  return on ? null : children
}

function ToggleButton(props) {
  const {on, toggle} = useToggleContext()
  return <Switch on={on} onClick={toggle} {...props} />
}
import React from 'react'
import ReactDOM from 'react-dom'
import Toggle from './toggle'

function App() {
  return (
    <Toggle onToggle={on => console.log(on)}>
      <Toggle.On>The button is on</Toggle.On>
      <Toggle.Off>The button is off</Toggle.Off>
      <Toggle.Button />
    </Toggle>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

https://kentcdodds.com/blog/compound-components-with-react-hooks 링크에서 코드 발췌

위 코드와 같이 compound 패턴과 context api를 함께 사용하면 패턴의 효과가 극대화 된다. 자식 컴포넌트들 간의 공유되는 데이터를 부모 컴포넌트에서 context로 처리해주는 것이다. Toggle 컴포넌트를 사용하는 예제에서 몹시 사용이 간결해진것을 발견할 수 있다. 이와 같이 compound 패턴을 사용할 생각이 있다면 함께 context를 사용해보는 것도 고려해보자.

성능 주의점

context value 값 변경시 context provider 하위에 별다른 처리가 없다면 하위컴포넌트 모두 리렌더링된다!

context provider 컴포넌트는 여러개의 state 상태값을 가질 수 있고, value 값을 통하여 하위 컴포넌트 트리에 있는 컴포넌트들에게 값을 전달하여 준다.

context provider 컴포넌트는 하위 컴포넌트를 감싸고있는 상위 컴포넌트라고 할수있다. 만약 context provider에서 상태가 변경되어 이 컴포넌트가 리렌더링 되면 어떤 일이 일어날까?

React에서 리렌더링 조건 중 하나는 부모 컴포넌트가 리렌더링 된다면 자식 컴포넌트가 리렌더링이 된다 라는 것이 있다. 그렇다면 context provider가 리렌더링 된다면? 당연히 아무 처리도 하지 않은 자식 컴포넌트들은 무작정 리렌더링이 된다.

이는 위에서 이야기한 주로 context가 사용되는 상황 중 1번, 전체 컴포넌트 트리를 context로 감싸는 상황이나, 2번 일부 컴포넌트 트리를 감싸는데 이 일부 컴포넌트 트리가 꽤 복잡하고 큰 상황에서 문제가 생길 수 있다.

물론 하위 컴포넌트 트리가 간단하거나 복잡한 계산을 수행하지 않는다면 상관없겠지만 이외의 상황에서는 이를 주의해야한다.

이를 해결하는 방법은 2가지가 있다.
1. Provider 바로 아래의 컴포넌트에 React.memo를 사용
2. provider prop의 children으로 하위 컴포넌트를 전달

2가지 중 하나의 방법을 선택한다면 어떠한 하나의 컴포넌트에서 context 데이터를 변경하였는데 모든 컴포넌트 트리가 리렌더링 되는 무시무시한 상황을 피할 수 있을 것이다.

provider는 하나의 value 값만 아래에 전달한다!

Context Provider를 사용할 때, 코드를 자세히 보면 의아한 점이 있다.

const value = {a: 1, b: 2, c: ()=>{} }
<ToggleContext.Provider value={value}>
      {props.children}
</ToggleContext.Provider>

데이터들을 하나의 value 객체로 묶어 전달해준다.

그렇다. Context Provider는 하나의 value 값만을 하위 컴포넌트 트리에게 전달해줄 수 있다. Context가 이런식으로 작동한다면 문제가 생겨난다.

위와 같은 코드에서 value 객체 속 a 값이 변경되었다 생각해보자. 그렇다면 당연히 우리는 이 context를 사용하는 컴포넌트 중 a 값을 사용하지 않는 컴포넌트들은 리렌더링이 되지않기를 바랄것이다.

하지만 provider는 하나의 value값만 전달한다 하였고 위 value는 a,b,c 값을 갖고있는 하나의 객체 값이기 때문에 a 값이 변경된다면 value 객체 역시 새로운 객체가 생성되어 값을 가질 것이므로 위 value를 전달받는 모든 하위 컴포넌트 들이 렌더링된다.

이 역시 이 context의 value 사용하는 컴포넌트들이 많지 않거나, 복잡한 계산을 수행하지 않는다면 리렌더링 되는것은 큰 문제가 없을 것이다. 하지만 이외의 상황에서는 이와 같은 대안을 고려해야 한다.

상황에 맞게 여러개의 context provider들로 나누어주자!

provider는 하나의 값만 전달해주기 때문에, 위와 같이 하나의 값의 변경으로 다른 값을 사용하는 컴포넌트의 리렌더링을 원치 않는다면 여러개의 context provider들로 나누어주어 여러 값을 받아주면 된다.
가장 흔한 케이스를 예시로 표현해보겠다.

const ToggleContext = React.createContext()
const ToggleActionContext = React.createContext()
function Toggle(props) {
  const [on, setOn] = React.useState(false)
  
  const stateValue = React.useMemo(() => ({on}), [on])
  const actionValue = React.useMemo(() => ({setOn}), [on])
  
  return (
    <ToggleStateContext.Provider value={stateValue}>
    	<ToggleActionContext.Provider value={actionValue}>
      		{props.children}
    	</ToggleActionContext.Provider>
	</ToggleStateContext.Provider>
  )
}

대표적인 상황이다. setOn를 컨텍스트를 통해 받는 컴포넌트들은 on 값이 변경될 때마다 리렌더링이 될때, 이런 식으로 stateContext와 actionContext를 분리하여 사용한다면 위와 같은 상황을 해결할 수 있다.

이와 같이 context api를 사용할 때, 성능 향상을 위해 context provider를 나누어 value값들을 전달해보자.

장단점

장점

  • 리액트의 내장 api로서 전역 데이터 사용을 위해 외부 라이브러리를 설치할 필요가 없다.
  • 상태 관리 도구가 아니므로 전역적으로 데이터를 사용하면서 원하는대로 상태를 처리할 수 있다.

단점

  • 상태 관리 도구가 아니므로 불필요한 리렌더링을 유발할 수 있다.
  • provider를 생성하고 value값을 전달하고 하는 과정이 번거로울 수 있다.
  • 선택적으로 값을 구독할 수 없다.

무엇을 사용할까?

다시한번 말하지만 context api는 상태관리 도구가 아니다.

전역 상태관리를 편리하고 효율적인 방식으로 하고 싶다면 zustand, zotai 등과 같은 라이브러리들을 고려해볼법하다.

단순한 프로젝트에서 외부 라이브러리 추가 없이 전역적 데이터를 다루고 싶다면 context를 사용하자.

복잡한 프로젝트에서 리렌더링 성능등과 같은 것들을 고려하면서 진행해야한다면, Redux나 다른 상태관리 도구들을 고려해볼법하다.

긴글 읽어주셔서 감사합니다! 잘못되었거나 수정해야할 부분이 있다면 댓글로 알려주세요! 즉시 반영하여 수정하도록 하겠습니다.

profile
처음에는 웹 frontend 분야에 자신있는, 허나 다양한 분야를 경험하고 배우고자 노력하는 공학자 김태현입니다.

0개의 댓글