상태 변화 함수들이 존재
하고 있음 (onCreate, onEdit, onRemove 등등)const [data, setData] = useState([]);
그 상태를 업데이트하기 위해서는 기존의 상태를 참조
해야 했기 때문임useReducer
라는 Hooks를 사용하여 이런 복잡하고 긴 상태 변화 로직을 컴포넌트 바깥으로 분리
할 수 있게 되어서 컴포넌트를 더 가볍게 작성
할 수 있도록 도와준다.useReducer의 필요성
useReducer
는 useState를 대체할 수 있는 기능으로, 왼쪽에 보이는 reducer라는 상태 변화 함수를 컴포넌트 바깥에 분리함으로써 다양한 상태 변화 로직을 컴포넌트 외부에 switch - case 문법
을 통해 쉽게 처리할 수 있도록 바꿀 수 있음.useState를 사용하듯이 배열을 반환
하게 되고, 배열의 비구조화 할당
을 통해서 사용한다고 보면 됨.
첫 번째로 반환받게 되는 0번째 index인 count
는 그냥 state
고, 이 state를 useState에서 사용했던 것처럼 return 안에 {count}
이런 식으로 사용 가능함
두 번째로 반환받게 되는 1번째 index인 dispatch
는 상태를 변화시키는 action을 발생시키는 함수
라고 생각하면 됨. (상태 변화 함수)
useReducer 함수를 호출할 때는 첫 번째로 꼭 reducer
라는 함수를 전달해주어야 함. reducer는 상태 변화를 일으킨 dispatch를 처리해주는 역할
을 하게 됨.
두 번째로 전달하는 인자
인 1의 경우 count state의 초기값
.
dispatch
함수를 호출 시 type이라는 프로퍼티가 객체
에 들어있고, dispatch와 함께 전달되는데 이것을 action 객체
라고 부름. (action = 상태 변화, 다시 말해 상태 변화를 설명할 객체) dispatch가 호출되면서 전달된 이 action 객체는 reducer로 날아가게 된다.
reducer 함수는 dispatch가 일어나면 처리하기 위해 호출이 되는데 첫 번째 인자로는 가장 최신 state
를 받고, 두 번째 인자로는 이 dispatch를 호출할 때 전달해줬던 action 객체
를 전달받게 된다.
add 1 버튼 클릭 시 reducer 함수가 실행되고 이 reducer 함수가 받게 된 인자인 state는 1이 되고(현재), action 객체는 type: 1이라는 객체를 받게 됨
그러면 상태 변화를 처리하는 reducer 함수는 switch - case
문을 이용해서 action type에 따라 각각 다르게 반환하여 새로운 state가 된다.
(type : 1로 전달 > return state + 1에 더해져서 state는 2가 반영된다)
✔️ 정리
useReduce를 사용해서 count라는 state를 만들면, 초기 값은 1로 할당됨. 처음 1인 count state의 값을 변경하고 싶다면, 상태 변화 함수인 dispatch를 호출해서 상태 변화를 일으키면 상태 변화 처리 함수인 reducer가 그걸 처리하게 되는 것이라고 생각하면 됨. reducer로 전달한 action 객체는 dispatch가 일어나면 switch - case 문을 이용해 action type에 따라 새로운 state를 반환한다.
src/App.js
// import useReducer
import { useCallback, useEffect, useMemo, useRef, useReducer } from "react";
import "./App.css";
import DiaryEditor from "./DiaryEditor";
import DiaryList from "./DiaryList";
// 상태변화를 밖으로 분리하는 reducer 함수 작성 (switch - case)
// reducer가 리턴하는 값이 새로운 상태의 값이 된다
// data에 어떤 action이 필요한지 알아보기
// action의 4가지 case (INIT, CREATE, REMOVE, EDIT)
const reducer = (state, action) => {
switch(action.type){
// action 객체에서 data 프로퍼티를 꺼내서 새로운 state가 된다
case "INIT" : {
return action.data;
}
// create_date와 newItem은 여기에서 생성
// 새로운 일기 작성 시 새 아이템을 원본 배열에 추가해서 리턴
case "CREATE" : {
const created_date = new Date().getTime();
const newItem = {...action.data, created_date};
return [newItem, ...state];
}
// action 객체의 targetId가 아닌 애들만 필터링해서 새로운 state 값으로 전달
case "REMOVE" : {
return state.filter((it) => it.id !== action.targetId);
}
// EDIT action 발생하면 map 함수를 사용
// 전달받은 타겟아이디와 일치하는 요소를 찾아준 다음
// 그 요소의 값은 content만 newContent로 수정해주고,
// 나머지 요소는 그대로 돌려준 후 그 요소들을 합쳐서 새로운 배열을 만들고
// 새로운 state로 보내줌
case "EDIT" : {
return state.map((it) => it.id === action.targetId
? {...it, content: action.newContent } : it
);
}
// switch - case 문에는 반드시 default case를 작성해야 한다
// reducer는 항상 새로운 state를 리턴해줘야 하는 의무가 있는데,
// default일 경우 타입을 잘못 전달했구나라고 생각해서 상태 변화를 시키지 않도록 한다.
// (state를 그대로 전달해서 새로운 값으로 사용하면 그냥 값이 안 바뀜)
default : return state;
}
};
function App() {
// useReducer (상태변화처리하는 reducer와 data state의 초기값(빈배열)
const [data, dispatch] = useReducer(reducer, []);
// getData > API 콜해서 적절히 가공 후 한방에 data를 초기화하는 함수
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,
content : it.body,
emotion : Math.floor(Math.random() * 5) + 1,
created_date : new Date().getTime(),
id : dataId.current++
}
});
// dispatch 호출
// reducer는 action 객체를 받는데 INIT type을 받고,
// 그 action의 필요한 data는 initData가 된다
// 기존의 setData가 할 일은 reducer가 대신 한다
dispatch({type:"INIT", data:initData});
};
// 생성하는 함수
// dispatch 호출 CREATE type일 때 기존의 newItem에 있던 프로퍼티를 그대로 전달
const onCreate = useCallback(
(author, content, emotion) => {
dispatch({type:"CREATE", data:{author, content, emotion, id:dataId.current}});
dataId.current += 1;
}, []);
// 삭제하는 함수
// dispatch 호출 REMOVE type일 때 타겟아이디를 가진 일기를 지우라고 전달
const onRemove = useCallback((targetId) => {
dispatch({type:"REMOVE", targetId});
},[]);
// 수정하는 함수
// dispatch 호출 EDIT type일 때 타겟아이디와 새로운 컨텐츠를 전달
const onEdit = useCallback((targetId, newContent) => {
dispatch({type:"EDIT", targetId, newContent});
}, []);
...
}