[React] context API로 전역 상태 관리하기
Context API
Context API의 Context는 컴포넌트의 단계마다 Props를 일일이 전달하지 않고, 원하는 컴포넌트 트리에 데이터를 제공할 수 있는 State Management 툴입니다.
React는 데이터를 상위에서 하위 컴포넌트로 Props를 전달하는 구조입니다. 그래서 구현된 컴포넌트 트리에 컴포넌트 개수가 많아질 경우에는 데이터를 전달하는 과정이 복잡해질 수 있습니다. Context API를 이용하면 각 컴포넌트마다 데이터를 파라미터로 받거나 하위 컴포넌트에 데이터를 전달하는 과정이 생략됩니다.
즉, 이러한 관리를 전역적으로 상태를 관리한다고 말할 수 있습니다.
Context API에는 Provider
, createContext
, Consumer
의 개념이 있습니다. 이들의 개념만 알아도 Context API를 사용하기에는 무리가 없습니다. 그만큼 러닝커브도 낮기 때문에, 러닝 커브가 높은 Redux보다 요즘 다시 떠오르는 추세이기도 합니다.
createContext
- context 객체를 만듭니다. 이 함수를 호출하면 Provider
와 Consumer
컴포넌트를 사용할 수 있습니다.
Provider
- Provider 컴포넌트를 통해서 Context의 값을 설정할 수 있습니다. 값을 설정하고 전달하고 싶은 컴포넌트 트리의 최상단에서 감싸주면 됩니다.
Consumer
- context의 변화를 구독하는 컴포넌트입니다. 이 컴포넌트를 사용하면 설정된 context를 사용할 수 있습니다. 좀더 간편하게 사용하기 위해서 React Hook에 내장된 useContext
를 사용하여도 좋습니다.
간단한 예시를 통해 배우는 Context API
만약 전역상태 관리 없이 State를 전달하고자 한다면 이런 식의 코드가 될 겁니다.
ContextSample.js
import React, { createContext, useContext } from 'react';
function Child({text}) {
return <div> 안녕하세요? {text} </div>;
}
function Parent({ text }) {
return <Child text={text} />;
}
function GrandParent({ text }) {
return <Parent text={text} />;
}
function ContextSample() {
return <GrandParent text="GOOD" />;
}
export default ContextSample;
ContextSample 함수는 GrandParent를 렌더링 하면서 "GOOD" 이 저장된 text를 하위 컴포넌트인 GrandParent에 전달하고 있습니다. GrandParent 함수 또한, 상위 컴포넌트에서 받아온 text를 하위 컴포넌트에 전달합니다. 최종적으로 Child 컴포넌트에 있는 <div> 태그를 화면에 렌더링 하게 되는데, 결국 ContextSample()에서 전달받은 "GOOD"이라는 단어가 화면에 렌더링 될 것입니다.
하나의 파일 안에서 조그만한 함수로 예를 들어서, 별 거 없어 보일 수도 있습니다. 하지만 관리해야 할 파일이 많아지고
이 파일들이 전역 State를 공유하고 있다면 어떻게 될까요? 하나하나 파일을 보면서 Props를 잘 전달하는지 확인해야 되고 State 관리하기도 어려워질 것입니다. 이를 예방하기 위해 전역 상태 관리 툴을 사용하는 것이며, 그중 하나가 Context API입니다. 근데 넌
Context API를 사용하여 위의 예제를 바꿔보겠습니다.
import React, { createContext, useContext, useState } from 'react';
// Context를 만들때는 createContext를 사용한다.
const MyContext = createContext('defaultValue');
function Child() {
// useContet는 context의 값을 읽어와서 사용할 수 있게 해주는 React에 내장된 Hook이다.
const text = useContext(MyContext);
return <div> 안녕하세요? {text} </div>;
}
function Parent() {
return <Child/>;
}
function GrandParent() {
return <Parent/>;
}
function ContextSample() {
const [value, setValue] = useState(true);
return (
// Provider를 통해서 value를 설정하여 MyContext의 값을 설정할 수 있다.
<MyContext.Provider value={value ? "Good" : "Bad"}>
<GrandParent/>
<button onClick={() => setValue(!value)}>Click Me!</button>
</MyContext.Provider>
);
}
export default ContextSample;
createContext 함수로 Context(MyContext)를 생성합니다. 가장 상위 컴포넌트인 ContextSample() 함수에서 Provider를 통해서 value 값을 설정할 수 있고, 이를 통해 MyContext의 값이 설정됩니다. createContext()로 만든 Context는 다른 파일에서 선언할 수 있습니다. 즉, 다른 파일에서 작성된 Context는 쉽게 불러와서 언제든지 사용할 수 있다는 것이 Context API의 장점입니다.
최상위 컴포넌트에서는 Provider를 통해 Context의 값을 선언하고 하위 컴포넌트로 해당 값을 전달해주는 역할을 합니다. 이 Context의 값을 사용하기 위해서는 useContext Hook을 이용하여 사용할 수 있습니다. useContext는 Context의 값을 읽어와서 사용할 수 있게 해주는 React에 내장된 훅입니다.
TodoList에 적용해보는 Context API
지금까지 진행한 기존 코드를 Toggle로 보여드리겠습니다.
import React, { useRef, useMemo, useCallback, useReducer } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';
import useInputs from './useInputs';
function countActiveUsers(users) {
console.log('활성 사용자 수를 세는중...');
return users.filter((user) => user.active).length;
}
const initialState = {
inputs: {
username: '',
email: '',
},
users: [
{
id: 1,
username: 'velopert',
email: 'public.velopert@gmail.com',
active: true,
},
{
id: 2,
username: 'tester',
email: 'tester@example.com',
active: false,
},
{
id: 3,
username: 'liz',
email: 'liz@example.com',
active: false,
},
],
};
function reducer(state, action) {
switch (action.type) {
case 'CREATE_USER':
return {
users: state.users.concat(action.user),
};
case 'TOGGLE_USER':
return {
...state,
users: state.users.map((user) =>
user.id === action.id ? { ...user, active: !user.active } : user
),
};
case 'REMOVE_USER':
return {
...state,
users: state.users.filter((user) => user.id !== action.id),
};
default:
return state;
}
}
function App() {
const [{ username, email }, onChange, reset] = useInputs({
username: '',
email: '',
});
const [state, dispatch] = useReducer(reducer, initialState);
const nextId = useRef(4);
const { users } = state;
const onCreate = useCallback(() => {
dispatch({
type: 'CREATE_USER',
user: {
id: nextId.current,
username,
email,
},
});
reset();
nextId.current += 1;
}, [username, email,reset]);
const onToggle = useCallback((id) => {
dispatch({
type: 'TOGGLE_USER',
id,
});
}, []);
const onRemove = useCallback((id) => {
dispatch({
type: 'REMOVE_USER',
id,
});
}, []);
const count = useMemo(() => countActiveUsers(users), [users]);
return (
<>
<CreateUser
username={username}
email={email}
onChange={onChange}
onCreate={onCreate}
/>
<UserList users={users} onRemove={onRemove} onToggle={onToggle} />
<div>활성 사용자 수 : {count}</div>
</>
);
}
export default App;
import React from "react";
const User = React.memo(function User({ user, onRemove, onToggle }) {
const { username, email, id, active } = user;
return (
<div>
<b
style={{
color: active ? "green" : "black",
cursor: "pointer",
}}
onClick={() => onToggle(id)}
>
{username}
</b>
<span>({email})</span>
<button onClick={() => onRemove(id)}>삭제</button>
</div>
);
});
function UserList({ users, onRemove, onToggle }) {
return (
<div>
{users.map((user) => (
<User
user={user}
key={user.id}
onRemove={onRemove}
onToggle={onToggle}
/>
))}
</div>
);
}
export default React.memo(UserList);
import React from "react";
function CreateUser({ username, email, onChange, onCreate }) {
console.log('CreateUser');
return (
<div>
<input
name="username"
placeholder="계정명"
onChange={onChange}
value={username}
/>
<input
name="email"
placeholder="이메일"
onChange={onChange}
value={email}
/>
<button onClick={onCreate}>등록</button>
</div>
);
}
export default React.memo(CreateUser);
import { useState, useCallback } from 'react';
function useInputs(initialForm) {
const [form, setForm] = useState(initialForm);
// change
const onChange = useCallback((e) => {
const { name, value } = e.target;
setForm((form) => ({ ...form, [name]: value }));
}, []);
const reset = useCallback(() => setForm(initialForm), [initialForm]);
return [form, onChange, reset];
}
export default useInputs;
위의 코드는 useReducer Hook 함수를 이용하여 state 관리까지 되어있습니다.
위에서 수정된 코드는 주석으로 설명하겠습니다.
App.js
import React, {
useMemo,
useReducer,
createContext,
} from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';
function countActiveUsers(users) {
console.log('활성 사용자 수를 세는중...');
return users.filter((user) => user.active).length;
}
const initialState = {
inputs: {
username: '',
email: '',
},
users: [
{
id: 1,
username: 'velopert',
email: 'public.velopert@gmail.com',
active: true,
},
{
id: 2,
username: 'tester',
email: 'tester@example.com',
active: false,
},
{
id: 3,
username: 'liz',
email: 'liz@example.com',
active: false,
},
],
};
function reducer(state, action) {
switch (action.type) {
case 'CREATE_USER':
return {
users: state.users.concat(action.user),
};
case 'TOGGLE_USER':
return {
...state,
users: state.users.map((user) =>
user.id === action.id ? { ...user, active: !user.active } : user
),
};
case 'REMOVE_USER':
return {
...state,
users: state.users.filter((user) => user.id !== action.id),
};
default:
return state;
}
}
// UserDispatch 생성
export const UserDispatch = createContext(null);
function App() {
// onCreate, onToggle 삭제
const [state, dispatch] = useReducer(reducer, initialState);
const { users } = state;
const count = useMemo(() => countActiveUsers(users), [users]);
return (
{/*dispatch(action) 값을 UserDispatch Context의 값으로 설정하고 하위 컴포넌트에 전달.*/}
<UserDispatch.Provider value={dispatch}>
{/*CreateUser 컴포넌트에 아무 Props도 전달하지 않음*/}
<CreateUser/>
{/*UserList 컴포넌트에 onRemove, onToggle 함수를 전달하지 않음*/}
<UserList users={users} />
<div>활성 사용자 수 : {count}</div>
</UserDispatch.Provider>
);
}
export default App;
UserList.js
import React, { useContext } from 'react';
// App.js에서 UserDispatch Context를 import함
import { UserDispatch } from './App';
const User = React.memo(function User({ user }) {
const { username, email, id, active } = user;
// App.js에 있는 useReducer의 dispatch를 useContext Hook을 이용하여 사용할 수 있다.
const dispatch = useContext(UserDispatch);
return (
<div>
<b
style={{
color: active ? 'green' : 'black',
cursor: 'pointer',
}}
{/*dispath로 useReducer에 있는 action을 이용한다.*/}
onClick={() => dispatch({ type: 'TOGGLE_USER', id })}
>
{username}
</b>
<span>({email})</span>
{/*dispath로 useReducer에 있는 action을 이용한다.*/}
<button onClick={() => dispatch({ type: 'REMOVE_USER', id })}>
삭제
</button>
</div>
);
});
// 하위 컴포넌트인 User 함수에 onRemove, onToggle 함수를 전달할 필요가 없다.
function UserList({ users }) {
return (
<div>
{users.map((user) => (
<User user={user} key={user.id} />
))}
</div>
);
}
export default React.memo(UserList);
CreateUser.js
import React, { useContext, useRef } from 'react';
import { UserDispatch } from './App';
import useInputs from './useInputs';
// 기존에는 파라미터로 username, email, onChange, onCreate를 받아왔지만,
// Context API를 사용하기 때문에 받아올 필요가 없다.
function CreateUser() {
console.log('CreateUser');
// useContext로 받아온 Context의 값을 사용한다.
const dispatch = useContext(UserDispatch);
const [{ username, email }, onChange, reset] = useInputs({
username: '',
email: '',
});
const nextId = useRef(4);
// App.js에 있던 기존 onCreate 함수를 CreateUser 컴포넌트에서 관리한다.
const onCreate = () => {
dispatch({
type: 'CREATE_USER',
user: {
id: nextId.current,
username,
email,
},
});
reset();
nextId.current += 1;
};
return (
<div>
<input
name="username"
placeholder="계정명"
onChange={onChange}
value={username}
/>
<input
name="email"
placeholder="이메일"
onChange={onChange}
value={email}
/>
<button onClick={onCreate}>등록</button>
</div>
);
}
export default React.memo(CreateUser);
'공부 || 정리 > React' 카테고리의 다른 글
[React] useState (0) | 2021.07.21 |
---|---|
[React] useReducer / 커스텀 Hook 만들기 (0) | 2021.04.16 |
[React] useMemo / useCallback / React.memo (0) | 2021.04.11 |
[React] 배열 항목 제거, 수정 / useEffect (0) | 2021.04.09 |
[React] 배열 렌더링 / useRef 활용 / 배열에 항목 추가 (0) | 2021.04.04 |
댓글
이 글 공유하기
다른 글
-
[React] useState
[React] useState
2021.07.21 -
[React] useReducer / 커스텀 Hook 만들기
[React] useReducer / 커스텀 Hook 만들기
2021.04.16 -
[React] useMemo / useCallback / React.memo
[React] useMemo / useCallback / React.memo
2021.04.11 -
[React] 배열 항목 제거, 수정 / useEffect
[React] 배열 항목 제거, 수정 / useEffect
2021.04.09