useReducer?
useState와 마찬가지로 state를 생성 및 상태를 관리하는 hook이다.
state 구조가 복잡할 때 또는 같은 코드가 반복될 때 useState에 비해 코드를 간결하게 짤 수 있다.
컴포넌트에서 상태변화 로직을 분리할 수 있어서 컴포넌트를 가볍게 작성할 수 있는 것이다.
상태 변화 함수를 컴포넌트 밖으로 분리해서 switch case문처럼 쉽게 바꿀 수 있다.
우선 예제를 통해 얼마나 깔끔해지는지 대충 살펴보자. 코드가 어떻게 바뀌는 건지 지금은 알 필요 없다. 그냥 코드가 얼마나 깔끔해지는지 한 눈에 확인해보자.
[useReducer을 사용하지 않았을 때]
const Counter=()=>{
const [count, setCount]=useState(1);
const add1=()=>{
setCount(count+1);
}
const add10=()=>{
setCount(count+10);
}
const add100=()=>{
setCount(count+100);
}
const add1000=()=>{
setCount(count+1000);
}
}
return(
<div>
{count}
<button onClick={add1}>add 1</button>
<button onClick={add10}>add 10</button>
<button onClick={add100}>add 100</button>
<button onClick={add1000}>add 1000</button>
</div>
)
[useReducer을 사용했을 때]
const reducer=(state, action)=>{
switch(action.type){
case 1:
return state+1;
case 10:
return state+10;
case 100:
return state+100;
case 1000:
return state+1000;
default:
return state;
}
}
const Counter=()=>{
const [count, dispatch] = useReducer(reducer, 1);
return(
<div>
{count}
<button onClick={()=>dispatch({type:1})}>add 1</button>
<button onClick={()=>dispatch({type:10})}>add 10</button>
<button onClick={()=>dispatch({type:100})}>add 100</button>
<button onClick={()=>dispatch({type:1000})}>add 1000</button>
</div>
)
}
동작 방식
이제 위의 코드로 useReducer 훅의 동작 방식에 대해 알아보자.
reducer, dispatch, action 이 세가지만 알면 끝이다.
dispatch : 요구하는 애
reducer : state 상태를 바꿔주는 애
action : 요구사항
겨울이니까 붕어빵을 예시로 들어보겠다. 분식 트럭에 가서 "붕어빵 하나 주세요"(action) 라고 요구(dispatch)를 했다. 그럼 사장님(reducer)이 나에게 붕어빵을 줄 것이고, 붕어빵(state)이 하나 줄어들 것이다. 다시 정리하자면, 내가 붕어빵이라는 상태를 변경하기 위해서는 요구라는 dispatch에 붕어빵을 달라는 action을 담아 사장님이라는 reducer에게 전달해야한다. action은 "붕어빵 주세요" "떡볶이 주세요" "어묵 주세요" 등 다른 요구로 바뀔 수 있다. 그러니까 action마다 다 다르게 state 값을 변경할 수 있다.
위에서 설명한 걸 코드로 구체화해보자.
dispatch에 action을 담는다
=> dispatch(action)
reducer은 dipatch가 보낸 action을 보고 state를 변경한다
=> dispatch(action) → reducer(state, action)
어느정도 감이 잡혔다면 이제 문법을 살펴보자.
기본 문법은 다음과 같다.
const [count, dispatch] = useReducer(reducer, 1);
- count라는 state초기값은 1이다.
- count의 값을 변경하고 싶으면 상태변화 함수인 dispatch를 호출한다.
- dispatch가 호출되면 상태변화 처리 함수인 reducer가 값 변경을 처리한다.
<button onClick={()=>dispatch({type:1})}>add 1</button>
- dispatch함수는 호출될 때 객체를 전달한다. 이 객체에는 type이라는 프로퍼티가 들어있는데, 이렇게 dispatch와 함께 전달되는 객체를 action 객체라고 부른다.
- action은 상태변화를 의미한다. 즉, 상태변화를 설명할 객체라는 뜻이다.
const reducer=(state, action)=>{
switch(action.type){
case 1:
return state+1;
case 10:
return state+10;
case 100:
return state+100;
case 1000:
return state+1000;
default:
return state;
}
}
첫번째 인자로는 state, 두번째 인자로는 dispatch를 호출할 때 전달됐던 action 객체가 들어간다.
add 1버튼을 누르면 reducer 함수가 실행되는데 이 때 state는 1이고 action 객체는 type:1이다.
switch문을 사용해서 action의 타입에 따라 다른 값을 반환해줌. 반환 과정에서 state 값이 변경됨.
이번에는 조금 더 긴 다른 코드를 짜봤다.
dispatch가 호출되면 reducer이 호출된다는데, 직접 확인해보자. 붕어빵 장사를 코드로 옮겨봤다. 팔기 or 만들기 버튼을 클릭했을 때 dispath 함수가 호출되게 만들었다. 버튼을 누를 때 마다 콘솔창에 "호출됨" 이 찍히는 걸 알 수 있다.
import "./styles.css";
import React, { useState, useReducer } from "react";
const reducer = (state, action) => {
console.log("호출됨");
};
export default function App() {
const [number, setNumber] = useState(0);
const [result, dispatch] = useReducer(reducer, 0);
return (
<div className="App">
<h2>분식트럭입니다.</h2>
<p>붕어빵 개수 : {result} </p>
<input
type="number"
value={number}
onChange={(e) => setNumber(parseInt(e.target.value))}
step="1000"
/>
<button
onClick={() => {
dispatch();
}}
>
팔기
</button>
<button onClick={() => dispatch()}>만들기</button>
</div>
);
}
이번에는 dispatch에 type, data를 추가해서 state, action을 콘솔에 찍어봤다.
import "./styles.css";
import React, { useState, useReducer } from "react";
const reducer = (state, action) => {
console.log(state, action);
switch (action.type) {
case "SELL":
return state + action.payload;
case "MAKE":
return state - action.payload;
default:
return state;
}
};
export default function App() {
const [number, setNumber] = useState(0);
const [result, dispatch] = useReducer(reducer, 0);
return (
<div className="App">
<h2>분식트럭입니다.</h2>
<p>남은 돈 : {result} </p>
<input
type="number"
value={number}
onChange={(e) => setNumber(parseInt(e.target.value))}
step="1000"
/>
<button
onClick={() => {
dispatch({ type: "SELL", payload: number });
}}
>
팔기
</button>
<button onClick={() => dispatch({ type: "MAKE", payload: number })}>
만들기
</button>
</div>
);
}
어떤 부분에서 useState보다 간결하다는지 모르겠는데요?
위에서는 동작 원리를 보여주기 위해 간단한 코드만 짰다. 그렇기 때문에 한줄만 쓰면 되는 useState가 더 좋아보일 수 있다. state 변경이 많아지는 코드일수록 useReducer이 얼마나 유용한지 눈에 보인다. 이제 실제로 useState 코드를 useReducer로 바꾸고 비교해보자.
이게 무슨 동작을 하는 코드인지는 자세히 이해할 필요 없다. 주석처리된 setState와 dispatch, reducer 함수를 비교하며 어떤 방식으로 쓰이는 건지 파악하면 된다. dispatch에서는 reducer 안에서 쓰일 타입과 데이터를 인자로 받고, reducer에서 기존에 setState에서 했던 동작을 하고 동작 결과를 return 해주고 있다.
const reducer = (state, action) => {
switch (action.type) {
case 'INIT': {
return action.data;
}
case 'CREATE': {
const created_date = new Date().getTime();
const newItem = {
...action.data,
created_date,
};
return [newItem, ...state];
}
case 'REMOVE': {
return state.filter((it) => it.id !== action.targetId);
}
case 'EDIT': {
return state.map((it) => (it.id === action.targetId ? { ...it, content: action.newContent } : it));
}
default:
return state;
}
};
function App() {
// const [data, setData] = useState([]);
const [data, dispatch] = useReducer(reducer, []);
const dataId = useRef(0);
const getData = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/comments').then((res) => res.json());
const initData = res.slice(0, 20).map((it) => {
return {
author: it.email,
};
});
dispatch({ type: 'INIT', data: initData });
// setData(initData);
};
// 마운트 시점에서 getData가 호출됨
useEffect(() => {
getData();
}, []);
const onCreate = useCallback((author, content, emotion) => {
// const newItem = {
// author
//};
dispatch({
type: 'CREATE',
data: { author },
});
// setData((data) => [newItem, ...data]);
}, []);
const onRemove = useCallback((targetId) => {
dispatch({ type: 'REMOVE', targetId });
// setData((data) => data.filter((it) => it.id !== targetId));
}, []);
const onEdit = useCallback((targetId, newContent) => {
dispatch({ type: 'EDIT', targetId, newContent });
// setData(data.map((it) => (it.id === targetId ? { ...it, content: newContent } : it)));
}, []);
return (
<div className="App">
<DiaryEditor onCreate={onCreate} />
<DiaryList onEdit={onEdit} onRemove={onRemove} diaryList={data} />
</div>
);
}
export default App;
'나도 공부한다 > React' 카테고리의 다른 글
React "too many re-render" 문제 해결 (0) | 2024.02.01 |
---|---|
React의 Context API 알아보기 (0) | 2024.01.12 |
리액트 프로젝트 최적화하기 - 3. useCallback (0) | 2024.01.04 |
firebase로 프로젝트 쉽게 배포하기 (1) | 2024.01.02 |
useMemo와 useEffect의 차이를 모르겠어서 공부한 기록 ( + useLayoutEffect) (1) | 2024.01.02 |