본문 바로가기
개발 기록/front - react

React Context API로 상태 관리하기 : Props Drilling 해결

by jeong11 2024. 12. 27.
반응형

React Query에 이어 React Context API로 상태 관리하는 법을 공부해보려 한다 

 

<React Query로 상태 관리하기>

https://tiny-immj.tistory.com/118

 

React Query로 서버 상태 관리하기 : 데이터 패칭과 캐싱

※ 체크리스트① React Query의 개념과 필요성② React Query의 주요 기능(데이터 패칭, 캐싱, 뮤테이션) 익히기③ React Query를 활용한 서버 상태 관리 방법의 실습   1. React Query 1-1. React Query의 정의 R

tiny-immj.tistory.com


 

※체크 리스트 

① React Context API를 상태 관리 목적으로 사용하는 방법의 이해하기

② Props Drilling 문제를 해결하는 Context API의 기본 개념 학습 

③ Context API를 사용해 전역 상태 관리와 컴포넌트 간 데이터 공유를 구현하기 

 

1. Context API

1-1. Context API의 정의 

React Context API는 컴포넌트 계층 구조를 통해 데이터를 전달할 때, 
Props Drilling 문제를 해결하기 위한 전역 상태 관리 도구이다 

 

1-2. Context API의 주요 구성 요소

▶ Context : 전역상태를 관리하는 객체를 생성

▶ Provider : 데이터를 하위 컴포넌트에 전달

▶ Consumer : 데이터를 소비하는 컴포넌트

 

1-3. 동작 방식 

Provider에서 데이터를 제공하고, 하위 컴포넌트에서 Consumer 또는 useContext Hook으로 데이터를 소비한다 

 

1-4. 장점 

● Props Drilling 없이 컴포넌트 간 데이터를 공유할 수 있음 

● React의 내장 기능으로 추가 설치가 필요하지 않음 

 

※ Props Drilling 이게 도대체 뭔데? 

1) 정의 

React에서 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달해야 할 때, 그 데이터를 직접적으로 사용하지 않는 중간 컴포넌트에도 props를 통해 전달하는 과정을 말한다 
→ 데이터가 최종적으로 필요한 컴포넌트까지 전달하기 위해 중간 컴포넌트를 거쳐가야 하는 경우 발생한다 

 

import React from 'react';

// 최상위 App 컴포넌트에서 데이터 생성 
function App() {
	const user = { name: 'Alice', age: 25 };
    
    return (
    <div>
		<h1>Props Drilling Ex</h1>
		<Parent user={user} />
    </div>
    )
}

// 중간 Parent 컴포넌트
function Parent({ user }) {
	return(
	<div>
  		<h2>Parent Component</h2>
        <Child user={user} />
	</div>        
	);
}

// 중간 Child 컴포넌트
function Child({ user }) {
	return(
	<div>
    	<h3>Child Component</h3>
        <UserDetail user={user} />
    </div>
	);
}

// 실제로 데이터를 사용하는 UserDetail 컴포넌트
function UserDetail({ user }) {
	return(
	<div>
    	<h4>User Detail</h4>
        <p>Name: {user.name}</p>
        <p>Age: {user.age}</p>
	</div>
	);
}

export default App;

 

2) 문제점

코드 복잡성 증가

중간 컴포넌트가 많을수록 props 전달 코드가 중복되고 관리가 어려워진다 

 

유지보수 어려움

구조가 변경되거나 새로운 데이터를 추가하면 여러 컴포넌트를 수정해야 할 수 있다 

 

불필요한 렌더링

중간 컴포넌트가 불필요하게 렌더링될 가능성이 있다 

 

3) 문제를 해결하는 방법 

☞ Context API 사용해 계층 구조에 상관없이 데이터를 필요한 컴포넌트에 직접 전달한다  

☞ Redux, Zustand 같은 상태 관리 라이브러리를 사용해 전역 상태로 데이터를 관리하여 컴포넌트 간 데이터 전달 문제를 해결한다 

☞ React Query 등 데이터 fetching 라이브러리를 사용해 상태 관리를 간소화한다  

 

4) Context API와 Props Drilling의 차이점

Props Drilling은 데이터를 필요 없는 컴포넌트까지 전달해야 하지만,
Context API는 데이터를 필요한 곳에서 직접 접근할 수 있다 

 

 

2. Context API를 사용한 상태 관리 설정

▶ 순서

CounterContext를 생성하여 데이터를 공유할 준비를 한다 

CounterProvider로 상태를 관리하고 하위 컴포넌트에 제공할 데이터와 함수를 설정한다 

