ContextAPI
1. props와 context
보통 React 컴포넌트에서 다른 컴포넌트로 데이터를 전달할 때 props를 사용합니다. 부모 컴포넌트가 자식 컴포넌트에게 props를 통해 데이터를 전달하면, 자식 컴포넌트는 이 props를 통해 받은 데이터를 사용할 수 있습니다.
그러나 데이터를 필요로 하는 컴포넌트가 데이터를 생성한 컴포넌트의 직접적인 자식이 아니라, 여러 단계의 자식 컴포넌트를 거쳐야 하는 경우가 있습니다. 이런 상황에서 props를 통해 데이터를 전달하면, 해당 데이터를 실제로 필요로 하지 않는 중간 컴포넌트들도 props를 전달해야 하는 문제가 발생합니다. 이렇게 props가 여러 자식 컴포넌트를 거쳐 전달되는 현상을 props drilling이라고 합니다. 아래 그림의 왼쪽 부분이 이러한 props를 통한 데이터 전달 방식을 보여줍니다.
props drilling은 여러 가지 문제점을 야기합니다. 데이터를 사용하지 않는 중간 컴포넌트들이 불필요한 props를 다루게 되어 코드가 복잡해집니다. 코드가 복잡해지면서 데이터의 흐름을 파악하기 어려워져 유지보수성이 낮아집니다. 또한 props에 의존적인 컴포넌트는 다른 상황에서 재사용하기 어려워집니다. 이러한 props를 통한 데이터 전달 방식의 문제점을 개선하기 위해 도입된 것이 context입니다.

위 그림의 오른쪽 부분은 Context를 사용한 데이터 전달 방식을 보여줍니다. Context를 사용하면 props를 여러 단계에 걸쳐 전달하지 않고도 필요한 컴포넌트에 직접 데이터를 전달할 수 있습니다. Context를 props에 대한 전역 데이터 저장소라고 생각하면 됩니다.
Context는 데이터를 필요로 하는 자손 컴포넌트에게 직접 데이터를 전달할 수 있도록 합니다. 이를 통해 중간 컴포넌트들은 데이터 전달에 관여하지 않아도 되며, 결과적으로 코드의 복잡성을 줄이고 유지보수성을 향상시킬 수 있습니다.
1.1 props로 전달 예시
props를 통한 데이터 전달 방식의 예시를 살펴보겠습니다. 먼저 Vite를 사용하여 프로젝트를 생성합니다.
npm create vite@latest my-app -- --template react
cd my-app # my-app 프로젝트로 이동
npm install # 의존성 설치
App.css
.app-container {
width: 600px;
border: 2px solid #ddd;
border-radius: 4px;
}
.header {
display: flex;
justify-content: flex-end;
align-items: center;
background: cornflowerblue;
color: white;
padding: 10px;
font-weight: bold;
}
.content {
padding: 20px;
}
.container {
border: 1px solid lightgray;
border-radius: 4px;
padding: 15px;
}
App.jsx
import React from 'react';
import './App.css';
function App() {
const user = {
name: '위니북스',
email: 'wenivooks@weniv.com',
};
return (
<div className="app-container">
<header className="header">
<p>{user.name}님 환영합니다.</p>
</header>
<main className="content">
<ChildOne user={user} />
</main>
</div>
);
}
// user prop을 받아 ChildTwo로 전달
const ChildOne = ({ user }) => {
return (
<div className="container">
<h1>홈페이지</h1>
<ChildTwo user={user} />
</div>
);
};
// user prop을 받아 name과 email을 ChildThree로 전달
const ChildTwo = ({ user }) => {
return (
<div className="container">
<h2>설정페이지</h2>
<ChildThree name={user.name} email={user.email} />
</div>
);
};
// name과 email prop을 직접 사용
const ChildThree = ({ name, email }) => {
return (
<div className="container">
<h3>마이페이지</h3>
<p>이름: {name}님</p>
<p>이메일: {email}</p>
</div>
);
};
export default App;
이 예시에서는 App, ChildOne, ChildTwo, ChildThree 총 4개의 컴포넌트가 있습니다. user prop은 App 컴포넌트에서 정의되어 ChildOne, ChildTwo 컴포넌트를 거쳐 ChildThree까지 전달됩니다. App 컴포넌트는 user 객체를 정의하고 ChildOne에 전달합니다. ChildOne 컴포넌트는 user prop을 받아 ChildTwo로 전달합니다. ChildTwo 컴포넌트는 user prop을 받아 name과 email을ChildThree로 전달합니다. ChildThree 컴포넌트는 name과 email prop을 직접 사용합니다.

