✏️ useState를 사용하는 경우
✏️ useReducer를 사용하는 경우
State을 변경하고 관리하는 것은 useSate으로도 가능한데 useReducer를 사용하는 이유는 무엇일까?
✏️ useState의 경우, 관리해야할 State가 적고 단순한 숫자,문자열, boolean값일 경우 사용하는 것이 좋다.
✏️ useReducer의 경우, 관리해야할 State가 많고 구조가 복잡한 규모가 다소 큰 프로젝트에서 사용하는 것이 좋고 유지보수가 좋다.
useReducer 구조
const [state, dispatch] = useReducer(reducer, initialState, init);
useReducer는 총 4가지로 구성된다.
첫번째 인자인 reducer가 return하는 값으로 state(상태)를 갱신하는 역할
설명) reducer가 반환한 새로운 값을 number라는 state에 갱신한다. 그 이전까지의 number의 값은 0으로 초기값을 설정한다.
const [number, dispatch] = useReducer(reducer, { count: 0 });
업데이트해야할 정보를 가지고 있는 것으로, dispatch의 인자에 담긴다.
쉽게 말해, reducer가 무엇을 해야할지 담겨있는 명령어 이다.
설명) action은 대부분 type이라는 값에 객체 형태로 담긴다. decrement,increment라는 명령어가 dispatch의 인자로 담겨있는 것이다.
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
reducer함수를 실행시킨다. dispatch의 인자로 action을 담아서 업데이트를 일으키기 위해서 사용된다.
쉽게 말해 ruducer에게 action이라는 명령어를 주는 명령자 역할이다.
설명) 버튼 클릭시 dispatch함수가 실행되어 action을 인자에 담고 reducer에 업데이트를 요청한다.
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
dispatch에 의해 실행되는 함수로, 외부에서 state을 업데이터히는 로직을 담당하는 함수이다. action의 값에 따라서 기존의 state값을 새롭게 return한다.
쉽게 말해, dispatch의 action 명령에 따라서 state값을 변경하는 역할을 한다.
설명) reducer함수 내부의 switch문에서는 action의 값에 따라 발생할 case가 결정된다. 결정된 case안에서 기존의 state값이 새로운 값으로 변경되어 return되어지는 과정이다.
function reducer(state, action) {
switch (action.type) {
case "decrement":
// action의 type이 "decrement"일 때, 현재 state에서 1을 뺀 값을 반환함
return state - 1;
case "increment":
// action의 type이 "increment"일 때, 현재 state에서 1을 더한 값을 반환함
return state + 1;
default:
// 정의되지 않은 action type이 넘어왔을 때는 에러를 발생시킴
throw new Error("Unsupported action type:", action.type);
}
}
예시 출처: React Hooks :: useReducer에 대해 알아보기
💡중요) reducer는 새로운 state값을 만든다.
reducer는 기존의 state를 수정(modify)하거나, 추가(add)하거나, 덮어쓰지(overwrite) 하지 않고, 기존의 state를 새로운 state로 (대체)replace한다.
쉽게 말해,기존의 state를 바로 수정하지 않고 복사한 뒤에 복사한 값을 수정하여 새로운 값으로 변경하면 된다.
⭐️ 복사를 할때는 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)가 있다.
✏️ 얕은 복사는 참조(주소)값의 복사를 의미한다.
const obj = { value: 10 } const newObj = obj newObj.value = 5; console.log(obj.value); // 5 console.log(newObj.value); // 5
위와 같이 obj를 newObj에 참조할당할 경우, newObj의 값을 변경하면 obj 또한 같은 주소값을 공유하기 때문에 데이터가 함께 변경된다.
이처럼 reducer에서 만약 얕은 복사로 기존의 state값을 변경하면 값이 변경되어서 기존의 state값을 사용하던 다른 컴포넌트에서 에러가 발생할 수 있게 된다.따라서 reducer에서는 깊은 복사를 꼭 사용하도록 하자!
✏️ 깊은 복사는 값 자체를 복사하여 다른 주소값을 지닌다.
JS의 원시타입은 깊은 복사가 가능하며, 이는 독립적인 메모리에 값 자체를 새롭게 할당하여 생성하는 것이다. 많이 사용되는 깊은 복사는 Object.assign()와 spread연산자가 있다.
const obj = { value: 10 } const newObj = { ...obj } newObj.value = 5 console.log(obj.value); // 10 console.log(newObj.value); // 5
reducer함수는 순수 함수로 작성하기 때문에, async함수가 될 수 없다. 따라서, redux를 사용하면 리덕스 미들웨어인 thunk, saga를 통해 비동기 처리가 가능하다.
✏️ 순수 useReducer의 비동기 처리
이벤트 발생 > 비동기 작업 > dispatch > 상태 변경(reducer) > 렌더링
✏️ 리덕스 미들웨어의 비동기 처리
dispatch > 비동기 작업 > 상태 변경 > 렌더링
useReducer와 ContextAPI를 통해 redux의 기능 대부분을 구현 가능하다. 하지만, 규모가 큰 프로젝트에서는 useReducer와 ContextAPI의 조합으로는 비동기적인 작업을 할시 불편하다.
💡 ContextAPI, useReducer, Redux에 대한 자세한 설명
Redux란?
ContextAPI + useReducer vs Redux
출처 및 참고하기 좋은 사이트