import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import "./App.css";
import DiaryEditor from "./DiaryEditor";
import DiaryList from "./DiaryList";
function App() {
const [data, setData] = useState([]);
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 {
name: it.email,
content: it.body,
hungry: (Math.floor(Math.random() * 9) + 1) * 10,
created_date: new Date().getTime(),
id: dataId.current++,
};
});
setData(initData);
};
useEffect(() => {
getData();
}, []);
const onCreate = useCallback((name, content, hungry) => {
const created_date = new Date().getTime();
const newItem = {
name,
content,
hungry,
created_date,
id: dataId.current,
};
dataId.current += 1;
setData((data) => [newItem, ...data]);
}, []);
const onRemove = useCallback((targetId) => {
setData((data) => data.filter((it) => it.id !== targetId));
}, []);
const onEdit = useCallback((targetId, newContent) => {
setData((data) =>
data.map((it) =>
it.id === targetId ? { ...it, content: newContent } : it
)
);
}, []);
const getDiaryAnalysis = useMemo(() => {
const reallyHungy = data.filter((it) => it.hungry >= 80).length;
const notHungry = data.filter((it) => it.hungry < 40).length;
const reallyHungryRatio = (reallyHungy / data.length) * 100;
const notHungryRatio = (notHungry / data.length) * 100;
return { reallyHungy, notHungry, reallyHungryRatio, notHungryRatio };
}, [data.length]);
const { reallyHungy, notHungry, reallyHungryRatio, notHungryRatio } =
getDiaryAnalysis;
return (
<div className="App">
<DiaryEditor onCreate={onCreate} />
<div>전체 일기 : {data.length}</div>
<div>배부른 일기 개수 : {notHungry}</div>
<div>배고픈 일기 개수 : {reallyHungy}</div>
<div>배부른 일기 비율 : {notHungryRatio}%</div>
<div>배고픈 일기 비율 : {reallyHungryRatio}%</div>
<DiaryList onEdit={onEdit} onRemove={onRemove} diaryList={data} />
</div>
);
}
export default App;
현재 제 컴포넌트 최상단의 App 컴포넌트입니다. onCreate, onEdit, onRemove 상태 변화 처리함수를 가지고 있습니다. 컴포넌트가 길어지고 무거워지는 것은 좋은 것이 아닙니다. 그래서 복잡하고 긴 상태 변화 처리 함수를 컴포넌트 바깥으로 분리해보도록 하겠습니다.
useState 처럼 react의 상태관리를 돕는 react hooks 입니다. useReducer를 이용하면 상태 관리 로직들을 컴포넌트에서 분리할 수 있습니다. 결과적으로 컴포넌트를 가볍게 사용할 수 있습니다.
import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
// const [data, setData] = useState([]);
const [data, dispatch] = useReducer(reducer, []);
기존에 사용하던 useState를 주석처리 해주고, useReducer hooks를 선언해주도록 합니다.
이제 컴포넌트 바깥에 reducer를 선언해주도록 합니다.
const reducer = (state, action) => {
switch (action.type) {
case "INIT":
case "CREATE":
case "REMOVE":
case "EDIT":
default:
return state;
}
};
이런 형태로 작성을 할 것입니다. 주의할 점은 default는 필수입니다.
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 {
name: it.email,
content: it.body,
hungry: (Math.floor(Math.random() * 9) + 1) * 10,
created_date: new Date().getTime(),
id: dataId.current++,
};
});
dispatch({ type: "INIT", data: initData });
// setData(initData); 주석처리
};
setData를 주석처리하고 대신 dispatch를 사용해줍니다. type을 INIT으로 보내고, init action에 필요한 데이터인 initData를 보냅니다.
const reducer = (state, action) => {
switch (action.type) {
case "INIT": {
return action.data;
}
case "CREATE":
case "REMOVE":
case "EDIT":
default:
return state;
}
};
reducer 함수에서는 return 값은 새로은 state 객체를 의미합니다. init case일 시 그 값으로 리턴해주면 새로운 state가 됩니다.
const onCreate = useCallback((name, content, hungry) => {
dispatch({
type: "CREATE",
data: { name, content, hungry, id: dataId.current },
});
dataId.current += 1;
}, []);
useCallback 함수 안에 dispatch를 선언해줍니다. 기존에 setData함수도 지워줍니다.
const reducer = (state, action) => {
switch (action.type) {
case "INIT": {
return action.data;
}
case "CREATE": {
const created_date = new Date().getTime();
const newItem = {
...action,
created_date,
};
return [newItem, ...state];
}
case "REMOVE":
case "EDIT":
default:
return state;
}
};
reducer 함수 안에 있는 create case에 로직을 작성해줍니다.
const onRemove = useCallback((targetId) => {
dispatch({ type: "REMOVE", targetId });
}, []);
targetId를 넘겨 reducer함수에서 제거해주도록 하겠습니다.
const reducer = (state, action) => {
switch (action.type) {
case "INIT": {
return action.data;
}
case "CREATE": {
const created_date = new Date().getTime();
const newItem = {
...state,
created_date,
};
return [newItem, ...state];
}
case "REMOVE": {
return state.filter((it) => it.id !== action.targetId);
}
case "EDIT":
default:
return state;
}
}
filter 함수를 사용하여 remove를 수행합니다.
const onEdit = useCallback((targetId, newContent) => {
dispatch({ type: "EDIT", targetId, newContent });
}, []);
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,
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;
}
};
map 함수를 사용하여 targetId가 같으면 컨텐츠를 변경합니다.
useState에서 useReducer로 모두 변경해보았습니다.
최종 코드입니다.
App.js
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 "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 {
name: it.email,
content: it.body,
hungry: (Math.floor(Math.random() * 9) + 1) * 10,
created_date: new Date().getTime(),
id: dataId.current++,
};
});
dispatch({ type: "INIT", data: initData });
// setData(initData);
};
useEffect(() => {
getData();
}, []);
const onCreate = useCallback((name, content, hungry) => {
dispatch({
type: "CREATE",
data: { name, content, hungry, id: dataId.current },
});
dataId.current += 1;
}, []);
const onRemove = useCallback((targetId) => {
dispatch({ type: "REMOVE", targetId });
}, []);
const onEdit = useCallback((targetId, newContent) => {
dispatch({ type: "EDIT", targetId, newContent });
}, []);
const getDiaryAnalysis = useMemo(() => {
const reallyHungy = data.filter((it) => it.hungry >= 80).length;
const notHungry = data.filter((it) => it.hungry < 40).length;
const reallyHungryRatio = (reallyHungy / data.length) * 100;
const notHungryRatio = (notHungry / data.length) * 100;
return { reallyHungy, notHungry, reallyHungryRatio, notHungryRatio };
}, [data.length]);
const { reallyHungy, notHungry, reallyHungryRatio, notHungryRatio } =
getDiaryAnalysis;
return (
<div className="App">
<DiaryEditor onCreate={onCreate} />
<div>전체 일기 : {data.length}</div>
<div>배부른 일기 개수 : {notHungry}</div>
<div>배고픈 일기 개수 : {reallyHungy}</div>
<div>배부른 일기 비율 : {notHungryRatio}%</div>
<div>배고픈 일기 비율 : {reallyHungryRatio}%</div>
<DiaryList onEdit={onEdit} onRemove={onRemove} diaryList={data} />
</div>
);
}
export default App;
useReducer를 사용하여 복잡한 상태관리 로직을 컴포넌트로부터 분리를 해보았습니다.
리액트 공식 홈페이지
https://ko.legacy.reactjs.org/docs/react-api.html#reactmemo
해당 게시글은 인프런 강의
"한입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지(이정환)"
를 정리한 내용입니다. 쉽게 잘 설명해주시니 여러분도 강의를 듣는 것을 추천드립니다.