[ 상태 변화 로직 분리하기 ]
App 컴포넌트에는 다양한 상태 변화 함수가 존재한다. 이러한 상태 변화 함수들은 컴포넌트 내에서만 존재할 수 있다.
왜냐하면 상태를 업데이트 하기 위해서는 기존의 상태를 참조해야하기 때문이다.
컴포넌트의 복잡하고 긴 상태변화 로직을 컴포넌트 바깥으로 분리하는 기능인 useReducer를 사용한다.
useReducer는 리액트의 상태 관리를 돕는 React Hooks이다.
* React Hooks : 함수형 컴포넌트에서 클래스형 컴포넌트의 기능을 가져와서 사용할 수 있도록 해주는 기능
[ useReducer ]
useState를 사용하면 여러 개의 상태 변화 함수를 모두 같은 컴포넌트 안에 작성해야 되서 컴포넌트의 코드가 길고
복잡해진다. 이러한 방법은 매우 좋지 않다.
useReducer를 사용하면 상태 변화 함수를 컴포넌트 바깥으로 분리해서 다양한 상태 변화 로직을 컴포넌트의
외부에서 switch/case처럼 처리할 수 있다.즉 useRuducer는 useState를 대체할 수 있는 기능이다.
const [count, dispatch] = useReducer( reducer,1 );
useRuducer는 배열을 반환하게 되고 사용할 때는 비구조화 할당을 사용하게 된다.
* 비구조화할당 : []을 활용해서 배열의 값을 순서대로 변수에 할당해서 사용하는 방법
( 값을 할당받지 못하는 변수는 undefined를 갖게 된다. )
첫번째로 반환받게 되는 0번째 인덱스 값은 state이다. 두번째로 반환받게 되는 1번째 인덱스는 state를 변화시키는
액션을 발생시키는 함수이다. 그리고 useRuducer를 호출할 때 reducer라는 함수를 꼭 전달을 해줘야한다.
dispatch가 상태 변화를 일으킨다면, reducer는 일어난 상태 변화를 처리하는 역할을 하게 된다.
또한 useRuducer를 호출할 때 전달하는 두번째 인자는 count state의 초기값이다.
useRuducer를 사용해서 count라는 state를 만들게 되면 그 초기 값을 1로 지정을 해주게 되고, count state를
변경하고 싶다면 상태 변화 함수인 dispatch를 호출해서 상태 변화를 일으키면 상태 변화를 처리하는 함수인
reducer가 그 변화를 처리하게 된다.
<button onClick={() => dispatch({type:1})}> + </button>
dispatch 함수를 호출할 때는 객체를 같이 전달해준다. 그 객체에는 type이라는 프로퍼티가 꼭 저장이 되어있다.
dispatch와 함께 전달이 되는 이 객체를 action 객체라고 부른다. * action == 상태 변화
action 객체는 상태 변화를 설명할 객체이다. dispatch가 호출이 되면서 전달된 action 객체는 reducer로 전달되게
된다. dispatch가 일어나면 상태 변화가 일어나야 하고 그 상태 변화에 대한 처리는 reducer가 수행을 한다.
const reducer = ( state, action ) => {
switch( action.type ){
case 1 :
return state + 1;
...
default :
return state;
}
};
reducer는 dispatch가 일어난 상태 변화를 처리하기 위해 호출이 되는데 첫번째 파라미터로는 현재 가장 최신의
state를 전달받고 두번째 파라미터로는 dispatch를 호출할 때 전달해줬던 action 객체를 전달받게 된다.
버튼을 클릭하면 reducer 함수가 실행이 되는데 reducer 함수가 받게 되는 state 파라미터의 값은 1이고,
action 객체는 type : 1이라는 객체를 받게 된다. 상태 변화를 처리하는 reducer 함수에서는 switch/case문을
사용해서 action의 type에 따라서 각각 다른 값을 반환할 수 있다. 이 때 새로 반환하는 값은 새로운 state가 된다.
즉, 버튼을 클릭하게 되면 case 1을 실행하게 되어 현재 state 값인 1에 1을 더해서 2를 반환하게 되고 2는 새로운
state가 된다. count에는 state가 업데이트가 되서 2가 반영이 된다.
dispatch는 호출만 하면 현재 state를 reducer 함수가 알아서 참조를 해서 useCallback을 사용하면서
dependency array를 신경써주지 않아도 된다.
* useCallback : memoization된 콜백 함수를 다시 반환해주는 역할을 한다.
[ App 컴포넌트의 상태 변화 로직을 useReduer를 사용해서 분리시켜보기 ]
1. data를 useState가 아니라 useReducer를 통해서 관리하기
useReducer를 사용해서 data state 생성한다. 비구조화 할당으로 생성되는 배열의 첫번째 인자로는 data state를
전달해주고, 두번째 인자로는 상태 변화를 일으키는 함수인 dispatch를 전달해준다.
* 상태 변화를 일으키는 함수인 dispatch는 이름을 바꾸면 안된다.
useReducer는 리액트에서 import를 받아야하고, 인자로는 상태 변화를 처리해주는 함수인 reducer와 data state의
초기값인 빈 배열을 전달해준다.
// data state는 일기 데이터 배열을 저장하기 때문에 배열로 초기값을 만들어준다.
const [data, dispatch] = useReducer(reducer, []);
2. 상태 변화를 처리해주는 reducer 함수를 App 컴포넌트 외부에 생성하기
* useState를 사용하는 대신 reducer를 사용하는 이유 : 복잡한 상태 변화 로직을 컴포넌트 밖으로 분리하기 위해서
const reducer = (state, action) => {
switch (action.type) {
....
}
};
reducer 함수의 첫번째 파라미터는 상태 변화가 일어나기 직전의 state이고, 두번째 파라미터로는 어떤 상태 변화를
일으켜야 하는지에 대한 정보가 담겨 있는 action 객체이다. reducer에서는 action 객체에 담겨져있는 type
프로퍼티와 switch/case를 통해서 상태 변화를 처리할 수 있다. reducer 함수가 리턴하는 값이 새로운 state의 값이
된다.
3. 어떤 type들의 action이 존재하는지 확인하기
reducer는 항상 새로운 state를 리턴해줘야하는 의무가 존재한다.
① getData : initData를 API 호출하고 가공을 해서 한번에 데이터를 초기화
→ initData를 통해서 data를 초기화한다.
dispatch({type : "INIT", data : initData})
INIT action에 필요한 데이터도 함께 전달해줘야한다. 전달해주지 않으면 reducer는 알 방법이 없다.
reducer는 action 객체를 받는데 action의 type은 "INIT"이고, 그 action에 필요한 data는 initData이다.
dispatch 함수를 작성해준다면 setData는 더이상 필요가 없다. setData가 할 일을 dispatch 함수가 대신 해준다.
const reducer = (state, action) => {
switch (action.type) {
case "INIT": {
return action.data;
}
default:
return state;
}
};
getData 함수에서는 dispatch를 일으켰을 때 type을 INIT으로 전달하면서 어떤 데이터로 초기화할 것인지 지칭하는
data 프로퍼티에 initData를 넣어놨기 때문에 reducer에서 받았을 때 action 객체의 data 프로퍼티를 꺼내서 그 값을
전달해준다면 action.data는 새로운 state가 된다. 그래서 그대로 action.data를 리턴해주면 된다.
② onCreate : 기존에 존재하던 데이터에 새로운 일기를 추가하는 함수
const onCreate = useCallback((author, content, emotion) => {
dispatch({
type: "CREATE",
data: { author, content, emotion, id: dataId.current },
});
dataId.current += 1;
}, []);
data에는 author, content, emotion, id를 전달해준다.
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];
}
default:
return state;
}
};
reducer는 새로운 일기 아이템을 원본 일기 아이템에 추가를 한 새로운 배열을 새로운 state 값으로 리턴을 해야한다.
[ newItem, ...state ] 에서 state는 상태 변화가 일어나기 전의 일기 데이터가 들어있는 배열이고 newItem은
새로 생성한 일기 데이터이다. 이 2개를 합쳐주면 새로운 state값을 리턴할 수 있다.
③ onDelete : targetID로 전달해준 일기를 삭제한다.
const onDelete = useCallback((targetId) => {
dispatch({ type: "DELETE", targetId });
}, []);
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 "DELETE": {
return state.filter((it) => it.id !== action.targetId);
}
default:
return state;
}
};
④ onEdit : targetID로 전달받은 일기의 내용을 newContent로 수정한다.
const onEdit = useCallback((targetId, newContent) => {
dispatch({ type: "EDIT", targetId, newContent });
}, []);
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 "DELETE": {
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;
}
};
Edit type의 action이 발생하게 되면 action으로 targetID와 newContent가 전달이 된다. 기존 state에서 map
함수를 사용해서 targetID와 일치하는 요소를 찾아준 다음에, 그 요소의 content 값을 newContent로 변경을 해준다.
일치하는 요소가 아니라면 기존 값을 그대로 돌려준다. 그 모든 요소들을 합쳐서 새로운 배열을 만들어서
새로운 state로 반환해준다.
[ App 컴포넌트 전체 확인하기 ]
import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
import "./App.css";
import DiaryEditor from "./DiaryEditor";
import DiaryList from "./DiaryList";
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 "DELETE": {
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() {
// data state는 일기 데이터 배열을 저장하기 때문에 배열로 초기값을 만들어준다.
const [data, dispatch] = useReducer(reducer, []);
const dataId = useRef(1); // ID는 1부터 시작
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({ type: "INIT", data: initData });
};
useEffect(() => {
getData();
}, []);
const onCreate = useCallback((author, content, emotion) => {
dispatch({
type: "CREATE",
data: { author, content, emotion, id: dataId.current },
});
dataId.current += 1;
}, []);
const onDelete = useCallback((targetId) => {
dispatch({ type: "DELETE", targetId });
}, []);
const onEdit = useCallback((targetId, newContent) => {
dispatch({ type: "EDIT", targetId, newContent });
}, []);
const getDiaryAnalysis = useMemo(() => {
// emotion이 3이상인 일기의 개수
const goodCount = data.filter((it) => it.emotion >= 3).length;
// emotion이 3미만인 일기의 개수
const badCount = data.length - goodCount;
// emotion이 3이상인 일기 비율
const goodRatio = (goodCount / data.length) * 100;
return { goodCount, badCount, goodRatio };
}, [data.length]);
//getDiaryAnalysis()의 반환값이 객체이므로 객체로 비구조화 할당을 받는다.
const { goodCount, badCount, goodRatio } = getDiaryAnalysis;
return (
<div className="App">
<DiaryEditor onCreate={onCreate} />
<div>전체 일기 : {data.length}</div>
<div>emotion이 3이상인 일기 개수 : {goodCount}</div>
<div>emotion이 3미만인 일기 개수 : {badCount}</div>
<div>emotion이 3이상인 일기 비율 : {goodRatio}</div>
<DiaryList onEdit={onEdit} diaryList={data} onDelete={onDelete} />
</div>
);
}
export default App;
'Front-End > React' 카테고리의 다른 글
[ React ] React 실전 ① 페이지 라우팅 (0) | 2022.11.02 |
---|---|
[ React ] React 기본 ⑨ 컴포넌트 트리에 데이터 공급하기 - Context (0) | 2022.10.23 |
[ React ] React 기본 ⑦ 최적화 - 최적화 완성 (0) | 2022.10.22 |
[ React ] React 기본 ⑦ 최적화 - useCallback (0) | 2022.10.22 |
[ React ] React 기본 ⑦ 최적화 - React.memo (0) | 2022.10.22 |