아래 코드를 보면 App 컴포넌트에서 onToggle
, onRemove
, onModify
가 구현되어 있고, 이 함수들은 UserList 컴포넌트를 거쳐 각 User 컴포넌트들에게 전달되고 있다.
여기서 UserList 컴포넌트의 경우, 해당 함수들을 직접 사용하지 않고 함수들을 전달하기 위한 중간다리 역할만 하고 있다.
function UserList({users, onRemove, onToggle, onModify}){
return(
<div>
{users.map(user =>
<User
user={user}
key={user.id}
onRemove={onRemove}
onToggle={onToggle}
onModify={onModify}
/>)}
</div>
)
}
위와 같이 특정 함수를 특정 컴포넌트를 거쳐 원하는 컴포넌트에게 전달하는 작업은 리액트로 개발할 때 자주 발생할 수 있는 일이다. 크게 불편하진 않지만 3~4개 이상의 컴포넌트를 거쳐 전달을 해야하는 일이 발생하게 된다면, 이는 매우 번거로울 것이다.
그럴 땐, 리액트의 Context API 와 이전 글에서 다뤘던 dispatch
를 함께 사용하면 이러한 복잡한 구조를 해결할 수 있다.
리액트의 Context API 를 사용하면 프로젝트 안에서 전역적으로 사용할 수 있는 값을 관리할 수 있다. 이 값은 꼭 상태를 가르키지 않아도 된다. 함수일수도 있고, 어떤 외부 라이브러리 인스턴스일수도 있고, 심지어 DOM 일 수도 있다.
물론 Context API 를 사용해서 프로젝트의 상태를 전역적으로 관리할 수도 있다. 이는 나중에 자세히 다뤄보도록 하겠다.
Context 를 만들 땐, 다음과 같이 React.createContext()
라는 함수를 사용한다.
const UserDispatch = React.createContext(null);
createContext
의 파리미터에는 Context 의 기본값을 설정할 수 있다. 여기서 설정하는 값은 Context 를 쓸때 값을 따로 지정하지 않을 경우 사용되는 기본(초기)값이다.
Context 를 만들면 Context 안에 Provider
라는 컴포넌트가 들어있는데, 이 컴포넌트를 통하여 Context 의 값을 정할 수 있다. 이 컴포넌트를 사용할 때는 value
라는 값을 설정해주면 된다.
<UserDispatch.Provider value={dispatch}>...</UserDispatch.Provider>
이렇게 설정해주고 나면 Provider
에 의하여 감싸진 컴포넌트 중 어디서든지 Context 의 값을 바로 조회해서 사용할 수 있다.
Context 를 만들고 내보내주었다. 그리고 dispatch
를 Context 의 값으로 설정해주었다.
import React, { useRef, useState, useMemo, useCallback, useReducer } from 'react';
import UserList from './Components/UserList';
import CreateUser from './Components/CreateUser';
const countActiveUsers = (users) => {
console.log('활성 사용자 수를 세는중...');
return users.filter(user => user.active).length;
}
const initialState = {
input: {
username: '',
email: '',
id: ''
},
users: [
{
id: 1,
username: 'velopert',
email: 'public.velopert@gmail.com',
active: true
},
{
id: 2,
username: 'tester',
email: 'tester@example.com',
active: true
},
{
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),
input: {
...state.input,
username: '',
email: ''
}, //input 값 초기화
}
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)
}
case 'MODIFY_USER':
return {
...state,
input: {
username: action.username,
email:action.email,
id: action.id
}
}
case 'UPDATE_USER':
return {
...state,
users: state.users.map(user => user.id === state.input.id ? {...user, username: state.input.username, email: state.input.email} : user),
input: {
...state.input,
username: '',
email: ''
}
}
default:
return state;
}
}
export const UserDispatch = React.createContext(null);
// Point!, Context 를 만들고 내보내주었다.
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const nextId = useRef(4);
const { users } = state;
const { username, email } = state.input;
const onChange = useCallback((e) => {
const { name, value } = e.target;
dispatch({
type: 'CHANGE_INPUT',
name,
value
})
}, []);//재선언할 필요가 없는 함수이다.
const onCreate = useCallback(() => {
dispatch({
type: 'CREATE_USER',
user: {
id: nextId.current,
username: username,
email: email
},
})
nextId.current += 1;
}, [username, email])
//여기서 (...)users를 사용해 user를 업데이트하는게 아니기 때문에 deps에 없음!
const onToggle = useCallback((id)=> {
dispatch({
type: 'TOGGLE_USER',
id: id
//굳이 아래처럼 안해도 됨!
//user: {
// id: id
// }
})
}, [])
const onRemove = useCallback((id) => {
dispatch({
type: 'REMOVE_USER',
id: id
})
}, [])
const onModify = useCallback(({username, email, id})=> {
dispatch({
type: 'MODIFY_USER',
username: username,
email: email,
id: id
})
}, [])
const onUpdate = useCallback(()=>{
dispatch({
type: 'UPDATE_USER'
})
}, [])
const count = useMemo(() => countActiveUsers(users), [users]);
return (
<UserDispatch.Provider value={dispatch}>
// Point!, 태그로 감싼 부분의 어디서든 Context value 인 dispatch 를 조회하여 사용할 수 있다.
<CreateUser username={username} email={email} onChange={onChange} onCreate={onCreate} onUpdate={onUpdate} />
<UserList users={users} onToggle={onToggle} onRemove={onRemove} onModify={onModify}/>
<div>활성사용자 수 : {count}</div>
</UserDispatch.Provider>
);
}
export default App;
Context 를 다 만든 후, App 에서 onToggle
과 onRemove
, onModify
를 지우고 UserList 에게 props 로 전달하는 것도 지웠다.
import React, { useRef, useState, useMemo, useCallback, useReducer } from 'react';
import UserList from './Components/UserList';
import CreateUser from './Components/CreateUser';
const countActiveUsers = (users) => {
console.log('활성 사용자 수를 세는중...');
return users.filter(user => user.active).length;
}
const initialState = {
input: {
username: '',
email: '',
id: ''
},
users: [
{
id: 1,
username: 'velopert',
email: 'public.velopert@gmail.com',
active: true
},
{
id: 2,
username: 'tester',
email: 'tester@example.com',
active: true
},
{
id: 3,
username: 'liz',
email: 'liz@example.com',
active: false
}
]
};
function reducer(state, action){
switch (action.type){
case 'CHANGE_INPUT':
return {
...state,
input: {
...state.input,
[action.name]: action.value
}
}
case 'CREATE_USER':
return {
users: state.users.concat(action.user),
//concat() 메서드는 인자로 주어진 배열이나 값들을 기존 배열에 합쳐서 새 배열을 반환한다.
input: {
...state.input,
username: '',
email: ''
}, //input 값 초기화
}
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)
}
case 'MODIFY_USER':
return {
...state,
input: {
username: action.username,
email:action.email,
id: action.id
}
}
case 'UPDATE_USER':
return {
...state,
users: state.users.map(user => user.id === state.input.id ? {...user, username: state.input.username, email: state.input.email} : user),
input: {
...state.input,
username: '',
email: ''
}
}
default:
return state;
}
}
export const UserDispatch = React.createContext(null);
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const nextId = useRef(4);
const { users } = state;
const { username, email } = state.input;
const onChange = useCallback((e) => {
const { name, value } = e.target;
dispatch({
type: 'CHANGE_INPUT',
name,
value
})
}, []);//재선언할 필요가 없는 함수이다.
const onCreate = useCallback(() => {
dispatch({
type: 'CREATE_USER',
user: {
id: nextId.current,
username: username,
email: email
},
})
nextId.current += 1;
}, [username, email])
//여기서 (...)users를 사용해 user를 업데이트하는게 아니기 때문에 deps에 없음!
const onUpdate = useCallback(()=>{
dispatch({
type: 'UPDATE_USER'
})
}, [])
const count = useMemo(() => countActiveUsers(users), [users]);
return (
<UserDispatch.Provider value={dispatch}>
<CreateUser username={username} email={email} onChange={onChange} onCreate={onCreate} onUpdate={onUpdate} />
<UserList users={users} />
<div>활성사용자 수 : {count}</div>
</UserDispatch.Provider>
);
}
export default App;
이후 User 컴포넌트에서 Context 를 조회하였고 Context 의 값을 사용해주었다.
( Context 의 값을 사용하기 위해서는 useContext
라는 hook 이 필요하다. )
import React, {useContext} from 'react';
import { UserDispatch } from '../App';
const User = React.memo(function User({user}){
const dispatch = useContext(UserDispatch);
return(
<div>
<b
style={{
cursor: 'pointer',
color: user.active ? 'green' : 'black'
}}
onClick={()=> {
dispatch({ type: 'TOGGLE_USER', id: user.id });
}}>{user.username}</b> <span>{user.email}</span>
<button onClick={() => {
dispatch({ type: 'REMOVE_USER', id: user.id})
}}>삭제</button>
<button onClick={() => {
dispatch({ type: 'MODIFY_USER', username: user.username, email: user.email, id: user.id})
}}>수정</button>
</div>
)
})
function UserList({users}){
return(
<div>
{users.map(user =>
<User
user={user}
key={user.id}
/>)}
</div>
)
}
export default React.memo(UserList);
추가로 CreateUser 컴포넌트에서도 전역값인 dispatch
를 바로 사용할 수 있도록 구현하였다. props 로 username 과 email 을 전달하는 건 input 이 꼭 App 컴포넌트에 있어야 하기 때문에 어쩔 수 없다.
App 에서 onUpdate
와 onCreate
, onChange
를 지우고 CreateUser 에게 전달하는 props 도 지웠다.
import React, { useRef, useState, useMemo, useCallback, useReducer } from 'react';
import UserList from './Components/UserList';
import CreateUser from './Components/CreateUser';
const countActiveUsers = (users) => {
console.log('활성 사용자 수를 세는중...');
return users.filter(user => user.active).length;
}
const initialState = {
input: {
username: '',
email: '',
id: ''
},
users: [
{
id: 1,
username: 'velopert',
email: 'public.velopert@gmail.com',
active: true
},
{
id: 2,
username: 'tester',
email: 'tester@example.com',
active: true
},
{
id: 3,
username: 'liz',
email: 'liz@example.com',
active: false
}
]
};
function reducer(state, action){
switch (action.type){
case 'CHANGE_INPUT':
return {
...state,
input: {
...state.input,
[action.name]: action.value
}
}
case 'CREATE_USER':
return {
users: state.users.concat(action.user),
//concat() 메서드는 인자로 주어진 배열이나 값들을 기존 배열에 합쳐서 새 배열을 반환한다.
input: {
...state.input,
username: '',
email: ''
}, //input 값 초기화
}
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)
}
case 'MODIFY_USER':
return {
...state,
input: {
username: action.username,
email:action.email,
id: action.id
}
}
case 'UPDATE_USER':
return {
...state,
users: state.users.map(user => user.id === state.input.id ? {...user, username: state.input.username, email: state.input.email} : user),
input: {
...state.input,
username: '',
email: ''
}
}
default:
return state;
}
}
export const UserDispatch = React.createContext(null);
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const nextId = useRef(4);
const { users } = state;
const { username, email } = state.input;
const count = useMemo(() => countActiveUsers(users), [users]);
return (
<UserDispatch.Provider value={dispatch}>
<CreateUser username={username} email={email}/>
<UserList users={users} />
<div>활성사용자 수 : {count}</div>
</UserDispatch.Provider>
);
}
export default App;
이후 CreateUser 컴포넌트에서 Context 를 조회하였고 Context 의 값을 사용해주었다.
before)
import React from "react";
function CreateUser({username, email, onChange, onCreate, onUpdate}){
return(
<div>
<input
name="username"
placeholder="계정명"
onChange={onChange}
value={username}
/>
<input
name="email"
placeholder="이메일"
onChange={onChange}
value={email}
/>
<button onClick={onCreate}>등록</button>
<button onClick={onUpdate}>업데이트</button>
</div>
)
}
export default CreateUser;
after)
import React, { useContext, useRef } from "react";
import { UserDispatch } from "../App";
function CreateUser({username, email}){
const dispatch = useContext(UserDispatch);
const nextId = useRef(4);
return(
<div>
<input
name="username"
placeholder="계정명"
onChange={(e)=>{
const {name, value} = e.target;
dispatch({ type: 'CHANGE_INPUT', name, value})
}}
value={username}
/>
<input
name="email"
placeholder="이메일"
onChange={(e)=>{
const {name, value} = e.target;
dispatch({ type: 'CHANGE_INPUT', name, value})
}}
value={email}
/>
<button onClick={()=>{
dispatch({ type: 'CREATE_USER', user: {
id: nextId.current,
username: username,
email: email}
});
nextId.current += 1;
}}>등록</button>
<button onClick={() => {
dispatch({ type: 'UPDATE_USER' });
}}>업데이트</button>
</div>
)
}
export default React.memo(CreateUser);
useReducer
를 사용할 때 이렇게 dispatch
를 Context API 를 이용하여 전역적으로 사용할 수 있게 해주면, 컴포넌트에게 함수를 전달해야 하는 상황에서 코드의 구조가 훨씬 깔끔해질 수 있다. 예를 들어 깊은 곳에 위치하는 컴포넌트에게 여러 컴포넌트를 거쳐 함수를 전달해야 하는 상황에서 Context API 를 사용하면 좋을 것이다.