[React] 컨텍스트 (Context)

React에서 props만으로 데이터를 전달할 때 불편한 점이 많습니다.


부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하려면,
중간 단계에 있는 모든 컴포넌트가 불필요하게 props를 받아야 하는 문제가 발생합니다.

 

이 문제를 해결하기 위해 React는 Context API라는 기능을 제공합니다.


이 글에서는 Context의 개념부터 사용법, 최적화 방법까지 하나씩 알아보겠습니다.


01

Context가 필요한 이유

1. Props의 단점: Props Drilling

React에서 컴포넌트 간 데이터를 전달할 때 보통 props를 사용합니다.


하지만 부모 → 자식 → 손자로 props를 계속 전달해야 하는 경우,
중간 단계의 컴포넌트들이 불필요한 props를 받아야 하는 문제가 발생합니다.


이러한 현상을 Props Drilling(프롭스 드릴링)이라고 합니다.

 

❌ Props Drilling 문제 예제

function App() {
  const user = { name: "Alice", age: 25 };
  return <Parent user={user} />;
}

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

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

function GrandChild({ user }) {
  return <div>이름: {user.name}, 나이: {user.age}</div>;
}

  문제점

  • GrandChild에서 데이터를 사용하려면 App → Parent → Child → GrandChild 순으로 props를 전달해야 합니다.
  • Parent와 Child는 user 데이터를 직접 사용하지 않더라도 props를 전달해야 합니다.
  • 컴포넌트가 많아질수록 코드가 복잡해지고 유지보수가 어려워집니다.

02

Context란?

Context는 컴포넌트 간 데이터를 props 없이 전달할 수 있는 방법입니다.
즉, 전역적인 데이터 저장소 역할을 하는 객체입니다.

 

Context를 사용하면?

  • 중간 컴포넌트에서 불필요한 props 전달 없이 필요한 컴포넌트에서 직접 데이터 접근 가능
  • 여러 개의 Context를 생성하여 서로 다른 전역 상태를 관리 가능

1. Context 사용법

1) Context 생성하기
먼저, createContext를 사용하여 새로운 Context를 만듭니다.

import { createContext, useContext } from "react";

// Context 생성
export const UserContext = createContext(null);

// Context 사용하기 (useContext 훅 활용)
export const useUser = () => {
  return useContext(UserContext);
};

createContext로 전역 상태를 저장할 공간을 만들고, useContext로 값을 가져오는 형태입니다.

 

2) Provider로 값 전달하기
Context를 생성한 후, Provider를 사용하여 데이터를 공급해야 합니다.

import { UserContext } from "./UserContext";
import { useState } from "react";

const UserProvider = ({ children }) => {
  const [user, setUser] = useState({ name: "Alice", age: 25 });

  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  );
};

export default UserProvider;

이제 UserContext.Provider 안에 있는 모든 하위 컴포넌트는 user 값을 사용할 수 있습니다.

 

3) Context 사용하기 (useContext 훅 활용)
이제 하위 컴포넌트에서 useContext를 사용하여 Provider에서 제공한 데이터를 직접 가져올 수 있습니다.

import { useUser } from "./UserContext";

function Profile() {
  const { user } = useUser();
  return <div>이름: {user.name}, 나이: {user.age}</div>;
}

이제 Profile 컴포넌트는 user 데이터를 props 없이 바로 가져와서 사용할 수 있습니다.


03

Context 성능 최적화 (불필요한 리렌더링 방지)

Context를 사용할 때 잘못 사용하면 이전보다 더 많은 리렌더링이 발생할 수도 있습니다.
특히, Provider의 value에 객체를 직접 전달하면 컴포넌트가 불필요하게 렌더링 될 수 있습니다.

 

1. Context 사용 시 발생하는 리렌더링 문제

<UserContext.Provider value={{ user, setUser }}>
  {children}
</UserContext.Provider>

문제점:

  • React는 value={{ user, setUser }}를 새로운 객체로 인식
  • 리렌더링이 발생할 때마다 value 객체가 새롭게 생성됨
  • 불필요한 리렌더링이 발생하여 성능 저하 가능

 

2. 해결 방법: Context를 2개로 분리 (stateContext & dispatchContext)

stateContext에는 변경될 수 있는 값만 저장하고, dispatchContext에는 변하지 않는 함수를 저장하면 리렌더링을 줄일 수 있습니다.

 

📝 Context 분리 예제

import { createContext, useContext, useState, useMemo } from "react";

// 상태 저장 (stateContext)
export const UserStateContext = createContext(null);

// 함수 저장 (dispatchContext)
export const UserDispatchContext = createContext(null);

export const UserProvider = ({ children }) => {
  const [user, setUser] = useState({ name: "Alice", age: 25 });

  // 변경되지 않는 함수들은 useMemo로 감싸서 리렌더링 방지
  const memoizedDispatch = useMemo(() => ({ setUser }), []);

  return (
    <UserStateContext.Provider value={user}>
      <UserDispatchContext.Provider value={memoizedDispatch}>
        {children}
      </UserDispatchContext.Provider>
    </UserStateContext.Provider>
  );
};

// Context 가져오는 함수
export const useUserState = () => useContext(UserStateContext);
export const useUserDispatch = () => useContext(UserDispatchContext);

 

3. 최적화된 Context 사용법

이제 컴포넌트에서 stateContext와 dispatchContext를 각각 사용하면 불필요한 리렌더링을 줄일 수 있습니다.

import { useUserState, useUserDispatch } from "./UserContext";

function Profile() {
  const user = useUserState();
  return <div>이름: {user.name}, 나이: {user.age}</div>;
}

function ChangeProfile() {
  const { setUser } = useUserDispatch();

  return (
    <button onClick={() => setUser({ name: "Bob", age: 30 })}>
      프로필 변경
    </button>
  );
}
  • 이제 Profile과 ChangeProfile이 독립적으로 렌더링됩니다.
  • user 값이 변경될 때 ChangeProfile만 리렌더링 되고, Profile은 불필요하게 렌더링 되지 않습니다.

04

마무리

React에서 Props Drilling 문제는 컴포넌트가 많아질수록 코드가 복잡해지고 유지보수가 어려워지는 문제를 발생시킵니다. 이 문제를 해결하기 위해 Context API를 사용하면, 중간 컴포넌트를 거치지 않고 필요한 곳에서 직접 데이터를 가져올 수 있습니다.

 

하지만 Context API도 무조건 좋은 해결책은 아닙니다.
전역 상태가 많아지면 관리하기 어려워질 수 있습니다.
Provider의 value 값이 변경될 때마다 불필요한 리렌더링이 발생할 수도 있습니다.

 

이러한 문제를 방지하려면
Context를 stateContext와 dispatchContext로 나누어 관리하는 것이 효과적입니다.
useMemo를 활용하여 불필요한 렌더링을 최소화하는 것도 중요합니다.

 

결국, 상황에 따라 Props, Context API, Redux 같은 상태 관리 라이브러리를 적절히 선택하는 것이 가장 중요합니다.