[ React.JS ] useReducer, useMemo, useCallback
2025.01.21 기점으로 ( 본인 소속 ) 컴퓨터공학과 스터디 사이트 제작을 위한 React 학습. [ 1학년 겨울방학 프로젝트 ]
React 의 기술적인 구현 기록보다, React 를 처음 접하며 프로젝트를 진행하는 과정에서의 흐름과 기본적인 개념을 정리하기 위해 작성.
타 React 프로젝트 구현 시 개인 복기용으로 참고할 수 있도록, 첫 구현 과정에서 겪은 문제점과 새롭게 정립한 개념을 총망라하여 상세히 작성되어 있음.
( 개인적인 학습 목적이므로 직접적인 도움이 전혀 되지 않습니다 ❌ )
𝟏. useReducer
useReducer : 컴포넌트 내부에 새로운 State 를 생성하는 React Hook
모든 useState 는 useReducer 로 대체가 가능
상태 관리 코드를 컴포넌트 외부로 분리할 수 있음
useState 를 사용하면 ?
- 컴포넌트 내부에 상태 관리 코드를 작성해야 함
반면, useReducer 를 사용한다면 ?
- 컴포넌트 내부에서는 state 생성만 해두고, 실제로 state 를 관리하는 코드는 컴포넌트 외부에서 관리할 수 있음.
React 컴포넌트 주된 역할은, UI 를 렌더링 하는 것.
State 를 관리하는 코드가 복잡하고 길어진다면, useReducer 를 통해 유지/보수, 가독성을 높일 수 있음.
useReducer 개념과 장점
React의 useReducer는 상태 관리를 보다 체계적으로 할 수 있도록 돕는 훅이다. 보통 useState보다 복잡한 상태 로직이 필요한 경우에 사용된다. 상태 변화가 여러 단계로 구성되거나, 관련된 상태들이 함께 변경되어야 할 때 유용하다.
1️⃣ useReducer의 기본 문법
useReducer는 두 개의 인수를 받는다. 첫 번째는 reducer 함수, 두 번째는 초기 상태이다.
1
const [state, dispatch] = useReducer(reducer, initialState);
✅ state : 현재 상태를 의미한다.
✅ dispatch : 액션을 발생시키는 함수이다. 이 함수에 액션 객체를 전달하면, reducer가 실행된다.
✅ reducer : 상태를 업데이트하는 함수이다.
✅ initialState : 초기 상태 값이다.
2️⃣ useReducer vs useState
🔹 useState는 단순한 상태를 관리할 때 적합하지만, useReducer는 상태 변경이 복잡하거나 로직이 많아질 경우 코드의 유지보수를 쉽게 할 수 있도록 돕는다.
🔹 상태가 여러 개의 값으로 이루어져 있을 때는 useReducer를 사용하는 것이 좋다.
🔹 useReducer를 사용하면 상태 업데이트 로직을 컴포넌트 외부로 분리할 수 있어 가독성과 유지보수성이 향상된다.
3️⃣ reducer 함수 개념
reducer는 현재 상태(state)와 액션(action)을 받아 새로운 상태를 반환하는 순수 함수이다.
여기서 action 객체는 type과 payload(선택 사항)를 포함할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
function reducer(state, action) {
switch(action.type) {
case "INCREMENT":
return state + 1;
case "DECREMENT":
return state - 1;
case "RESET":
return 0;
default:
return state;
}
}
4️⃣ 실전 예제: 🗒️ Todo List 만들기
아래는 useReducer를 사용하여 간단한 Todo List를 구현한 코드이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import './App.css'
import { useReducer, useRef } from 'react'
import Header from './components/Header'
import Editor from './components/Editor'
import List from './components/List'
const tempData = [];
function reducer(state, action) {
switch (action.type) {
case "CREATE":
return [action.data, ...state];
case "UPDATE":
return state.map(todo =>
todo.id === action.targetId ? { ...todo, isDone: !todo.isDone } : todo
);
case "DELETE":
return state.filter(todo => todo.id !== action.targetId);
default:
return state;
}
}
function App() {
const [todo, dispatch] = useReducer(reducer, tempData);
const idRef = useRef(0);
const onCreate = (content) => {
dispatch({
type: "CREATE",
data: {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
},
});
};
const onUpdate = (targetId) => {
dispatch({
type: "UPDATE",
targetId
});
};
const onDelete = (targetId) => {
dispatch({
type: "DELETE",
targetId
});
};
return (
<div className='App'>
<Header />
<Editor onCreate={onCreate} />
<List todo={todo} onUpdate={onUpdate} onDelete={onDelete} />
</div>
)
}
export default App
5️⃣ 정리
✔️ useReducer는 복잡한 상태 로직을 관리할 때 유용하다.
✔️ 상태 변경 로직을 컴포넌트 외부로 분리하여 가독성을 높인다.
✔️ 여러 상태가 연결된 경우, dispatch를 통해 액션을 실행하면 깔끔한 코드 유지가 가능하다.
이제 useReducer의 기본적인 개념과 활용법을 이해했을 것이다. 🏆 상태 관리가 복잡해질 경우 언제든지 useReducer를 고려할 수 있다. 🚀
📁 Exam Code.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import { useReducer } from "react";
function reducer(state, action) {
// -> 상태를 실제로 변환시키는 변환기 역할
console.log(state, action);
switch(action.type) {
case "INCREASE":
return state + action.data;
case "DECREASE":
return state - action.data;
default:
return state;
}
}
const Exam = () => {
const [state, dispatch] = useReducer(reducer, 0);
// dispatch : 상태 변화가 있어야 한다는 사실을 알리는, 발송하는 함수
const onClickPlusButton = (e) => {
// 인수: 상태가 어떻게 변화되길 원하는지
// -> action 객체
dispatch({
type : "INCREASE", // 상태를 어떻게 변화시킬지 원하는지
data : 1,
});
}
const onClickMinusButton = (e) => {
// 인수: 상태가 어떻게 변화되길 원하는지
// -> action 객체
dispatch({
type : "DECREASE", // 상태를 어떻게 변화시킬지 원하는지
data : 1,
});
}
return (
<div>
<h1>{state}</h1>
<button onClick={onClickPlusButton}>+</button>
<button onClick={onClickMinusButton}>-</button>
</div>
)
}
export default Exam;
Action 객체 : 상태(state)를 변경하기 위해 dispatch 함수에 전달되는 객체
1
2
type : "INCREASE", // 상태를 어떻게 변화시킬지 원하는지
data : 1,
🌱 state++;를 사용하면 안 되는 이유
- state++는 원본 데이터를 직접 변경한다 (Mutating State) : reducer 는 새로운 상태를 반환 해야하지 기존 상태를 직접 변경해서는 안됨.
- return state + action.data;는 새로운 상태를 반환한다 : 이 방식은 기존 state 값을 변경하지 않고, 새로운 값을 반환하는 방식.
- state++를 사용하면 리액트가 상태 변경을 감지하지 못할 수도 있음 : 리액트는 상태(state)가 변경되었을 때 리렌더링을 트리거하는데, 기존 상태를 직접 변경하면 리액트가 변경을 감지하지 못할 수도 있음.
𝟏-𝟏. Todo List - Update (useReducer)
📁 App.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import './App.css'
import { useState, useRef, useReducer } from 'react'
import Header from './components/Header'
import Editor from './components/Editor'
import List from './components/List'
const tempData = [
/*Data Modeling*/
/*
{
id: 0,
isDone: false,
content: "React Project id: 0",
date: new Date().getTime(),
},
*/
];
function reducer(state, action) {
switch(action.type) {
case "CREATE":
return [action.data, ...state];
case "UPDATE":
return state.map((entryTodo) => entryTodo.id === action.targetId
? { ...entryTodo, isDone: !entryTodo.isDone}
: entryTodo
);
case "DELETE":
return state.filter((entryTodo) => entryTodo.id !== action.targetId);
default:
return state;
}
}
function App() {
const [todo, dispatch] = useReducer(reducer, tempData);
const idRef = useRef(0);
const onCreate = (content) => {
dispatch({
type:"CREATE",
data: {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
},
});
};
const onUpdate = (targetId) => { // update 하고자 하는 target의 id 를 인수로.
dispatch({
type: "UPDATE",
targetId: targetId
})
};
const onDelete = (targetId) => {
dispatch({
type: "DELETE",
targetId: targetId,
})
};
return (
<div className='App'>
<Header />
<Editor onCreate={onCreate} />
<List todo={todo} onUpdate={onUpdate} onDelete ={onDelete} />
</div>
)
}
export default App
𝟐. 최적화 (Optimization)
𝟐-𝟏. 최적화란
React App 내부의 최적화 방법
- 컴포넌트 내부의 불필요한 연산 방지
- 컴포넌트 내부의 불필요한 함수 재생성 방지
- 컴포넌트의 불필요한 리렌더링 방지
𝟐-𝟐. useMemo
Memoization 기법을 기반으로 불필요한 연산을 최적화하는 리액트 훅
Memoization (기억해두기, 메모해두기)
최초로 한번 계산된 결과값을 메모리에 보관 후, 해당
결과값이 필요할 때 메모리 에서 제공
📁 TodoItem.jsx ( 코드 내부 주석 참고 )
기존 코드는 search 바에 무엇인가 검색한다고 해서 실제로 todo data 가 실제로 추가되거나, 완료되는 것이 아니라서 불필요한 리렌더링 발생. ( total, done, notdone)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import "./List.css";
import TodoItem from "./TodoItem";
import { useState, useMemo } from "react";
const List = ({todo, onUpdate, onDelete}) => {
const [search, setSearch] = useState("");
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getFilterData = () => {
if (search === "") {
return todo;
}
return todo.filter((entryItem) =>
entryItem.content
.toLowerCase()
.includes(search.toLowerCase())
); // 소문자로 검색해도 나오도록 설정
};
const filteredTodo = getFilterData();
const {totalCount, doneCount, notDoneCount} = useMemo(()=>{
console.log("getAnalyzedData 호출!");
const totalCount = todo.length;
const doneCount = todo.filter((entryTodo) => entryTodo.isDone).length;
const notDoneCount = totalCount - doneCount;
return {
totalCount,
doneCount,
notDoneCount,
};
}, [todo])
// 의존성배열: deps
// deps 에 포함된 값이 변경되었을 때만 첫번째 인수로 전달한 콜백함수를 다시 실행
// 해당 콜백함수가 반환하는 값을 useMemo 는 그대로 다시 반환을 해줌
return (
<div className="List">
<h4>Todo List🌱</h4>
<div>
<div>total: {totalCount}</div>
<div>done: {doneCount}</div>
<div>notDone: {notDoneCount}</div>
</div>
<input
value={search}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요"
/>
<div className="TodoWrapper">
{filteredTodo.map((entryItem) => {
return (
<TodoItem
key={entryItem.id}
{...entryItem}
onUpdate={onUpdate}
onDelete={onDelete}
/>
);
})}
</div>
</div>
)
};
export default List;
𝟐-𝟑. React.memo
React.memo
컴포넌트를 인수로 받아, 최적화된 컴포넌트로 만들어 반환
1
const MemoizedComponent = memo(Component)
MemoizedComponent 반환값: 최적화된 컴포넌트, props를 준으로 메모이제이션 됨.
즉, 이제 MemoizedComponent 는 부모 컴포넌트가 리렌더링 되더라도, 자신이 받는 props 가 바뀌지 않으면 리렌더링 되지 않음.
📁 Header.jsx (Header 컴포넌트는 날짜 객체만 렌더링 기능.
todo data가 수정된다고 해서 이 컴포넌트가 리렌더링 될 이유가 없다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "./Header.css";
import { memo } from "react";
const Header = () => {
return (
<div className="Header">
<h3>오늘은🗓️</h3>
<h1>
{new Date().toDateString()}
</h1>
</div>
)
};
export default memo(Header);
📁 TodoItem.jsx (예를 들어, 첫번째 아이템의 체크박스를 on/off 반복하다 보면 하단 아이템 컴포넌트들도 함께 리렌더링. 불필요한 리렌더링.)
checkbox 클릭 (todo 데이터 변경)
-> App Component todo의 state 값을 바뀌게됨
-> App Component 가 리렌더링
-> App 함수 재호출(onCreate, onUpdate, onDelete)
-> 함수는 객체타입에 해당, 새롭게 생성된 함수들이 같은 동작을 한다고 하더라도 새롭게 생성될 때마다 아예 다른 값으로 인식.
-> memo 메서드는 props 가 바뀌었을 때만 컴포넌트가 리렌더링 하도록 최적화. 객체 타입의 값은 매번 새롭게 전달이 될때마다 다른 주소를 가지기 때문에, memo 메서드는 props 가 바뀌었다고 판단.