Root에서 Provider로 앱 전체를 감싼다 

④ 하위 컴포넌트에서 useContext를 사용해 데이터와 함수를 쉽게 가져와 사용할 수 있다 

→ 이 구조를 통해 React의 컴포넌트 트리 어디서든 상태에 접근할 수 있는 효율적인 상태 관리가 가능하다 

 

2-1. 진행 

1) Context 생성

import { createContext } from 'react';

const CounterContext = createContext();
export default CounterContext;

→ 여기서는 CounterContext를 생성하여, 상태를 관리하고 제공할 준비를 한다 

● createContext : React의 내장 함수로, Context를 생성한다. Context는 컴포넌트 트리의 어떤 컴포넌트에서도 접근할 수 있는 전역적인 상태를 관리하기 위해 사용된다. 

● CounterContext : 해당 객체는 Provider와 Consumer를 제공한다 

● Provider : 데이터를 제공하는 역할

● Consumer : 데이터를 구독하여 읽는 역할 

 

2) Provider로 상태 관리

import React, { useState } from 'react';
import CounterContext from './CounterContext';

function CounterProvider({ children }) {
	const [count, setCount] = useState(0);
	
    const increment = () => setCount((prev) => prev + 1);
    const decrement = () => setCount((prev) => prev - 1);
    
	return (
    	<CounterContext.Provider value={{ count, increment, decrement }}>
        	{children}
        </CounterContext.Provider>
	);
}

export default CounterProvider;

→ Provider 컴포넌트를 사용해 상태와 업데이트 함수를 공유 

● useState로 상태 관리하기 : 

useState(0)은 상태 변수 count를 초기값 0으로 선언한다 

setCount는 count 값을 변경하기 위해 사용하는 함수이다 

● increment 함수 : count를 1 증가시킨다

● decrement 함수 : count를 1 감소시킨다 

● CounterContext.Provider의 value 속성 : 하위 컴포넌트에게 제공할 데이터와 메소드를 지정한다 

{ count, increment, decrement }는 현재 상태와 이를 조작하는 함수를 하위 컴포넌트들이 사용할 수 있도록 전달한다 

● {children} : 이 Provider는 children으로 전달받은 컴포넌트를 감싸 상태를 제공하며, 감싸진 모든 하위 컴포넌트는 value에 접근할 수 있다  

 

3) Provider로 감싸기

import React from 'react';
import CounterProvider from './CounterProvider';
import App from './App';

function Root() {
	return (
		<CounterProvider>
			<App />
		</CounterProvider>
	);
}

export default Root;

→ 최상위 컴포넌트를 Provider로 감싸 데이터 공유 

● CounterProvider : 

App 컴포넌트를 CounterProvider로 감싸서 Context를 사용할 수 있는 환경을 설정한다 

이제 App와 하위 컴포넌트는 CounterProvider가 제공하는 value(count, increment, decrement)를 사용할 수 있다 

● 루트 컴포넌트 구조 : 

루트 컴포넌트는 전체 애플리케이션에서 상태를 관리할 준비를 하며 Provider를 통해 모든 하위 컴포넌트에 상태와 상태 변경 함수를 전달한다 

 

2-2. Context 사용 예시

App 컴포넌트나 하위 컴포넌트에서 Context를 사용하는 방법

import React, { useContext } from 'react';
import CounterContext from './CounterContext';

function Counter() {
	const { count, increment, decrement } = useContext(CounterContext);

	return (
		<div>
			<h1>Count: {count}</h1>
			<button onClick={increment}>Increment</button>
			<button onClick={decrement}>Decrement</button>
		</div>
	);
}

export default Counter;

● useContext(CounterContext)를 사용해 Provider가 제공하는 데이터를 쉽게 가져온다 

 

3. Context 데이터 소비하기 

3-1. 두 가지 방식으로 수행 가능  

1) useContext Hook 사용 방식 (현대적 방식) 

import React, { useContext } from 'react';
import CounterContext from './CounterContext';

function Counter() {
	const { count, increment, decrement } = useContext(CounterContext);
    
    return (
    	<div>
        	<p>Count : {count}</p>
        	<button onClick={increment}>Increment</button>
        	<button onClick={decrement}>Decrement</button>
        </div>
    );
}

export default Counter;

① 단계별 설명 

useContext로 데이터 가져오기 → 데이터 표시 및 동작 연결 

② 장점 : 

코드가 간결하고 직관적임

여러 Context를 사용할 때도 간편하게 데이터를 가져올 수 있음 

함수형 컴포넌트에서  React Hook과 함께 사용할 수 있음 

 

 

