Language/ReactJS

[React] Context API - useContext

Yuu's 2023. 4. 17. 18:29
반응형

 

Context란?

리액트에서 두 개 이상의 컴포넌트 간의 데이터를 주고 받기 위해 상위 컴포넌트에서 하위 컴포넌트로 "props"를 전달해줍니다. 마찬가지로 "Context"도 "props"가 아닌 또 다른 방법으로 컴포넌트 간에 값을 전달하는 방법입니다. 즉, 컴포넌트간 값을 공유하는 여러 방법 중 하나인 셈이죠.

왜 Context를 사용하죠?

"데이터를 주고 받을 때 props를 사용한다면서요...!? 그럼 왜 Context가 필요하죠? "라는 의문이 들 수도 있습니다. 아래의 이미지를 봅시다.

 

Prop drilling - React 공식 문서

색이 칠해져 있는 요소는 데이터가 필요한 컴포넌트입니다. 색이 칠해져 있지 않은 요소는 데이터가 필요 없는 컴포넌트입니다. 이미지를 다시 한번 보면 색이 칠해져 있지 않은 컴포넌트는 데이터가 필요하지 않지만 하위 컴포넌트로 데이터를 전달해주기 위한 "전달 매개체" 역할을 하고 있습니다. 코드로 한 번 표현해볼까요?

// 코드는 벨로퍼트님의 게시글에서 가져왔습니다.
// https://velog.io/@velopert/react-context-tutorial
function App() {
  return <GrandParent value="Hello World!" />;
}

function GrandParent({ value }) {
  return <Parent value={value} />;
}

function Parent({ value }) {
  return <Child value={value} />;
}

function Child({ value }) {
  return <GrandChild value={value} />;
}

function GrandChild({ value }) {
  return <Message value={value} />;
}

function Message({ value }) {
  return <div>Received: {value}</div>;
}

위의 코드 스니펫을 보시면 정작 `value` 데이터가 필요한 컴포넌트는 `Message`이지만 많은 컴포넌트들이 하위 컴포넌트로 계속 전달해주고 있습니다. 이러한 현상을 "Props drilling"이라고 합니다. 이처럼 깊숙한 곳에 위치한 컴포넌트에 데이터를 전달해야 하는 경우 개발자 입장에서는 정말 불편하고 실수 할 가능성이 높아지겠죠.

 

Teleport

그러면 데이터가 필요한 곳에 "텔레포트" 마법처럼 불러올 수 있다면 정말 편하지 않을까요? 그래서 이러한 불편함을 Context를 사용하면 편안하게 데이터가 필요한 곳에 전달해줄 수 있습니다. 하지만 그렇다고해서 "Props drilling"이 꼭 나쁜 것은 아닙니다. 데이터를 전달해줄 때 컴포넌트 한~두개 정도 거쳐서 "props"를 전달하는 경우라면 오히려 "props"를 이용하는게 훨씬 좋은 방법입니다. 

 

나중에 설명하겠지만 Context에도 단점이 있기 때문입니다. "props"를 이용해 데이터를 전달하는 것이 데이터의 흐름을 명시적으로 확인할 수 있고 성능적으로 Context 혹은 Redux, Mobx 등과 같은 상태 관리 라이브러리를 사용하는 것보다 더 좋은 장점도 있습니다. 따라서 꼭 "Props drilling"이 나쁜 것은 아니며 상황에 맞게 적절한 방법을 사용하시면 됩니다.

Context 사용하기

Context를 사용하기 위해서 3가지 단계가 필요합니다.

  • Context 생성 (Create the Context)
  • Provider 생성 (Use the Context)
  • Consumer 생성 (Provide the Context)

Context 생성

context는 React의 내장 메서드인 Context API의 `createContext`를 사용합니다.

 

context 생성 - create the context

이미지를 보다시피 `createContext`를 사용해 `MyContext`라는 Context 객체를 생성하였습니다. 코드가 적힌 아래 부분 보시면 `MyContext` 객체는 `Consumer`, `Provider`라는 객체를 확인할 수 있습니다. 해당 객체는 리액트의 컴포넌트입니다.

Provider 생성

Provider 생성

생성한 `MyContext`의 Provider 컴포넌트로 Context를 사용할 "구간"을 설정해줍니다. 저는 이 부분을 "스코프"의 개념으로 이해하고 있습니다.

Consumer 생성

Consumer도 3가지 방법으로 생성할 수 있습니다.

  • Consumer Component
  • useContext hook
  • contextType

 

Consumer Component

Consumer Component

위 코드에서 `{value => <div>{value}</div>}`부분을 볼 수 있듯이 Context.Consumer의 자식은 반드시 함수여야하며 context의 현재값을 받고 React 노드를 반환합니다.

 

useContext Hook

일반적으로 `useContext` 훅을 가장 많이 사용합니다. 개인적으로도 `useContext` 훅을 사용하는 것이 편합니다.

 

우선 useContext 훅을 사용하기 전에 Provider 컴포넌트를 리턴하는 컴포넌트를 생성해줍니다.

// DarkModeContext.jsx
import { createContext, useContext, useState } from 'react';

export const DarkModeContext = createContext();

