그전 시간 까지 CRUD 구현한 것의 최적화에 관련된 훅들을 배웠다.
이번에는 UseState 를 대신하여 사용할 수 있는 useReducer 라는 훅과, 커스텀 훅, 그리고 전역 상태관리가 가능한 context API 까지 배워본다.
useState 는 state 를 관리할 때 정말 좋은 훅이다.
상태관리할 state가 적으면 정말 좋지만... 많아지면 조금 불편해진다
또한 상태 업데이트를 무조건 컴포넌트 안에서만 해야하는 단점이 있다.
이를 해결하는 다른 state 훅이 존재한다.
상태 업데이트 로직을 컴포넌트로부터 분리할 수 있게 되고, state 들을 집합해서 관리 할 수 있다.
reducer 는 현재 상태와 액션 객체를 파라미터로 받아와서 새로운 상태를 반환해주는 함수이다.
현재 상태에 어떠한 액션을 통해 다른 상태로 바꿔주는 느낌이다.
function reducer(state, action) {
// 새로운 상태를 만드는 로직
// const nextState = ...
return nextState;
}
리턴 된 상태는 컴포넌트가 지닐 새로운 상태가 된다.
action 은 이러한 state 를 업데이트하기 위한 정보들을 가지는데, 주로 type 프로퍼티를 가지고있고, 상태 업데이트에 관련된 다른 프로퍼티 값들도 가지고 있다.
// 카운터에 1을 더하는 액션
{
type: 'INCREMENT'
}
이러한 타입과
// input 값을 바꾸는 액션
{
type: 'CHANGE_INPUT',
key: 'email',
value: 'tester@react.com'
}
타입 + 필요 프로퍼티 값들을 지닌다. type 은 주로 대문자와 _ 로 구성한다.
const [state , dispatch] = useReducer(reducer, initialState);
state : 우리가 컴포넌트에서 사용할 상태(전체)
dispatch : reducer의 액션을 발생시키는 함수 (dispatch(action) 으로 사용)
reducer : 아까처럼 액션에 의해 state를 변형해 리턴하는 함수
initialState : 컴포넌트 상태의 default
이렇게 된다.
먼저 컴포넌트에 reducer 를 사용해보자
function Counter() {
const [number, dispatch] = useReducer(reducer, 0);
const onIncrease = () => {
dispatch({ type: 'INCREMENT' });
};
const onDecrease = () => {
dispatch({ type: 'DECREMENT' });
};
return (
<div>
<h1>{number}</h1>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
);
}
쓰이는 state는 현재 수인 number 가 끝이니 number 로 다이렉트로 정의
initialState 는 0으로 설정해서
const [number, dispatch] = useReducer(reducer, 0)
로 해주고
onIncrease, onDecrease 함수를 정의해줄 때 reducer 의 적용될 action을 dispatch 에 넣어준다.
따로 다른 컴포넌트가 필요하지 않으니, type 만 전달하여 어떤 case 인지 전달해준다.
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
그리고 컴포넌트 외부에 reducer 함수를 만든다.
reducer 함수는 action.type 에 따른 switch 문으로 구성된다.
dispatch 함수로 reducer 에 전달된 type에 따라 state를 1씩 증감하고 리턴한다.
이러면 간단히 counter 컴포넌트를 구성할 수 있다!

