https://www.udemy.com/course/react-next-master/
useReducer
는 useState
와 동일하게 새로운 state를 생성하고 업데이트하는 함수를 제공한다. 기본적으로 모두 useState
와 동일하지만, useState
는 state
관리를 오직 react component 내부에서만 가능하다는 것이다.
반면 useReducer
는 state
를 컴포넌트 외부에 state
관리 로직 분리가 가능하다. 따라서, 복잡한 state
를 관리해야할 때는 useReducer
를 통해 사용하는 것이 좋다.
import { useState } from "react"
export default function A() {
const [count, setCount] = useState(0);
const onDerease = () => {
setCount(count - 1)
}
const onIncrease = () => {
setCount(count + 1)
}
return <div>
<h4>{count}</h4>
<div>
<button onClick={onDerease}>-</button>
<button onClick={onIncrease}>+</button>
</div>
</div>
}
다음은 기본적인 useState
사용방법이다. 이전에 봤듯이 state
가 A
라는 react component 내부에서 사용되고, 동작하는 것을 알 수 있다. 이렇게 state
를 A
react component 내부에서 관리하지 않고, 외부에서 관리할 수 있도록 useReducer
를 사용해 바꾸어보자.
기본적인 useReducer
의 사용방법은 매우 단순하다.
const [state, dispatch] = useReducer(reducer, 0)
useReducer
는 인자로 두 개의 매개변수를 받는데, 첫번째는 reducer함수로 callback함수라고 생각하면 된다. 두번째는 state
의 초기값이다.
useReducer
의 반환값은 두 개인데 첫번째는 state
이고 두번째는 state
를 변경하는 dispatch
함수이다. 단, dispatch
함수는 직접 state
를 바꾸지 않고, useReducer
의 입력으로 받은 reducer
함수를 callback으로 실행한다. 이때 reducer
함수가 실행되어 state
를 변경하도록 우리가 로직을 만들면 된다.
function reducer(state, action) {
...
}
reducer
함수의 매개변수는 state
와 action
이다. state
는 useReducer
로 만든 state
이고 action
은 dispatch
를 실행할 때 입력한 매개변수이다.
dispatch({
type: "INCREATE",
data: 1
})
다음과 같이 dispatch
함수를 실행하였다면 {type: "INCREATE", data: 1}
가 reducer
의 action
으로 들어온다. action
의 형식이 반드시 type
, data
로 고정된 object일 필요는 없지만, 이는 오래전부터 사용해온 redux
에서의 관행이라고 보면 된다.
이제 A.jsx
code를 useState
에서 useReducer
로 변경하여 만들어보도록 하자.
import { useReducer } from "react"
// state, action이 온다.
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) // reducer함수, 초기값
// 값, 상태변화를 발동시키는 trigger함수 -> 단, 상태변화 trigger만 일으키지 reducer함수를 실행시켜준다.
return <div>
<h4>{count}</h4>
<div>
<button onClick={() => {
dispatch({
type: "DECREASE",
data: 1
})
}}>-</button>
<button onClick={() => {
dispatch({
type: "INCREASE",
data: 1
})
}} >+</button>
</div>
</div>
}
동일한 결과를 만들지만, useReducer
를 사용하면 reducer
함수를 B
react component 내부에 사용할 필요가 없어 매우 code가 간결해지고 관리하기 편하다.
react에서의 최적화는 불필요한 연산을 다시 수행하지 않도록 하는 것이 중요하다. 이를 가능하게 해주는 것이 바로 useMemo
이다.
만약 react component 내에서 특정 state
의 변화로 인해 react component가 리렌더링이 발생한다고 하자. 이때 전혀 해당 state
와 관련이 없어 리렌더링이 발생하지 않아도 되는 component들도 있을텐데, 이들의 리렌더링을 막고싶다면 useMemo
를 쓰면딘다.
useMemo
는 말 그대로 메모인데, 첫번째 인자로 실행할 callback함수이고 두번째 인자로 변화를 감지할 변수이다. 즉, 변수에 변화가 감지되면 callback함수를 실행하는 것이다. 만약 두번째 인자로 주어진 변수에 대한 변화가 없다면, 해당 react component 내의 다른 state
가 변화되도 첫번째 callback함수를 실행하지 않는다.
다음은 부모 component들로부터 todos
props를 받아 화면에 렌더링해주는 code이다. 여기서 state
로 search
state를 갖는데, search
state는 input
tag의 value
이기 때문에 input tag에 값을 입력하면, 해당 react component가 계속해서 리렌더링될 것이다.
import { useState, useMemo } from "react"
import TodoItem from "./Todoitem"
import "./TodoList.css"
export default function TodoList({todos, onUpdate, onDelete}) {
const [search, setSearch] = useState("")
const onChangeSearch = (e) => {
setSearch(e.target.value)
}
const filterTodos = () => {
if (search === "") {
return todos
}
return todos.filter((todo) => {
return todo.content.toLowerCase().includes(search.toLowerCase())
})
}
const getAnalyzedTodoData = () => {
console.log("TODO 분석 함수 호출")
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>전체 투두: {totalCount}</div>
<div>완료 투두: {doneCount}</div>
<div>미완 투두: {notDoneCount}</div>
</div>
<input
value={search}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요."/>
<div className="todos_wrapper">
{
filterTodos().map((todo) => {
return <TodoItem key={todo.id} {...todo}
onUpdate={onUpdate}
onDelete={onDelete} />
})
}
</div>
</div>
}
input
tag에 값을 입력하면 search
state가 계속해서 변하기 때문에 리렌더링이 발생하고 getAnalyzedTodoData
가 계속해서 실행된다. 따라서 TODO 분석 함수 호출
가 반복적으로 나타날 것이다.
getAnalyzedTodoData
함수는 사실 search
state와는 별로 상관이 없다. state
가 바뀌어도 리렌더링 될 필요가 없다는 것이다. 오직 props로 주어지는 todos
의 변화에 관련이 된다.
따라서 useMemo
를 사용하여 search
가 바뀌어도 getAnalyzedTodoData
을 호출하지 않도록 하여, 리렌더링이 발생하지 않도록 하자.
const {totalCount, doneCount, notDoneCount} = useMemo(()=> {
return getAnalyzedTodoData()
}, [todos])
사실 별로 어려울 것이 없다. callback함수로 getAnalyzedTodoData
를 실행하고 반환하는데, 이 조건이 todos
props가 변할 때이다. 따라서, input
tag의 search
state가 바뀌어도 getAnalyzedTodoData
가 실행되지 않아, 리렌더링되지 않는다.
이렇게 useMemo
를 통해서, 불필요한 리렌더링을 줄일 수 있다.
다음의 code를 보도록 하자.
function App() {
const [todos, dispatch] = useReducer(reducer, mockData)
...
return (
<div className="App">
<Header/>
<TodoEditor onCreate={onCreate}/>
<TodoList todos={todos}
onUpdate={onUpdate}
onDelete={onDelete}
/>
</div>
)
}
export default App
다음의 code에서 todos
state의 값이 변하면 Header
react component도 리렌더링되게 된다. 이는 원치 않은 리렌더링이 발생하게 되는 것인데, Header
는 todos
state와 상관없는 component이기 때문이다.
그렇다면 react component를 어떻게 최적화하여 불필요한 리렌더링을 막을 수 있을까?? 이를 가능하게 해주는 것이 바로, react
의 memo
이다. memo
는 high order function으로 react component를 입력으로 받아, 최적화된 react component로 반환해준다.
memo
를 쓰게되면 최적화된 react component는 부모로부터 제공받은 props가 변하지 않는다면 리렌더링이 발생하지 않도록 최적화가 이루어진다.
이제, Header
react component를 최적화시켜보도록 하자.
import "./Header.css"
import {memo} from "react"
function Header() {
return <div className="Header">
<h1>{new Date().toDateString()}</h1>
</div>
}
const OptimizedHeaderComponent = memo(Header)
export default OptimizedHeaderComponent
OptimizedHeaderComponent
가 바로 memo
를 통해 최적화된 react component이다.
한가지 조심해야할 것 중 하나는, props
로 전달되는 모든 것들 중 하나라도 바뀌면 memo
로 최적화를 한 react component일지라도 리렌더링이 발생한다는 것이다.
js의 경우 객체, 배열, 함수도 여기에 포함되는데, 함수가 중요하다. 함수 또한, 부모 react component에서 리렌더링이 발생하면 자식 react component가 props로 함수를 받았을 때, props의 주소값이 달라져 리렌더링이 발생한다.
import "./Todoitem.css"
import { memo } from "react"
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)
다음의 예제에서 id
, isDone
, content
와 같은 primitive type 뿐만 아니라, onUpdate
, onDelete
와 같은 함수들도 바뀌면 TodoItem
은 props가 바뀌었기 때문에 리렌더링을 진행한다. 그런데, 함수는 사실 한 번 선언해놓고 잘 바뀌지 않는데, 어떻게 바뀐다는 것일까??
function App() {
const [todos, dispatch] = useReducer(reducer, mockData)
const idRef = useRef(3) // 값이 안변하니까
const onCreate = (content) => {
dispatch({
type: "CREATE",
data: {
id: idRef.current++,
isDone: false,
content: content,
createdDate: new Date().getTime()
}
})
}
const onUpdate = (targetId) => {
dispatch({
type: "UPDATE",
data: targetId
})
}
const onDelete = (targetId) => {
dispatch({
type: "DELETE",
data: targetId
})
}
return (
...
)
}
export default App
App
은 TodoItem
의 부모 react component로 onUpdate
와 onDelete
를 props로 제공해준다. 여기서 만약 App
component의 todos
state가 변한다고 하자. todos
state가 변하면 App
도 리렌더링이 실행되는데 App
이 리렌더링이 되는 순간 onUpdate
와 onDelete
도 다시 재정의된다. 따라서 함수가 레퍼런싱하고 있는 객체가 달라지기 때문에 값이 변했다고 판단한다는 것이다.
이러한 문제 때문에 위의 TodoItem
은 todo
state가 변해도 리렌더링이 되지 않아야 하는데, 함수의 레퍼런스가 변하기 때문에 props
가 변경되었다 판단하고 리렌더링이 발생하는 것이다.
그래서 불필요한 함수 재생성을 막는 useCallback
을 사용해보도록 하자.
useCallback
역시도 react hook이기 때문에 react
를 통해서 import하면 된다.
import { useCallback } from 'react'
사용 방법은 간단하다. 첫번째 인자로 다시 재정의하지 않고싶은 함수를 넣고, 두 번째 인자는 다시 재정의하기 위한 data를 넣으면 된다. 즉 사용 방법은 useEffect
와 완전히 동일하다.
const onUpdate = useCallback((targetId) => {
dispatch({
type: "UPDATE",
data: targetId
})
}, [])
const onDelete = useCallback((targetId) => {
dispatch({
type: "DELETE",
data: targetId
})
}, [])
우리의 경우 onUpdate
, onDelete
가 재정의되는 것을 막고자하기 때문에 useCallback
을 사용하면 된다. 첫번재 인자로 함수를 넣고, 두 번째 인자는 비워주면 된다. 왜냐하면 함수 재정의가 발생되는 것을 절대 원치 않기 때문이다.