이전에는 useState를 사용해서 상태를 관리하고 업데이트 해줬는데, 사실 한가지 방법이 더있다. 바로 useReducer를 사용하는 것이다. useReducer는 처음 사용해보면 사용법이 복잡하다고 느낄 수 있는데 크게 알아야 할 부분은 3가지로 action, dispatch, reducer이다.
useState가 setValue를 통해 상태를 직접 지정해 변경해줬다면 useReducer는 액션 객체를 기반으로 상태를 업데이트한다. 여기서 액션 객체란 업데이트할 때 참조하는 객체를 말한다. 액션 객체를 분리해서 작성할 수도 있고 dispatch 안에 직접 작성할 수도 있다.
dispatch 안에 액션객체를 넣으면 상태변화를 발생시킨다. 다음과 같이 type을 이용해 어떤 업데이트를 진행할 건지 명시하고 업데이트할 때 diff 처럼 참조가 필요한 다른 값이 있다면 이 객체 안에 넣을 수도 있다.
// dispatch안에 액션객체를 넣는다!!
dispatch({ type: 'INCREMENT', diff: 4 })
하지만 어떤 액션객체인지를 판별하고 실제 업데이트해주는 부분은 아직 작성하지 않았다. 여기서 reducer라는 개념이 있는데, reducer
란 상태를 업데이트하는 함수를 말한다.
// dispatch가 실행되면 reducer를 통해 상태를 업데이트해준다!!
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
defulat:
return state;
}
}
reducer는 현재 상태와 액션객체를 파라미터로 받아와 새로운 상태를 반환해주는 형태를 갖추고 있어야 한다. 보면 dispatch로 액션을 발생시키고 reducer에서 어떤 액션타입인지 switch문을 통해 판별한 다음 액션에 맞게 INCREMENT면 1증가, DECREMENT면 1감소를 해주는 작업을 하고 있다.
reducer를 작성한 다음 최종적으로 useReducer
를 사용해서 첫번째로 reducer 함수를, 두번째로 초기 상태값을 넣어주면 number라는 현재 상태값과 dispatch를 반환해주게 된다.
const [number, dispatch] = useReducer(reducer, 0);
그렇다면 useState를 쓸까 useReducer를 쓸까? 🤔
이건 정해진 답이 없다. 다만 권장사항은 컴포넌트가 관리하는 값이 많지 않거나 복잡하지 않다면 useState를 쓰는 편이 좋고, 반대로 관리하는 값이 많거나 복잡한 경우에는 useReducer를 사용하는게 좋다고 한다.
useReducer를 사용하면 상태 업데이트로직을 컴포넌트 밖으로 분리가 가능하고 심지어 다른 파일에서 작성도 가능하기 때문이다.
Counter는 구조가 간단하고 실습하기 좋은 예제이므로 먼저 Counter 예제를 활용해서 useReducer에 대해 실습해보도록 하겠다.
import React, { useReducer } from "react";
// 첫번째. 리듀서 작성!!
function reducer(state, action) {
switch (action.type) {
case "INCREMENT":
return state + 1;
case "DECREMENT":
return state - 1;
default:
throw new Error("Unhandled action");
}
}
function Counter() {
// 두번째. useReducer를 사용해 reducer와 초기값을 넘겨준다음 상태값과 dispatch를 넘겨받는다.
const [number, dispatch] = useReducer(reducer, 0);
// 세번째. dispatch를 이용해 액션객체를 생성해서 넘겨준다.
const onIncrease = () => {
dispatch({ type: "INCREMENT" });
};
const onDecrease = () => {
dispatch({ type: "DECREMENT" });
};
return (
<>
<h1>{number}</h1>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</>
);
}
export default Counter;
생각해보니 간단하지? 😲
마지막으로 저번시간에 배열 렌더링과 성능 최적화까지 마친 코드를 가지고 useReducer로 바꿔주는 작업을 해보겠다. 기존에 App.js에서 작성된 코드는 아깝겠지만 날려주고 새롭게 작성해보도록 하겠다.
useState를 사용할 때는 inputs 따로, users 따로 관리해줬는데 이번에는 통합해서 관리해보도록 하겠다.
// 초기상태를 갖는 객체를 만든다.
const initialState = {
inputs: {
username: "",
email: "",
},
users: [
{
id: 1,
username: "홍길동",
email: "홍길동@gmail.com",
active: true,
},
{
id: 2,
username: "이순신",
email: "이순신@example.com",
active: false,
},
{
id: 3,
username: "강감찬",
email: "강감찬@example.com",
active: false,
},
],
};
리듀서 함수 뼈대 작성과 useReducer를 사용해 리듀서 함수와 초기값을 넣어주고 현재 상태와 dispatch를 반환 받는다. 그리고 비구조할당을 통해 users와 username, email를 분리해준다.
// 리듀서 함수 뼈대 작성
function reducer(state, action) {
return state;
}
function App() {
// useReducer를 사용해 리듀서함수와 초기값을 넣고 현재상태와 dispatch를 반환받음
const [state, dispatch] = useReducer(reducer, initialState);
const nextId = useRef(4);
const { users } = state;
const { username, email } = state.inputs;
(...생략...)
}
먼저 input 상태 변환을 해주는 onChange
함수부터 작성해보겠다.
// CHANGE_INPUT 액션타입에 해당하는 동작을 작성해서 새로운 상태를 반환해준다.
function reducer(state, action) {
switch (action.type) {
case "CHANGE_INPUT":
return {
...state,
inputs: {
...state.inputs,
[action.name]: action.value,
},
};
default:
throw new Error("Unhandled action");
}
}
// input 값이 바뀌게 될 때 실행
const onChange = useCallback((e) => {
const { name, value } = e.target;
// 디스패치 발생시킴(타입과 참조값을 넣어줌)
dispatch({
type: "CHANGE_INPUT",
name,
value,
});
}, []);
마찬가지로 유저 생성을 하는 onCreate, 유저 삭제를 하는 onRemove, 유저 수정을 하는 onToggle도 작성해주면 된다.
///////////////////////////////////////
// reducer 안에 작성
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),
};
///////////////////////////////////////
// App함수 안에 작성
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,
});
}, []);