// Provider 컴포넌트를 리턴하는 컴포넌트를 만듬
export function DarkModeProvider({ children }) {
  const [darkMode, setDarkMode] = useState(false); // context로 관리할 데이터

  const toggleDarkMode = () => {
    setDarkMode(!darkMode);
    updateDarkMode(!darkMode);
  };
	
  // HoC 패턴
  return (
    <DarkModeContext.Provider value={{ darkMode, toggleDarkMode }}>
      {children}
    </DarkModeContext.Provider>
  );
}

 

그리고 context를 사용할 범위에 생성해준 `DarkModeProvider` 컴포넌트를 씌워줍니다.

 

// App.jsx
import { useState } from 'react';
import Header from './components/Header/Header';
import TodoList from './components/TodoList/TodoList';
import { DarkModeProvider } from './components/context/DarkModeContext';

function App() {
  return (
    <DarkModeProvider>
      <Header />
      <TodoList />
    </DarkModeProvider>
  );
}

그리고나서 Header 컴포넌트 내부에 다크모드 버튼을 생성해 해당 버튼을 토글하면 다크모드를 설정해보겠습니다.

 

// Header.jsx
import React from 'react';
import styles from './Header.module.css';
import { DarkModeContext } from '../context/DarkModeContext';
import { BsFillSunFill, BsFillMoonFill } from 'react-icons/bs';

export default function Header({ filters, filter, onFilterChange }) {
  const { darkMode, toggleDarkMode } = useContext(DarkModeContext);

  return (
    <header className={styles.header}>
      <button onClick={toggleDarkMode} className={styles.toggle}>
        {!darkMode ? <BsFillMoonFill /> : <BsFillSunFill />}
      </button>
      <ul className={styles.filters}>
        <...>
      </ul>
    </header>
  );
}

 

이처럼 Header 컴포넌트 내부에 데이터가 필요하므로 `useContext()`를 이용해 필요한 데이터를 가져왔습니다.

 

contextType

마지막으로 `contextType` 방법입니다. 요즘 사용하지 않는 방법인 것 같으니 알아두고만 있으면 좋을 것 같습니다.

class MyClass extends React.Component {
	render() {
		const { isLoggedIn } = this.context;

		return (<div>{isLoggedIn}</div>)
	}
}
MyClass.contextType = UserContext

 

여기까지 Context를 사용하는 방법을 알아봤습니다. 그런데 만약 여러개의 데이터를 사용해야 한다면 어떻게해야 할까요? 

여러 Context 구독하기

그럴 때는 Provider를 중첩해서 사용하면 됩니다.

 

중첩 Provider

이렇게 중첩 Provider를 사용한 후 Consumer를 개별 노드로 만들어 설계하는데 context 변화로 인해 다시 렌더링하는 과정을 빠르게 유지하기 위함입니다.

 

Consumer를 개별 노드로 설계

 

Provider가 많아지면 많아질수록 비동기의 콜백 지옥처럼 Context Hell이 발생합니다.

 

Provider Hell

물론 해결 방안도 있습니다. "cloneElement" API와 합성 함수 패턴을 이용하면 조금 더 가독성 좋게 사용 가능합니다...

 

Provider Hell 해결 방안

위의 코드는 'react-pendulum' 라이브러리에서 "MultiProvider.tsx"에서 구현된 코드와 동일합니다.

 

하지만 Context를 너무 많이 남용해서는 안됩니다. 만약 Context의 데이터가 변경사항이 생기면 Context를 구독(의존)하는 모든 컴포넌트들이 리렌더링을 하게 됩니다. 이 경우 성능 상의 문제가 될 수 있겠죠?

Context 활용 사례

  • 다크모드와 같은 theme를 설정해줄 때
  • 사용자 인증 혹은 사용자 로그인 유무 확인할 때
  • 국가별 언어 설정
  • 사용자의 디바이스에 맞는 반응형 어플리케이션을 개발할 때
  • 외부 라이브러리 혹은 데이터에 접근할 때 (DB, API, 테스트 데이터 모킹 등)
  • 전역적으로 사용해야 되는 모달 창, 로딩 컴포넌트 등...

 

정리 및 결론

Context의 한계

  • 성능 저하
    • Context의 state를 구독(의존)하고 있는 모든 컴포넌트가 Context의 state가 변경사항이 생기면 모두 리렌더링을 한다.
  • 어려운 재사용
    •  Context를 벗어나면 재사용이 어렵다. → Context의 state와 의존 관계로 컴포넌트의 독립성이 떨어진다.(개인적인 생각입니다...)

Context의 한계를 극복하는 방법

  • 성능 저하
    • 테마(다크모드), 국가별 언어 설정 등 빈번히 업데이트가 되지 않는 상태에 주로 사용
    • 아니면 정말 필요한 곳에서 사용한다. → "구간(스코프?)" 설정을 촘촘하게 한다. 즉, Provider를 최대한 가깝게 설씌워준다.
  • 어려운 재사용
    • 마찬가지로 정말 필요하고 근접한 곳에 Provider를 씌워준다.
    • 혹은 Redux, Mobx, Rcoil 등과 같은 상태 관리 라이브러리를 함께 사용해 공통적으로 사용해야 할 state를 같이 관리해준다.

 

정리

  • Context를 너무 남용하지 말자.
  • 사용해야 한다면 정말 필요한 곳에 최대한 가깝게 Provider를 씌워주자.
  • Context API는 상태 관리 라이브러리를 대체할 수 없다. 상호 보완으로 함께 사용해야 한다.

 

오류나 올바르지 못한 개념이 있다면 언제든 피드백 주세요:)
반응형