모던 리액트 Deep Dive 스터디

리액트 훅 깊게 살펴보기(useState, useEffect, useMemo, useCallback)

려낭 2024. 11. 12. 23:34

리액트 함수 컴포넌트에서 가장 중요한 개념이 바로 훅이다.

리액트 훅에 대해 알아보자.

 

3.1.1 useState

 

useState

함수 컴포넌트 내부에서 상태를 정의하고, 이 상태를 관리할 수 있게 해주는 훅

 

useState 구현 살펴보기

 

기본적인 사용법

import (useState } from 'react'

const [state , setState ] = useState(initialState)

 

useState의 인수로는 사용할 state의 초깃값을 넘겨준다. > 아무런 값을 넘겨주지 않으면 초깃값은 undefined

useState 훅의 반환 값은 배열이다.

배열의 첫 번째 원소로 state 값을 사용할 수 있고 , 두번째 원소인 setState 함수로 state값을 변경할 수 있다.

 

 

useState 내부의 모습을 구현한 모습

const MyReact = (function () {
  const global = {}
  let index = 0

  function useState(initialState) {
    if (!global.states) {
      // 애플리케이션 전체의 states 배열을 초기화한다.
      // 최초 접근이라면 빈 배열로 초기화한다.
      global.states = []
    }

    // states 정보를 조회해서 현재 상태값이 있는지 확인하고,
    // 없다면 초기값으로 설정한다.
    const currentState = global.states[index] || initialState
    // states의 값을 위에서 조회한 현재 값으로 업데이트한다.
    global.states[index] = currentState

    // 즉시 실행 함수로 setter를 만든다.
    const setState = (function () {
      // 현재 index를 클로저로 가둬서 이후에도 계속해서 동일한 index에
      // 접근할 수 있도록 한다.
      let currentIndex = index
      return function (value) {
        global.states[currentIndex] = value
        // 컴포넌트를 렌더링한다. 실제로 컴포넌트를 렌더링하는 코드는 생략했다.
      }
    })()

    // useState를 쓸 때마다 index를 하나씩 추가한다. 이 index는 setState에서 사용된다.
    // 즉, 하나의 state마다 index가 할당돼 있어 그 index가 배열의 값(global.states)을
    // 가리키고 필요할 때마다 그 값을 가져오게 한다.
    index = index + 1

    return [currentState, setState]
  }

  // 실제 useState를 사용하는 컴포넌트
  function Component() {
    const [value, setValue] = useState(0)
    // ...
  }
})

 

함수의 실행이 끝났음에도 함수가 선언된 환경을 기억할 수 있는 방법 >> 1장에서 소개한 클로저이다.

매번 실행되는 함수 컴포넌트 환경에서 state 값을 유지하고 사용하기 위해 클로저를 활용한다.

 

 

게으른 초기화

 

useState에 변수 대신 함수를 넘기는 것을 게으른 초기화(lazy initialization)라고 한다.

 

//일반적인 useState 사용
//바로 값을 집어넣는다.
const [count, setCount ] = useState(
	Number.parseInt(window.localStorage.getItem(cacheKey)),
)

//게으른 초기화
//위 코드와 차이점 : 함수를 실행해 값을 반환한다.
const [count, setCount ] = useState(() => 
	Number.parseInt(window.localStorage.getItem(cacheKey)),
)

 

리액트 공식 문서에는  이러한 게으른 초기화는 useState의 초깃값이 복잡하거나 무거운 연산을 포함하고 있을 때 사용하라고 한다.

게으른 초기화 함수는 state가 처음 만들어질 때만 사용된다.

이후에 리렌더링이 발생하면 이 함수의 실행은 무시된다.

 

useState 내부에 함수를 넣으면 최초 렌더링 이후에는 실행되지 않고 state 값을 넣을 때만 실행된다.

 

 

3.1.2 useEffect

 

  • 두 개의 인수를 받는데, 첫 번째는 콜백, 두 번째는 의존성 배열이다. 이 두 번째 의존성 배열의 값이 변경되면 첫 번째 인수인 콜백을 실행한다.
  • 클래스 컴포넌트의 생명주기 메서드와 비슷한 작동을 구현할 수 있다. 두 번째 의존성 배열에 빈 배열을 넣으면 컴포넌트가 마운트 될 때만 실행된다.
  • 클린업 함수를 반환할 수 있는데, 이 클린업 함수는 컴포넌트가 언마운트될 때 실행된다.

 

useEffect 의 정확한 정의는 

애플리케이션 내 컴포넌트의 여러 값들을 활용해 동기적으로 부수 효과를 만드는 메커니즘이다.

이 부수효과가 언제 일어나는지보다 어떤 상태값과 함께 실행되는지 살펴보는 것이 중요하다.

 

useEffect의 일반적인 형태

function Component() {
//...
useEffect(() => {
//do something
}, [props, state])
//...
}

 

첫 번째 인수로는 실행할 부수 효과가 포함된 함수를, 두 번째 인수로는 의존성 배열을 전달한다.

 

 

useEffect는 어떻게 의존성 배열이 변경된 것을 알고 실행될까?

 

function Component() {
	const [counter, setCounter] = useState(0)
    
    function handleClick() {
    	setCounter((prev) => prev + 1)
    }
    
    return (
    	<>
        <h1>{counter}</h1>
        <button onClick={handleClick}>+<button>
        </>
      )
   )

 

버튼을 클릭하면 counter에 값을 1씩 올리는 평범한 컴포넌트다.