2) Consumer 컴포넌트 사용 방식 (예전의 방식)

import React from 'react';
import CounterContext from './CounterContext';

function Counter() {
	return (
		<CounterContext.Consumer>
			{({ count, increment, decrement }) => (
			<div>
				<p>Count: {count}</p>
				<button onClick={increment}>Increment</button>
				<button onClick={decrement}>Decrement</button>
			</div>
			)}
		</CounterContext.Consumer>
	);
}

export default Counter;

① 단계별 설명 : 

Consumer로 데이터 가져오기 → 데이터 표시 및 동작 연결 

 

② 단점 :

코드가 길고 읽기 어렵다 

중첩된 구조로 가독성이 떨어질 수 있다 

함수형 컴포넌트와 Hook이 없는 환경에서 주로 사용되던 방식이다 

 

 

3-2. Context API 상태 관리와 데이터 소비의 차이점 

구분 상태 관리 설정 데이터 소비하기
역할 Context를 생성하고 Provider로 데이터를 제공 useContext를 사용해 데이터를 읽고 활용
위치 일반적으로 Provider를 정의한 컴포넌트 Provider로 감싸진 하위 컴포넌트
코드 구성 createContext, Provider, 상태 관리 로직 포함  useContext를 통해 데이터와 함수에 접근 
주요 함수/Hook createContext, useState useContext 
사용 목적 데이터와 상태를 하위 컴포넌트로 전달할 준비  데이터를 읽고 화면에 표시하거나 로직에 사용,  
Context 상태 관리 설정에서 준비한 데이터를 실제로 사용하는 단계 

 

 

4. Context API로 상태 관리 구현

이 전에 설명한 Context 상태 관리 설정과 Context 데이터 소비하기에서 배운 내용을 응용하여 여러 개의 Context를 동시에 관리하고 사용하는 방법을 보여준다 

4-1. Context 상태 관리 단계들과 차이점 

1) Context 상태 관리 설정과 차이점 

● Context 상태 관리 설정은 단일 Context를 생성하고 Provider를 사용해 데이터를 제공하는 기본 구조를 설정하는데 중점을 둔다 

● Context API로 상태 관리 구현은 다중 context를 사용하며, 각각 Context에 독립적인 상태와 데이터를 할당해 관리한다 

 

2) Context 데이터 소비하기와의 차이점 

● Context 데이터 소비하기에서는 단일 Context에서 데이터를 읽고 사용하는 방식에 초점을 맞췄다 

● 이 코드는 여러 Context를 한 컴포넌트에서 동시에 소비하며, 각 Context에서 제공하는 데이터를 조합해 사용한다 

 

4-2. 코드 구조 분석 

1단계 : 다중 Context 관리 

const ThemeContext = createContext();
const CounterContext = createContext();

function App() {
	return (
	<ThemeContext.Provider value={{ theme: 'dark' }}>
		<CounterContext.Provider value={{ count: 10 }}>
			<ChildComponent />
		</CounterContext.Provider>
	</ThemeContext.Provider>
	)
}

① 여러 Context 생성 : 

ThemeContext : 테마 관련 데이터를 관리

CounterContext : 카운터 데이터를 관리 

② 중첩된 Provider : 

ThemeContext.Provider와 CounterContext.Provider를 중첩하여 각 Context에 데이터를 제공

ChildComponent는 중첩된 두 Provider로부터 데이터를 소비할 수 있음 

 

2단계 : 중첩된 Context 소비

function ChildComponent() {
	const { theme } = useContext(ThemeContext);
    const { count } = useContext(CounterContext);
    
    return (
    	<div style={{ color: theme === 'dark' ? 'white' : 'black' }}>
        	Count: {count}
        </div>
    );
}

① useContext로 다중 Context 소비 : 

useContext(ThemeContext)로 테마 데이터를 읽음

useContext(CounterContext)로 카운터 데이터를 읽음 

각 Context의 데이터를 독립적으로 가져와 조합하여 사용 

② 데이터 조합 : 

테마 데이터를 스타일에 적용하여 배경색이나 텍스트 색상을 변경

카운터 데이터를 화면에 표시 

 

 

 

5. 실습 과제

5-1. 과제 구현 목표와 예제 

① Context API를 사용하여 사용자 이름과 나이를 관리하고 화면에 표시하세요 

② 테마(Context)를 추가하여 배경색을 변경할 수 있는 버튼을 구현하세요 

 

예제 코드

function ThemeToggler() {
	const { theme, toggleTheme } = useContext(ThemeContext);
    
	return(
		<button onClick={toggleTheme}>
			현재 테마: {theme === 'dark' ? '다크' : '라이트'}
		</button>
	);
}

 

