[React.js 1팀] 7장. useReducer와 상태 관리
1. useReducer 이해하기
useReducer은 컴포넌트에서 상태 변화 코드를 분리할 때 유용한 리액트 훅이다.
1 - (1). 실습 준비하기
상태 변화 개념을 이해하기 전, 실습을 통해 useReducer의 기능을 알아보자. [할 일 관리] 앱을 열고 component 폴더에 TestComp 컴포넌트를 만든다.
TestComp.js 파일의 내용은 다음과 같다.
import {useState} from "react";
function TestComp() {
const [count, setCount] = useState(0);
const onIncrease = () => {
setCount(count+1);
}
const onDecrease = () => {
setCount(count-1);
}
return (
<div>
<h4>테스트 컴포넌트</h4>
<div>
<bold>{count}</bold>
</div>
<div>
<button onClick={onIncrease}>+</button>
<button onClick={onDecrease}>-</button>
</div>
</div>
)
}
export default TestComp;
TestComp 컴포넌트는 임의의 수를 표시하는 카운트와 이 값을 1씩 증감하는 버튼 2개로 구성한다. 카운트는 State 변수 count를 사용해 관리하는데, + 버튼을 누르면 함수 onIncrease를 호출해 1를 증가시키고, - 버튼을 누르면 onDecrease를 호출해 1 감소시킨다.
이제, 이 TestComp 컴포넌트를 App 컴포넌트의 자식으로 배치해 페이지에 랜더링하자.
(...)
import TestComp from './component/TestComp';
(...)
function App() {
(...)
return (
<div className="App">
<TestComp/>
(...)
</div>
);
}
export default App;
1 - (2). 상태 변화 코드란?
상태 변화 코드란 State 값을 변경하는 코드로, 앞서 만든 TestComp 컴포넌트에서 함수 onIncrease와 onDecrease 모두 count 값을 증감하므로 상태변화 코드라 볼 수 있다.
상태 변화 코드를 컴포넌트에서 분리한다는 말은 컴포넌트 내부에 작성했던 상태 변화 코드를 외부에 작성한다는 뜻이다. 그러나, 지금처럼 useState를 이용해 State를 생성하면 상태 변화 코드는 반드시 컴포넌트 안에 작성해야 하며, 분리할 수 없다. 반면 리액트 훅 useReducer을 사용하면 상태 변화 코드의 분리가 가능하다.
상태 변화 코드의 분리는 코드의 가독성을 높이고 유지 보수를 용이하게 만들기 위해 필요하다.
1 - (3). useReducer의 기본 사용법
useReducer은 useState과 같이 컴포넌트에서 State를 관리하는 리액트 훅이다. 차이점은 useReducer의 경우, State 관리를 컴포넌트 내부가 아닌 외부에서 할 수 있게 만들기 때문에 상태 변화 코드와 컴포넌트의 분리를 가능하게 한다.
이제, useReducer을 이용해 상태 변화 코드를 컴포넌트로부터 분리하자.
다음과 같이 TestComp에서 useState로 만든 기능을 모두 제거한다.
function TestComp() {
return (
<div>
<h4>테스트 컴포넌트</h4>
<div>
<bold>0</bold>
</div>
<div>
<button>+</button>
<button>-</button>
</div>
</div>
)
}
export default TestComp;
그 후 useState를 이용해 만들었던 카운트 기능을 useReducer을 이용해 구현하겠다. 먼저 TestComp 컴포넌트를 다음과 같이 수정한다.
import { useReducer } from "react"; // 1
function reducer() {} // 2
function TestComp() {
const [count, dispatch] = useReducer(reducer, 0); // 3
(...)
}
export default TestComp;
- useReducer을 사용하기 위해 react 라이브러리에서 불러옴.
- 새로운 함수 reducer을 컴포넌트 밖에 생성.
- useReducer을 호출하고 2개의 인수를 전달. 첫번째 인수 = 함수 reducer, 두번째 인수 = State의 초깃값.
useReducer도 useState처럼 배열을 변환하는데, 배열의 첫 번째 요소(해당 코드에서는 count)는 State 변수이고 두 번째 요소는 상태 변화를 촉발하는 함수 dispatch이다.
그 다음, 현재의 State 값을 담은 count를 페이지에 랜더링한다.
import { useReducer } from "react";
function reducer() {}
function TestComp() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<h4>테스트 컴포넌트</h4>
<div>
<bold>{count}</bold>
</div>
<div>
<button>+</button>
<button>-</button>
</div>
</div>
)
}
export default TestComp;
이제 버튼을 클릭하면 카운트를 증감하는 기능을 만들어야 한다. 다음과 같이 버튼을 클릭했을 때 함수 dispatch를 호출하도록 onClick 이벤트 핸들러를 설정한다.
import { useReducer } from "react";
function reducer() {}
function TestComp() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<h4>테스트 컴포넌트</h4>
<div>
<bold>{count}</bold>
</div>
<div>
<button onClick={() => dispatch({type: "INCREASE", data: 1})}>+</button> //1
<button onClick={() => dispatch({type: "DECREASE", data: 1})}>-</button> //2
</div>
</div>
)
}
export default TestComp;
1, 2 모두 버튼을 클릭하면 상태 변화를 촉발하는 함수 dispatch를 호출하고 인수로 객체를 전달한다. 이 객체에는 State의 변경 정보를 담고 있다. 이를 다른 표현으로 'action 객체'라고도 한다.
객체의 type 프로퍼티는 어떤 상황이 발생했는지를 나타내고, data 프로퍼티는 상태 변화에 필요한 값을 의미한다. + 버튼의 경우, type에는 증가를 의미하는 INCREASE를, 증가값은 1이므로 data에는 1을 넣은 것이다.
그러나, 아직은 버튼을 클릭해도 카운트 값의 변화가 없다. 그 이유는 실제 상태 변화는 함수 reducer에서 일어나기 때문이다. dispatch를 호출하면 함수 reducer이 실행되고, 이 함수가 반환하는 값이 새로운 State 값이 되는 것이다.
따라서 다음과 같이 TestComp에서 함수 reducer을 작성한다.
import { useReducer } from "react";
function reducer(state, action) { // 1
switch (action.type) {
case "INCREASE": // 2
return state + action.data;
case "DECREASE": // 3
return state - action.data;
default: // 4
return state;
}
}
function TestComp() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<h4>테스트 컴포넌트</h4>
<div>
<bold>{count}</bold>
</div>
<div>
<button onClick={() => dispatch({type: "INCREASE", data: 1})}>+</button>
<button onClick={() => dispatch({type: "DECREASE", data: 1})}>-</button>
</div>
</div>
)
}
export default TestComp;
- 함수 reducer에는 2개의 매개변수가 있음. 첫번째 state에는 현재 State 값이, 두번째 action에는 함수 dispatch를 호출하면서 인수로 전달한 action 객체가 저장됨.
- 함수 reducer가 반환하는 값 = 새로운 State 값. (기존 State 값 + data) 반환.
- (기존 State 값 - data) 반환.
- action 객체의 type이 INCREASE, DECREASE도 아닌 경우 state 값을 그대로 반환하므로 아무런 상태 변화가 일어나지 않음.
이제 + 버튼, - 버튼을 누르면 카운트가 변화하는 것을 확인할 수 있다.
useReducer은 함수 reducer을 이용해 상태 변화 코드를 컴포넌트 외부로 분리하므로, 새로운 상태 변화가 필요할 경우 함수 reducer를 수정해 대응하면 된다. 다음은 TestComp에 초기화 버튼을 추가한 코드이다.
import { useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
(...)
case "INIT": // 1
return 0;
default:
return state;
}
}
function TestComp() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<h4>테스트 컴포넌트</h4>
<div>
<bold>{count}</bold>
</div>
<div>
(...)
<button onClick={() => dispatch({type: "INIT"})}>초기화</button> // 2
</div>
</div>
)
}
export default TestComp;
- 함수 reducer에서 카운트 값을 초기화하는 새로운 case를 추가.
- 카운트 초기화 버튼 생성.
지금까지 useReducer을 이용한 State 관리 방법을 알아보았다. TestComp는 이후 실습에선 사용하지 않으니 App 컴포넌트에 작성한 관련 코드는 주석 처리하자.
2. [할 일 관리] 앱 업그레이드
함수 useReducer로 상태 변화 코드를 컴포넌트로부터 분리해 앱을 업그레이드 하자.
2 - (1). useState를 useReducer로 바꾸기
[할 일 관리] 앱에서 useState를 이용해 관리했던 App 컴포넌트의 State를 useReducer로 변경하자.
App.js 파일을 다음과 같이 수정한다.
import { useReducer, useRef } from "react"; // 1
(...)
function reducer(state, action) { // 2
// 상태 변화 코드
return state;
}
function App() {
const [todo, dispatch] = useReducer(reducer, mockTodo); // 3
(...)
}
export default App;
- useReducer을 react 라이브러리에서 불러오고, 기존의 useState 코드는 모두 삭제함.
- 함수 reducer는 매개변수로 저장한 state를 지금은 그대로 반환하도록 작성. 추후 수정 예정.
- 기존의 useState를 삭제하고 useReducer로 대체. 인수로 함수 reducer와 mockTodo를 초깃값으로 전달.
여기까지 진행했다면, setTodo가 사라져 오류가 발생할 것이다. 앞으로 상태 변화가 있을 때 setTodo 대신 dispatch를 호출할 것이다. 따라서 App 컴포넌트에서 함수 setTodo를 호출하는 코드를 모두 제거하자.
(...)
function reducer(state, action) {
return state;
}
function App() {
const [todo, dispatch] = useReducer(reducer, mockTodo);
const idRef = useRef(3);
const onCreate = (content) => {
idRef.current += 1;
};
const onUpdate = (targetId) => {
};
const onDelete = (targetId) => {
};
(...)
}
export default App;
onCreate, onUpdate, onDelete에 작성했던 함수 setTodo를 모두 삭제한다.
2- (2). Create: 할 일 아이템 추가하기
onCreate에서 dispatch를 호출하고, 인수로 할 일 정보를 담은 action 객체를 전달한다.
App 컴포넌트를 다음과 같이 수정한다.
(...)
function App() {
(...)
const onCreate = (content) => {
dispatch({ // 1
type: "CREATE", // 2
newItem: { // 3
id: idRef.current,
content,
isDone: false,
createDate: new Date().getTime(),
},
});
idRef.current += 1;
};
(...)
}
export default App;
- 새 할 일 아이템 생성을 위해 함수 dispatch 호출.
- 할 일을 추가할 것이므로 type = "CREATE".
- newItem에 추가할 데이터 설정.
(...)
function reducer(state, action) {
switch (action.type) {
case "CREATE": {
return [action.newItem, ...state]; // 1
}
default:
return state;
}
}
(...)
함수 reducer 코드도 위와 같이 변경하자. 호출 시에 기존의 배열에 새로운 아이템을 추가한다.
2 - (3). Update: 할 일 아이템 수정하기
(...)
const onUpdate = (targetId) => {
dispatch({
type: "UPDATE",
targetId,
})
};
(...)
type에는 수정을 의미하는 UPDATE를, targetId에는 체크 여부로 수정할 아이템의 id 설정한다.
function reducer(state, action) {
switch (action.type) {
case "CREATE": {
return [action.newItem, ...state];
}
case "UPDATE": {
return state.map((it) =>
it.id === action.targetId
? {
...it,
isDone: !it.isDone,
}
: it
);
}
default:
return state;
}
return state;
}
map 서드로 순회하며 매개변수 state에 저장된 아이템 배열에서 action.targetID와 id를 비교해 일치하는 아이템의 isDone을 토글한 새 배열 반환.
2 - (4). Delete: 할 일 삭제 구현하기
(...)
const onDelete = (targetId) => {
dispatch({
type: "DELETE",
targetId,
})
};
(...)
할 일을 삭제하는 함수 onDelete를 위와 같이 수정한다.
그 후 다음과 같이 함수 reducer에 case를 추가해준다.
(...)
case "DELETE": {
return state.filter((it) => it.id !== action.targetId);
}
(...)
action.type이 DELETE일 때, filter 서드로 id와 targetId를 비교하여 일치하는 할 일 아이템만 제외한 배열을 생성하여 반환한다.
이로써 기존의 useState를 사용해 구현한 [할 일 관리] 앱을 useReducer을 사용해 구현하였다. 실제 실무에서 State를 관리할 때에 간단한 경우에는 useState를, 복잡한 경우에는 useReducer을 사용한다고 하니, 리액트 훅을 적절히 선택하여 사용해 보자.
출처 : 이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023).
Corner React.js 1
Editor: Mingging