함수 컴포넌트는 렌더링 시마다 고유의 state와 props 값을 갖고 있다.

여기에 useEffect가 추가된다면

 

function Component() {
	const [counter, setCounter] = useState(0)
    
    useEffect(() => {
    	console.log(counter) //1,2,3,4
    }
    
    return (
    	<>
        <h1>{counter}</h1>
        <button onClick={handleClick}>+<button>
        </>
      )
   )

 

useEffect는 렌더링할 때마다 의존성에 있는 값을 보면서

이 의존성의 값이 이전과 다른 게 하나라도 있으면 부수 효과를 실행하는 것이다.

 

 

 

클린업 함수의 목적

 

클린업 함수는 무엇이고 어떤 일을 할까?

클린업 함수는 보통 이벤트를 등록하고 지울 때 사용한다고 알려져 있다.

 

import { useState, useEffect } from 'react'

export default function App() {
  const [counter, setCounter] = useState(0)

  function handleClick() {
    setCounter((prev) => prev + 1)
  }

  useEffect(() => {
    function addMouseEvent() {
      console.log(counter)
    }

    window.addEventListener('click', addMouseEvent)

    // 클린업 함수
    return () => {
      console.log('클린업 함수 실행!', counter)
      window.removeEventListener('click', addMouseEvent)
    }
  }, [counter])

  return (
    <>
      <h1>{counter}</h1>
      <button onClick={handleClick}>+</button>
    </>
  )
}


//실행결과
클린업 함수 실행! 0
1

클린업 함수 실행! 1
2

클린업 함수 실행! 2
3

클린업 함수 실행! 2
4

 

클린업 함수는 이전 counter 값, 즉 이전 state를 참조해 실행된다는 것을 알 수 있다.

클린업 함수는 새로운 값과 함께 렌더링된 뒤에 실행된다.

 

useEffect는 콜백이 실행될 때마다 이전의 클린업 함수가 존재한다면 그 클린업 함수를 실행한 뒤에 콜백을 실행한다.

이벤트를 추가하기 전에 등록했던 이벤트 핸들러는 삭제하는 코드를 클린업 함수에 추가해 특정 이벤트 핸들러가 무한히 추가되는 것을 방지한다.

 

'언마운트' 특정 컴포넌트가 DOM에서 사라지는 것

 

클린업 함수는 언마운트라기 보다는 함수 컴포넌트가 리렌더링됐을 때 의존성 변화 이전의 값을 기준으로 실행되는,

즉 이전 상태를 청소해주는 개념으로 보는 것이 옳다.

 

 

의존성 배열

 

의존성 배열은 빈 배열, 아무런 값도 넘기지 않거나, 원하는 값을 넣어줄 수 있다.

 

빈 배열 >> 최초 렌더링때 실행된 이후 실행되지 않는다.

아무런 값 x >> 렌더링 할 때 마다 실행

원하는 값 >> 값이 바뀔 때 마다 실행

 

그렇다면 의존성 배열에 아무런 값이 없는 useEffect가 매 렌더링마다 실행된다면

useEffect 없이 써도 되는 게 아닐까? 라고 생각할 수 있다.

 

useEffect는 컴포넌트의 렌더링이 완료된 이후에 실행되지만   함수 내부의 직접 실행은 컴포넌트가 렌더링 되는 도중에 실행되고, 서버 사이드 렌더링의 경우에는 서버에세도 실행된다.

 

무거운 작업일 경우에는 렌더링을 방해하기 때문에 성능에 악영향을 미칠 수 있다.

 

 

useEffect를 사용할 때 주의할 점

 

  • eslint-disable-line react-hooks/exhaustive-deps 주석은 최대한 자제하라
  • useEffect의 첫 번째 인수에 함수명을 부여하라
  • 거대한 useEffect를 만들지 마라
  • 불필요한 외부 함수를 만들지 마라

 

3.1.3 useMemo

 

비용이 큰 연산에 대한 결과를 저장(메모이제이션)해 두고, 저장된 값을 반환하는 훅.

흔히 리액트에서 최적화를 할 때 가장 먼저 언급되는 훅이다.

 

import { useMemo } from 'react'

const memoizedValue = useMemo(() => expensiveComputation(a,b), [a,b])

 

첫 번째 인수로는 어떠한 값을 반환하는 생성함수를 , 두 번째 인수로는 해당 함수가 의존하는 값의 배열을 전달한다.

렌더링 발생 시 의존성 배열의 값이 변경되지 않았으면 > 함수 재실행 x, 이전에 기억해 둔 값을 반환

의존성 배열의 값이 변경 > 첫 번째 인수의 함수를 실행한 후 그 값을 반환하고 그 값을 다시 기억한다.

 

 

3.1.4 useCallback

 

useMemo가 값을 기억했다면, useCallback은 인수로 넘겨받은 콜백 자체를 기억한다.

즉, 특정 함수를 새로 만들지 않고 다시 재사용한다는 의미다.

 

useCallback 의 첫 번째 인수로 함수를, 두 번째 인수로 의존성 배열을 집어 넣으면 useMemo와 마찬가지로 의존성 배열이 변경되지 않는 한 함수를 재생성하지 않는다.

 

함수의 재생성을 막아 불필요한 리소스 또는 리렌더링을 방지하고 싶을 때 useCallback을 사용할 수 있다.

 

useCallback을 useMemo로도 구현할 수 있지만 불필요하게 코드가 길어지고 혼동을 야기할 수 있어 별도로 제공하는 것으로 추측된다.

하지만 사실상 동일한 역할을 한다.