컴포넌트 계층 구조가 보이시나요? ChildOne과 ChildTwo 컴포넌트는 user prop을 직접 사용하지 않고 단순히 하위 컴포넌트로 전달하는 역할만 합니다. 이는 코드의 복잡성을 증가시키고 유지보수를 어렵게 만듭니다.
2. contextAPI
2.1 context 생성
Context API를 사용하면 props drilling 문제를 해결할 수 있습니다. Context는 쉽게 말해 데이터를 저장하는 전역 저장소라고 생각할 수 있습니다. 이 저장소는 애플리케이션 전체에서 접근 가능한 공간으로, 여러 컴포넌트에서 공유해야 하는 데이터를 보관합니다. Context를 생성하기 위해 React에서 제공하는 createContext 함수를 사용합니다. 이 함수를 통해 우리는 데이터를 저장하고 관리할 수 있는 저장소를 만들게 됩니다.
import { createContext } from 'react';
const UserContext = createContext();
createContext의 인자로는 기본값이 들어갑니다. 이 기본값은 Provider 컴포넌트를 사용하지 않은 경우나 상위 컴포넌트 트리에 일치하는 Provider가 없을 때 사용됩니다. 일반적으로 Provider를 사용해 값을 제공하므로 기본값을 사용하는 경우는 드뭅니다.
2.2 context로 데이터 전달
Context의 데이터를 자식 컴포넌트에 전달하기 위해서는 Context.Provider를 사용합니다. Context를 사용하는 최상위 컴포넌트를 Provider로 감싸 하위 컴포넌트에게 데이터를 제공합니다. 즉, Provider를 통해 Context의 데이터를 모든 하위 컴포넌트에 전달합니다. Provider의 value prop을 통해 공유할 데이터를 지정합니다.
App.jsx
import React, { createContext } from 'react';
import './App.css';
const UserContext = createContext({
name: '위니북스',
email: 'wenivooks@weniv.com',
});
function App() {
const user = {
name: '위니북스',
email: 'wenivbooks@weniv.com',
};
return (
<UserContext.Provider value={user}>
<div className="app-container">
<header className="header">
<p>{user.name}님 환영합니다.</p>
</header>
<main className="content">
<ChildOne />
</main>
</div>
</UserContext.Provider>
);
}
UserContext.Provider로 Context의 데이터를 사용하는 모든 컴포넌트의 최상위 컴포넌트를 감쌌습니다. valueprop으로는 user 객체 값을 전달하였습니다. 이제 모든 하위 컴포넌트에 UserContext의 데이터가 제공됩니다.
2.3 context 데이터 사용
Provider로 전달된 데이터를 사용하기 위해서는 Context.Consumer를 사용해야 합니다. Consumer를 통해 하위 컴포넌트들은 Context의 데이터에 접근할 수 있습니다. 즉, Provider로는 Context의 데이터를 전달하고 Consumer를 통해 데이터에 접근(소비)하여 사용합니다.
import React, { createContext } from 'react';
import './App.css';
const UserContext = createContext({
name: '위니북스',
email: 'wenivooks@weniv.com',
});
function App() {
const user = {
name: '위니북스',
email: 'wenivooks@weniv.com',
};
return (
<UserContext.Provider value={user}>
<div className="app-container">
<header className="header">
<UserContext.Consumer>
{(user) => <p>{user.name}님 환영합니다.</p>}
</UserContext.Consumer>
</header>
<main className="content">
<ChildOne />
</main>
</div>
</UserContext.Provider>
);
}
const ChildOne = () => {
return (
<div className="container">
<h1>홈페이지</h1>
<ChildTwo />
</div>
);
};
const ChildTwo = () => {
return (
<div className="container">
<h2>설정페이지</h2>
<ChildThree />
</div>
);
};
const ChildThree = () => {
return (
<UserContext.Consumer>
{(user) => (
<div className="container">
<h3>마이페이지</h3>
<p>이름: {user.name}님</p>
<p>이메일: {user.email}</p>
</div>
)}
</UserContext.Consumer>
);
};
export default App;
Consumer는 함수를 자식으로 가지는 컴포넌트입니다. Provider의 value로 전달받은 user 데이터를 함수의 인자로 받고, 이 함수 내에서 Context의 값을 사용할 수 있습니다. 함수는 JSX를 반환해야 합니다.
Consumer는 Context 값이 변경될 때만 해당 컴포넌트를 리렌더링합니다. 따라서 불필요한 리렌더링을 방지하여 성능을 최적화합니다.
인자로 받은 user 데이터의 name과 email을 사용하여 props가 아닌 Context로 화면에 데이터를 보여줍니다.
2.4 context 데이터 변경
ContextAPI는 데이터를 전달하는 기능은 제공하지만 데이터를 변경하는 기능은 제공하지 않습니다. 따라서 React의 useState나 useReducer와 같은 상태 관리 기능과 함께 사용하여 데이터를 변경합니다.
const [user, setUser] = useState({
name: '위니북스',
email: 'wenivooks@weniv.com',
});
context의 데이터로 사용될 값을 useState를 사용하여 state로 생성합니다.
import React, { createContext, useState } from 'react';
import './App.css';
const UserContext = createContext({
user: { name: '위니북스', email: 'wenivooks@weniv.com' },
setUser: () => {},
});
function App() {
const [user, setUser] = useState({
name: '위니북스',
email: 'wenivooks@weniv.com',
});
return (
<UserContext.Provider value={{ user, setUser }}>
<div className="app-container">
<header className="header">
<UserContext.Consumer>
{(user) => <p>{user.name}님 환영합니다.</p>}
</UserContext.Consumer>
</header>
<main className="content">
<ChildOne />
</main>
</div>
</UserContext.Provider>
);
}
UserContext.Provider의 value prop으로 user와 setUser를 전달합니다.
App.jsx
import React, { createContext, useState } from 'react';
import './App.css';
const UserContext = createContext({
user: { name: '위니북스', email: 'wenivooks@weniv.com' },
setUser: () => {},
});
function App() {
const [user, setUser] = useState({
name: '위니북스',
email: 'wenivooks@weniv.com',
});
return (
<UserContext.Provider value={{ user, setUser }}>
<div className="app-container">
<header className="header">
<UserContext.Consumer>
{({ user }) => <p>{user.name}님 환영합니다.</p>}
</UserContext.Consumer>
</header>
<main className="content">
<ChildOne />
</main>
</div>
</UserContext.Provider>
);
}
const ChildOne = () => {
return (
<div className="container">
<h1>홈페이지</h1>
<ChildTwo />
</div>
);
};
const ChildTwo = () => {
return (
<div className="container">
<h2>설정페이지</h2>
<ChildThree />
</div>
);
};
const ChildThree = () => {
return (
<UserContext.Consumer>
{({ user, setUser }) => (
<div className="container">
<h3>마이페이지</h3>
<p>이름: {user.name}님</p>
<p>이메일: {user.email}</p>
<button
onClick={() =>
setUser((prevUser) => ({
...prevUser,
email: 'newemail@example.com',
}))
}
>
이메일 변경
</button>
</div>
)}
</UserContext.Consumer>
);
};
export default App;
UserContext.Consumer를 통해 user와 setUser에 접근합니다. 이메일 변경 버튼을 추가하였고, 버튼을 누르면 전달받은 setUser 함수를 사용하여 이메일을 변경합니다.
| 이메일 변경 전 | 이메일 변경 후 |
|---|---|
![]() | ![]() |