5-2. 구현

1) 프로젝트 생성

npx create-react-app react-context-api-ex

 

2)  프로젝트 구성

UserContext.js : 사용자 정보를 위한 Context 생성

ThemeProvider.js : 테마 변경 기능을 위한 Context 생성

UserProvider.js : 사용자 정보 관리와 Provider 제공

App.js : 전체 애플리케이션 구조와 Context Provider 중첩

UserProfile.js : 사용자 정보를 화면에 표시

ThemeToggler.js : 테마를 변경하는 버튼

구조

 

3) 구현 

> UserContext.js

// src/context/UserContext.js
import { createContext } from 'react';

export const UserContext = createContext();

 

> UserProvider.js

// src/context/UserProvider.js
import React, { useState } from 'react';
import { UserContext } from './UserContext';

function UserProvider({ children }) {
	const [user, setUser] = useState({ name: '홍길동', age: 25 });
    
    return (
    	<UserContext.Provider value={{ user, setUser }}>
    		{children}
    	</UserContext.Provider>    
    );
}

export default UserProvider;

 

> ThemeProvider.js

// src/context/ThemeProvider.js
import React, { createContext, useState } from 'react';

export const ThemeContext = createContext();

function ThemeProvider({ children }) {
	const [theme, setTheme] = useState('light');
    
    const toggleTheme = () => {
    	setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
    };
    
    return (
    	<ThemeContext.Provider value={{ theme, toggleTheme }}>
        	{children}
        </ThemeContext.Provider>
    );
}

export default ThemeProvider;

 

> App.js

// src/App.js
import React from 'react';
import UserProvider from './context/UserProvider';
import ThemeProvider from './context/ThemeProvider';
import UserProfile from './components/UserProfile';
import ThemeToggler from './components/ThemeToggler';

function App(){
	return (
		<UserProvider>
			<ThemeProvider>
				<div style={{ textAlign: 'center', marginTop: '50px' }}>
					<UserProfile />
					<ThemeToggler />
				</div>
			</ThemeProvider> 
		</UserProvider>
	);
}

export default App;

 

> UserProfile.js

// src/components/UserProfile.js
import React, { useContext } from 'react';
import { UserContext } from '../context/UserContext';
import { ThemeContext } from '../context/ThemeProvider';

function UserProfile() {
	const { user } = useContext(UserContext);
	const { theme } = useContext(ThemeContext);
    
    return (
    	<div
			style={{
				background: theme === 'dark' > '#333' : '#fff',
				color: theme === 'dark' ? '#fff' : '#000',
				padding: '20px',
				borderRadius: '8px',
			}}
        >
			<h2>사용자 정보</h2>
			<p>이름: {user.name}</p>
			<p>나이: {user.age}</p>
        </div>
    );
}

export default UserProfile;

 

> ThemeToggler.js

// src/components/ThemeToggler.js
import React, { useContext } from 'react';
import { ThemeContext } from '../context/ThemeProvider';

function ThemeToggler() {
	const { theme, toggleTheme } = useContext(ThemeContext);
	
	return (
		<button
			onClick={toggleTheme}
			style={{
				marginTop: '20px',
				padding: '10px 20px',
				cursor: 'pointer',
			}}
		>
			현재 테마: {theme === 'dark' ? '다크':'라이트' }
		</button>
	);
}

export default ThemeToggler;

 

4) 실행

npm start

 

라이트 테마
다크 테마

 

 

※ 정리

Q&A

① Context API와 Redux의 차이점은 무엇인가요? 

→ 목적과 구현 방식에 있어 차이점이 있다 

Context API는 작은 애플리케이션이나 상태가 간단한 경우 유용하며 구현이 간단하고,

Redux는 큰 애플리케이션에서 복잡한 상태 관리를 효율적으로 처리하기 위해 사용된다 

Redux는 상태 관리 로직을 더 명확하게 정의하고 디버깅 도구와 성능 최적화를 제공한다 

 

② Context API는 어떤 규모의 애플리케이션에 적합한가요? 

→ 간단한 애플리케이션에서 여러 컴포넌트가 동일한 데이터를 공유해야 할 때 유용하다 

 

☞ Context API는 Props Drilling 문제를 해결하며, 간단한 전역 상태 관리에 유용하다 

☞ Provider로 상태를 제공하고 useContext로 데이터를 소비한다 

☞ Redux와 달리 복잡한 상태 관리는 적합하지 않을 수 있다 

 

 

반응형