막막할 수 있다. 하지만 state를 모아서 case by case로 관리하니 매우 가독성이 좋아진다.
import React, { useRef, useState, useMemo, useCallback } 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 App() {
return (
<>
<CreateUser />
<UserList users={[]} />
<div>활성사용자 수 : 0</div>
</>
);
}
export default App;
initialState 로 컴포넌트에 쓰이는 상태를 한곳에 넣는다. 쓰이는 state가 원래 users와 inputs 였기에 형태 그대로 프로퍼티에 추가해준다.
import React, { useRef, useReducer, useMemo, useCallback } 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) {
return state;
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const { users } = state;
const { username, email } = state.inputs;
return (
<>
<CreateUser username={username} email={email} />
<UserList users={users} />
<div>활성사용자 수 : 0</div>
</>
);
}
export default App;
또 state.뭐시기 로 접근하면 코드가 길어지니 비구조화 할당으로 state 내부 프로퍼티들을 추출해서 컴포넌트에 props 로 넣어준다.
그리고 이제 차례차례 onChange, onCreate, onToggle을 만들어줄 차례다.
import React, { useRef, useReducer, useMemo, useCallback } 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 'CHANGE_INPUT':
return {
...state,
inputs: {
...state.inputs,
[action.name]: action.value
}
};
default:
return state;
}
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const { users } = state;
const { username, email } = state.inputs;
const onChange = useCallback(e => {
const { name, value } = e.target;
dispatch({
type: 'CHANGE_INPUT',
name,
value
});
}, []);
return (
<>
<CreateUser username={username} email={email} onChange={onChange} />
<UserList users={users} />
<div>활성사용자 수 : 0</div>
</>
);
}
export default App;
먼저 type 을 CHANGE_INPUT 으로 정의한다.
그리고 onChange 함수를 정의해야한다.
const { name, value } = e.target;
dispatch({
type: 'CHANGE_INPUT',
name,
value
});
}
그전과 같이 타켓이 변해서 받은 e.target 의 name 과 value 를 비구조화 할당으로 받고
dispatch를 해줄 때 action 에 타입과 필요한 변수인 name, value를 전달해준다.
function reducer(state, action) {
switch (action.type) {
case 'CHANGE_INPUT':
return {
...state,
inputs: {
...state.inputs,
[action.name]: action.value
}
};
default:
return state;
}
}
그 이후 reducer 에 인풋이 type 으로 바뀔 때 리턴할 것들을 정의해준다.
...state,
inputs: {
...state.inputs,
[action.name]: action.value
}
불변성 지키면서 state 를 복사해서, 그안의 input의 정보도 복사하고 action.name 으로 받은 username 이나 email 프로퍼티의 값을 action.value 로 바꿔준다.
<CreateUser username={username} email={email} onChange={onChange} />
그리고 CreateUser에 props 를 비구조화 할당으로 전달해준다.
import React, { useRef, useReducer, useMemo, useCallback } 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 'CHANGE_INPUT':
return {
...state,
inputs: {
...state.inputs,
[action.name]: action.value
}
};
case 'CREATE_USER':
return {
inputs: initialState.inputs,
users: state.users.concat(action.user)
};
default:
return state;
}
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const nextId = useRef(4);
const { users } = state;
const { username, email } = state.inputs;
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,
email
}
});
nextId.current += 1;
}, [username, email]);
return (
<>
<CreateUser
username={username}
email={email}
onChange={onChange}
onCreate={onCreate}
/>
<UserList users={users} />
<div>활성사용자 수 : 0</div>
</>
);
}
export default App;
먼저 action 에서 type 을 "CREATE_USER" 로 정의한다.
그리고 onCreate 함수를 만들어보자.
const onCreate = useCallback(() => {
dispatch({
type: 'CREATE_USER',
user: {
id: nextId.current,
username,
email
}
});
nextId.current += 1;
}, [username, email]);
현재 가지고 있는 usernmae과 email 을 그대로 사용하면 된다.
dispatch 안의 action은 CREATE_USER 라는 타입과 함께, 생성할 user 를 id, username, email 을 가지게 객체로 만든다음 action 에 담아 전송한다.
그리고 id를 증가시켜준다.
다음은 reducer이다.
case 'CREATE_USER':
return(
{
inputs : initialState.inputs,
users : state.users.concat(action,.user)
};
)
생성하면 input은 초기화 시켜야 하므로, 초기화상태의 inputs 를 initialState 에서 받아와서 할당해주고, users 는 불변성 유지를 위해 concat 함수를 써서 action 을 통해 받아온 user 를 users에 추가해준다.
<CreateUser
username={username}
email={email}
onChange={onChange}
onCreate={onCreate}
/>
그리고 props 에 onCreate 를 넣어준다.
import React, { useRef, useReducer, useMemo, useCallback } 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 'CHANGE_INPUT':
return {
...state,
inputs: {
...state.inputs,
[action.name]: action.value
}
};
case 'CREATE_USER':
return {
inputs: initialState.inputs,
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 [state, dispatch] = useReducer(reducer, initialState);
const nextId = useRef(4);
const { users } = state;
const { username, email } = state.inputs;
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,
email
}
});
nextId.current += 1;
}, [username, email]);
const onToggle = useCallback(id => {
dispatch({
type: 'TOGGLE_USER',
id
});
}, []);
const onRemove = useCallback(id => {
dispatch({
type: 'REMOVE_USER',
id
});
}, []);
return (
<>
<CreateUser
username={username}
email={email}
onChange={onChange}
onCreate={onCreate}
/>
<UserList users={users} onToggle={onToggle} onRemove={onRemove} />
<div>활성사용자 수 : 0</div>
</>
);
}
export default App;
이전과 같이 onToggle과 onRemove 의 타입을 각각 TOGGLE_USER, REMOVE_USER 로 정의해준다.
그리고 onToggle onRemove 함수를 정의해준다.
const onToggle = useCallback(id => {
dispatch({
type: 'TOGGLE_USER',
id
});
}, []);
onToggle 함수는 현재 내가 토글 한 user 의 id 가 필요하기에 type과 함께 id를 action 에 넣어준다.
const onRemove = useCallback(id => {
dispatch({
type: 'REMOVE_USER',
id
});
}, []);
onRemove 도 동일하다.
그 이후 reducer 를 작업해준다.
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)
)
이러면 userReducer 만으로 컴포넌트 상태관리를 할 수 있다. 굿!!!!!!!!!!!!!!!!!!!