섹션 8: useReducer로 상태관리 로직 분리하기
useReducer
useState와의 공통점
- 새로운 state 생성
- State를 업데이트시키는 함수 제공
useState vs useReducer
- useState: 컴포넌트 내부에 State 관리 로직을 작성해야 함 → 컴포넌트 내부에 들어갈 로직이 복잡해지고 길어질 수록 가독성이 매우 안 좋아짐
- useReducer: 컴포넌트 외부에 State 관리 로직 분리 가능 → 컴포넌트 코드를 간결하게 유지 가능 (복잡한 상태관리 로직이 사용될 때 합리적)
Reducer 사용X
import { useState } from "react";
export default function A() {
const [count, setCount] = useState(0);
const onDecrease = () => {
setCount(count - 1);
};
const onIncrease = () => {
setCount(count + 1);
};
return (
<div>
<h4>{count}</h4>
<button onClick={onDecrease}>-</button>
<button onClick={onIncrease}>+</button>
</div>
);
}
👇
Reducer 사용
import { useReducer } from "react";
//컴포넌트 외에 로직 작성
function reducer(state, action) {
if (action.type === "DECREASE") {
return state - action.data;
} else if (action.type === "INCREASE") {
return state + action.data;
}
}
export default function B() {
const [count, dispatch] = useReducer(reducer, 0);
//count: state의 값
//dispatch: 상태변화를 발동시키는 트리거 역할을 하는 함수
//reducer: 상태변화를 처리하는 함수. 함수가 리턴될 시 count 값의 상태가 변화한다.
//0: 초기값
return (
<div>
<h4>{count}</h4>
<button
onClick={() => {
dispatch({ type: "DECREASE", data: 1 }); //action 객체
}}
>
-
</button>
<button
onClick={() => {
dispatch({ type: "INCREASE", data: 1 }); //action 객체
}}
>
+
</button>
</div>
);
}
투두리스트 업그레이드(useReducer)
Reducer 사용X
import { useState, useRef } from "react";
...
const mockData = [
{
id: 0,
isDone: true,
content: "React 공부하기",
createdDate: new Date().getTime(),
},
...
];
function App() {
const [todos, setTodos] = useState(mockData);
const idRef = useRef(3);
const onCreate = (content) => {
//content: input 태그에 사용자가 입력한 투두 항목
const newTodo = {
id: idRef.current++,
isDone: false,
content: content, //content로만 작성해도 가능(js 특성)
createdDate: new Date().getTime(),
}; //객체로 만드는 이유: 그렇게 설계해놨기 때문(mockData)
setTodos([...todos, newTodo]); //기존 배열+새로운 요소 추가
};
const onUpdate = (targetId) => {
setTodos(
todos.map((todo) =>
todo.id === targetId ? { ...todo, isDone: !todo.isDone } : todo
)
);
};
const onDelete = (targetId) => {
setTodos(todos.filter((todo) => todo.id !== targetId));
};
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
</div>
);
}
export default App;
👇
Reducer 사용
import { useRef, useReducer } from "react";
...
const mockData = [...];
function reducer(state, action) {
switch (action.type) {
case "CREATE": {
return [...state, action.data];
}
case "UPDATE": {
return state.map((todo) =>
todo.id === action.data ? { ...todo, isDone: !todo.isDone } : todo
);
}
case "DELETE": {
return state.filter((todo) => todo.id !== action.data);
}
}
}
function App() {
const [todos, dispatch] = useReducer(reducer, mockData);
const idRef = useRef(3);
const onCreate = (content) => {
//setTodo 대신 dispatch
dispatch({
//action객체
type: "CREATE",
data: {
id: idRef.current++,
isDone: false,
content: content, //content로만 작성해도 가능(js 특성)
createdDate: new Date().getTime(),
}, //객체로 만드는 이유: 그렇게 설계해놨기 때문(mockData)
});
};
const onUpdate = (targetId) => {
dispatch({
type: "UPDATE",
data: targetId,
});
};
const onDelete = (targetId) => {
dispatch({ type: "DELETE", data: targetId });
};
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
</div>
);
}
export default App;
섹션 8: 최적화
투두리스트 최적화 (useMemo: 재연산 방지하기)
...
export default function TodoList({ todos, onUpdate, onDelete }) {
...
const getAnalyzedTodoData = () => {
const totalCount = todos.length;
const doneCount = todos.filter((todo) => todo.isDone).length;
const notDoneCount = totalCount - doneCount;
return {
totalCount,
doneCount,
notDoneCount,
};
};
//구조분해 할당으로 받아오기
const { totalCount, doneCount, notDoneCount } = getAnalyzedTodoData();
return (
<div className="TodoList">
<h4>Todos</h4>
<div>
<div>전체 todo: {totalCount}</div>
<div>완료 todo: {doneCount}</div>
<div>미완 todo: {notDoneCount}</div>
</div>
<input
value={search}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요"
/>
...
</div>
);
}
- filter 함수를 사용하고 있으므로, 배열의 길이가 길어질 수록 배열 순회 연산량이 길어진다.
- searchBar에 검색어를 입력할 때마다 분석 함수가 실행된다. (불필요한 연산)
useMemo
특정 조건을 만족하지 않을 시 연산을 다시 수행하지 않도록 하는 리액트 훅. 연산 최적화를 목적으로 한다.
//구조분해 할당으로 받아오기
const { totalCount, doneCount, notDoneCount } = useMemo(() => {
//실행할 내용이 담긴 콜백함수
const totalCount = todos.length;
const doneCount = todos.filter((todo) => todo.isDone).length;
const notDoneCount = totalCount - doneCount;
return {
totalCount,
doneCount,
notDoneCount,
};
}, [todos]); //todos의 값이 바뀔 때마다 실행된다.
불필요한 연산 없이 todos의 값이 바뀔 때마다(새로운 todo가 추가되거나 삭제) 실행된다.
투두리스트 최적화2 (React.memo: 컴포넌트 리렌더 방지하기)
todo 추가 시 todo와 전혀 상관없는 Header 컴포넌트가 리렌더링 되는 현상이 발생
- 이유: todo 추가 시 부모 컴포넌트인 App 컴포넌트가 변경되고, 따라서 App 컴포넌트의 자식 컴포넌트인 Header 컴포넌트도 리렌더링된다.
- 해결 방안: React.memo
React.memo
부모 컴포넌트가 리렌더되더라도 인수로 받은 컴포넌트가 변경되지 않으면 해당 컴포넌트는 리렌더되지 않는다.
header.jsx
import { memo } from "react";
import "./Header.css";
export default unction Header() {
return (
<div className="Header">
<h1>{new Date().toDateString()}</h1>
</div>
);
}
👇
React.memo 사용
header.jsx
import { memo } from "react";
import "./Header.css";
function Header() {
return (
<div className="Header">
<h1>{new Date().toDateString()}</h1>
</div>
);
}
//memo: 인수로 컴포넌트 전달받아서 최적화된 컴포넌트로 반환
const OptimizedHeaderComponent = memo(Header);
export default OptimizedHeaderComponent;
<문제점> React.memo와 원시 자료형, 참조 자료형
TodoItem.jsx
import { memo } from "react";
import "./TodoItem.css";
function TodoItem({ id, isDone, createdDate, content, onUpdate, onDelete }) {
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDeleteButton = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<input onChange={onChangeCheckbox} type="checkbox" checked={isDone} />
<div className="content">{content}</div>
<div className="date">{new Date(createdDate).toLocaleDateString()}</div>
<button onClick={onClickDeleteButton}>삭제</button>
</div>
);
}
export default memo(TodoItem);
👉 이렇게 React.memo를 지정해 주어도, 투두리스트 하나의 체크박스를 체크했을 때 나머지 아이템들에서도 렌더링이 동시에 일어난다. 이유는 새로운 컴포넌트를 추가하거나 체크박스를 눌러 isDone의 상태를 바꿀 때, 나머지 TodoItem들이 전달받는 props도 모두 바뀌기 때문이다.
원시 자료형
- 변수에 값 자체가 저장됨
- 숫자, 문자열, 불리언, Null, Undefined
참조 자료형
- 변수에 참조값(주소값)으로 저장됨
- 매번 프로젝트가 렌더되어 다시 호출될 때마다 다른 참조값을 가지므로, 렌더링 이전과 동일하지 않다.
- 객체, 배열, 함수
→ TodoItem.jsx에서는 TodoItem의 props로 다음과 같은 항목들을 전달받고 있다. ({ id, isDone, createdDate, content, onUpdate, onDelete })
함수 onUpdate, onDelete는 상위 컴포넌트인 App 컴포넌트에서 정의 및 호출되고 있다.
참조 자료형의 특징 때문에, 값이 고정된 id, isDone, createdDate, content와 달리 함수인 onUpdate, onDelete는 렌더링 될 때마다 참조값이 달라지며, 따라서 렌더링 할 때마다 상태 변화가 일어난다.
따라서 TodoItem.jsx에서 한 아이템의 체크박스를 체크하여 상태 변화가 발생한다면 todos의 상태가 변하게 되고, todos는 App 컴포넌트에 정의되어 있으므로 상태변화를 감지한 App 컴포넌트도 리렌더링된다. App 컴포넌트가 리렌더링될 때마다 각 TodoItem들에게 내려주는 함수도 매번 바뀌게 되므로 나머지 TodoItem들에서도 렌더링이 다시 일어나게 된다.
<문제점 해결방안> 투두리스트 최적화3 (useCallback: 함수 재생성 방지하기)
App.jsx
import { useRef, useReducer, useCallback } from "react";
import "./App.css";
import Header from "./components/Header";
import TodoEditor from "./components/TodoEditor";
import TodoList from "./components/TodoList";
const mockData = [...];
function reducer(state, action) {
...
}
function App() {
const [todos, dispatch] = useReducer(reducer, mockData);
...
const onUpdate = useCallback((targetId) => {
dispatch({
type: "UPDATE",
data: targetId,
});
}, []); //의존성 배열. 배열 안의 내용이 바뀔 때 리렌더링됨
const onDelete = useCallback((targetId) => {
dispatch({ type: "DELETE", data: targetId });
}, []);
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
</div>
);
}
export default App;
출처
https://kmooc.udemy.com/course/react-next-master/learn/lecture/39610746