React 재활훈련- 6일차, useReducer, useMemo, React.Memo

0

react

목록 보기
6/11

https://www.udemy.com/course/react-next-master/

useReducer

useReduceruseState와 동일하게 새로운 state를 생성하고 업데이트하는 함수를 제공한다. 기본적으로 모두 useState와 동일하지만, useStatestate관리를 오직 react component 내부에서만 가능하다는 것이다.

반면 useReducerstate를 컴포넌트 외부에 state관리 로직 분리가 가능하다. 따라서, 복잡한 state를 관리해야할 때는 useReducer를 통해 사용하는 것이 좋다.

  • A.jsx
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 사용방법이다. 이전에 봤듯이 stateA라는 react component 내부에서 사용되고, 동작하는 것을 알 수 있다. 이렇게 stateA 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함수의 매개변수는 stateaction이다. stateuseReducer로 만든 state이고 actiondispatch를 실행할 때 입력한 매개변수이다.

dispatch({
    type: "INCREATE",
    data: 1
})

다음과 같이 dispatch함수를 실행하였다면 {type: "INCREATE", data: 1}reduceraction으로 들어온다. action의 형식이 반드시 type, data로 고정된 object일 필요는 없지만, 이는 오래전부터 사용해온 redux에서의 관행이라고 보면 된다.

이제 A.jsx code를 useState에서 useReducer로 변경하여 만들어보도록 하자.

  • B.jsx
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가 간결해지고 관리하기 편하다.

useMemo

react에서의 최적화는 불필요한 연산을 다시 수행하지 않도록 하는 것이 중요하다. 이를 가능하게 해주는 것이 바로 useMemo이다.

만약 react component 내에서 특정 state의 변화로 인해 react component가 리렌더링이 발생한다고 하자. 이때 전혀 해당 state와 관련이 없어 리렌더링이 발생하지 않아도 되는 component들도 있을텐데, 이들의 리렌더링을 막고싶다면 useMemo를 쓰면딘다.

useMemo는 말 그대로 메모인데, 첫번째 인자로 실행할 callback함수이고 두번째 인자로 변화를 감지할 변수이다. 즉, 변수에 변화가 감지되면 callback함수를 실행하는 것이다. 만약 두번째 인자로 주어진 변수에 대한 변화가 없다면, 해당 react component 내의 다른 state가 변화되도 첫번째 callback함수를 실행하지 않는다.

다음은 부모 component들로부터 todos props를 받아 화면에 렌더링해주는 code이다. 여기서 statesearch 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를 통해서, 불필요한 리렌더링을 줄일 수 있다.

React.Memo

다음의 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도 리렌더링되게 된다. 이는 원치 않은 리렌더링이 발생하게 되는 것인데, Headertodos state와 상관없는 component이기 때문이다.

그렇다면 react component를 어떻게 최적화하여 불필요한 리렌더링을 막을 수 있을까?? 이를 가능하게 해주는 것이 바로, reactmemo이다. 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

AppTodoItem의 부모 react component로 onUpdateonDelete를 props로 제공해준다. 여기서 만약 App component의 todos state가 변한다고 하자. todos state가 변하면 App도 리렌더링이 실행되는데 App이 리렌더링이 되는 순간 onUpdateonDelete도 다시 재정의된다. 따라서 함수가 레퍼런싱하고 있는 객체가 달라지기 때문에 값이 변했다고 판단한다는 것이다.

이러한 문제 때문에 위의 TodoItemtodo state가 변해도 리렌더링이 되지 않아야 하는데, 함수의 레퍼런스가 변하기 때문에 props가 변경되었다 판단하고 리렌더링이 발생하는 것이다.

그래서 불필요한 함수 재생성을 막는 useCallback을 사용해보도록 하자.

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을 사용하면 된다. 첫번재 인자로 함수를 넣고, 두 번째 인자는 비워주면 된다. 왜냐하면 함수 재정의가 발생되는 것을 절대 원치 않기 때문이다.

0개의 댓글