김은정
Study

3장. 리액트 훅 깊게 살펴보기

useState

  • 상태를 정의하고, 이 상태를 관리할 수 있게 해주는 훅

  • 클로저를 이용한 상태 관리

    ← 함수 내부에서 자체적으로 변수를 사용해 상태 값을 관리할 경우, 렌더링이 발생할 때마다 함수가 새로 실행되면서 state도 계속 초기화되기 때문

    ⇒ 함수의 실행이 끝났음에도 함수가 선언된 환경을 기억해, state 값을 유지하고 사용할 수 있음

  • 클로저를 사용함으로써 외부에 해당 값을 노출시키지 않고 오직 리액트에서만 쓸 수 있었고, 함수형 컴포넌트가 매번 실행되더라도 useState에서 이전의 값을 정확히 꺼내 쓸 수 있게 됨

  • 게으른 초기화(lazy initialization) : useState에 변수 대신 함수를 넘기는 것

    • state가 처음 만들어질 때만 사용됨. 이후에 리렌더링이 발생된다면 해당 함수의 실행은 무시됨

      ⇒ useState의 초깃값이 복잡하거나 무거운 연산을 포함하고 있을 때 사용하라

    • 즉, 실행 비용이 많이 드는 경우에 사용하는 것을 권장

useEffect

  • 컴포넌트 렌더링된 후에 어떠한 부수 효과를 일으키고 싶을 때 사용하는 훅

  • state와 props의 변화 속에 일어나는 렌더링 과정에서 실행되는 부수 효과 함수

  • 콜백, 의존성 배열을 인수로 받으며, 두 번째 의존성 배열의 값이 변경되면 첫 번째 인수인 콜백을 실행

    → 의존성 배열의 변경 유무 판단 방법 : 렌더링 시 의존성 배열의 값이 이전과 다른게 하나라도 있으면 부수 효과를 실행

  • 클린업 함수를 통한 정리 작업

    ⇒ 불필요한 이벤트 핸들러(무한히 추가, 이전에 등록한 이벤트 핸들러가 여전히 살아있음)를 방지

    ← 이벤트를 추가하기 전에 이전에 등록했던 이벤트 핸들러를 삭제하는 작업을 진행하기 때문

클린업 함수와 생명주기 메서드의 언마운트

  • 언마운트 : 특정 컴포넌트가 DOM에서 사라진다는 것을 의미하는 클래스형 컴포넌트 용어
  • 클린업 함수 : 함수형 컴포넌트가 리렌더링됐을 때 의존성 변화가 있었을 당시 이전의 값을 기준으로 실행되는, 말 그대로 이전 상태를 청소해주는 개념

의존성 배열이 없는 useEffect가 매 렌더링마다 실행된다면 그냥 useEffect 없이 써도 되는 게 아닐까?

  • useEffect : 클라이언트 사이드에서 실행되는 것을 보장해준다.

  • 직접 실행: 컴포넌트가 렌더링되는 도중에 실행된다. 서버 사이드 렌더링의 경우, 서버에서도 실행된다.

    ⇒ 함수형 컴포넌트의 반환을 지연 ⇒ 성능 악영향

useEffect 코드의 핵심은 의존성 배열의 이전 값과 현재 값의 얕은 비교이다.

  • 리액트 : props에서 꺼내온 값을 기준으로 렌더링함. 값을 비교할 때 Object.is 기반의 얕은 비교를 수행.

    ← 깊은 비교를 진행할 경우, 객체 내의 객체가 몇 개까지 존재하는지 알 수 없기 때문

    ⇒ 재귀적으로 비교해야함

    ⇒ 성능에 악영향

    ⇒ 얕은 비교로 충분!

  • useEffect : state와 props의 변화 속에서 일어나는 렌더링 과정 에서 실행되는 부수효과 함수

⇒ useEffect 훅 자체가 렌더링 과정과 연관이 있기 때문에, useEffect도 얕은 비교를 통해 값을 비교한다.

useEffect 사용 시 주의점

  1. eslint--disable-line react-hooks/exhaustive--deps 주석은 최대한 자제하라

    • useEffect 인수 내부에서 사용하는 값 중 의존성 배열에 포함되지 않은 값이 있을 때 발생하는 경고

    • 의존성 배열을 넘기지 않은 채 콜백 함수 내부에서 특정 값을 사용한다는 것은, 이 부수 효과가 실제로 관찰해서 실행돼야 하는 값과는 별개로 작동한다는 것을 의미

      → 값의 변경과 useEffect의 부수 효과가 별개로 작동하게 됨

    ⇒ useEffect에 빈 배열을 넘길 경우엔, 이게 최선인지 검토하고

    ⇒ 특정 값을 사용하지만 해당 값의 변경 시점을 피할 목적이라면 메모이제이션을 적절히 활용해볼 것을 고민하자

  2. useEffect의 첫 번째 인수에 함수명을 부여하라

    ⇒ 목적을 명확히 하고, 책임을 최소화함

  3. 거대한 useEffect를 만들지 마라

  4. 불필요한 외부 함수를 만들지 마라

useEffect의 경쟁 상태(rare condition)

  • useEffect의 콜백 인수로 비동기 함수를 바로 넣을 경우, 비동기 함수의 응답 속도에 따라 이전 state 기반으로 결과가 나와버리는 불상사가 생길 수 있음.

    ⇒ 비동기 함수를 실행하고자 할 때,

    1. useEffect 내부에서 비동기 함수를 선언해 실행하거나
    2. 즉시 실행 비동기 함수를 만들어 사용
  • 비동기 함수가 내부에 존재한다면, useEffect 내부에서 비동기 함수가 생성되고 실행되는 것을 반복하므로 클린업 함수 처리가 필요함.

⇒ 비동기 useEffect는 state의 경쟁 상태를 야기할 수 있고, cleanup 함수의 실행 순서도 보장할 수 없기 때문에 개발자의 편의를 위해 비동기 함수를 인수로 받지 않는다.

useMemo

  • 값의 메모제이션

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

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

  • 의존성 배열의 값이 변경되지 않았으면 → 함수를 재실행하지 않고 이전에 기억해둔 해당 값을 반환하고

    변경되었다면 → 첫 번째 인수의 함수를 실행한 후에 그 값을 반환하고 그 값을 다시 기억함

  • 비용이 많이 드는 연산에 사용을 권장

useCallback

  • 함수의 메모이제이션

  • 인수로 넘겨 받은 콜백 자체를 기억한다.

    → 특정 함수를 새로 만들지 않고 다시 재사용한다.

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

useRef

  • useState와의 공통점 : 컴포넌트의 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장한다.
  • useState와의 차이점 :
    • useRef는 반환값인 객체 내부에 있는 current로 값에 접근 또는 변경할 수 있다.
    • useRef는 그 값이 변하더라도 렌더링을 발생시키지 않는다.
  • 렌더링을 발생시키지 않고 원하는 상태값을 저장할 수 있는 특징을 활용해 useState의 이전 값을 저장하는 usePrevious() 같은 훅을 구현할 때 유용

useRef를 렌더링에 영향을 미치지 않는 고정된 값을 관리하기 위해 사용한다면, 그냥 함수 외부에서 값을 선언해서 관리하는 것도 동일한 기능을 수행할 수도 있지 않을까?

⇒ 외부에서 값을 선언할 때의 단점(해당 값을 value라고 가정하여 설명)

  1. 컴포넌트가 실행되어 렌더링되지 않았음에도 value라는 값이 기본적으로 존재하게 됨

    ⇒ 메모리에 불필요한 값을 갖게 함

  2. 각 컴포넌트에서 가리키는 값이 모두 value로 동일함

useContext

  • prop 내려주기(props drilling)를 극복하기 위해 등장한 개념

  • 명시적인 props 전달 없이도 선언한 하위 컴포넌트 모두에서 자유롭게 원하는 값을 사용할 수 있음

  • useContext 사용 시, 컴포넌트 재활용이 어려워짐

    ← Provider에 의존성을 가지고 있는 셈이 되기 때문

  • 콘텍스트는 상태를 주입해주는 API이지, 상태 관리를 위한 리액트의 API가 아니다.

    • 상태 관리 라이브러리가 되기 위해서는
      1. 어떠한 상태를 기반으로 다른 상태를 만들어 낼 수 있어야 한다.
      2. 필요에 따라 이러한 상태 변화를 최적화할 수 있어야 한다.
  • 콘텍스트는 단순히 props 값을 하위로 전달해 줄 뿐, useContext를 사용한다고 해서 렌더링이 최적화되지는 않는다.

useReducer

  • state 값을 변경하는 시나리오를 제한적으로 두고 이에 대한 변경을 빠르게 확인할 수 있게끔 한다.

    ⇒ state를 사용하는 로직과 이를 관리하는 비즈니스 로직을 분리할 수 있음

    ⇒ state 관리가 쉬워짐

  • 반환값은 useState와 동일하게 길이가 2인 배열이다.

    • state : 현재 useReducer가 가지고 있는 값
    • dispatcher : state를 업데이트하는 함수
  • useState와 달리 2개에서 3개의 인수를 필요로 한다.

    • reducer : useReducer의 기본 action을 정의하는 함수
    • initialState : 두 번째 인수로, useReducer의 초깃값
    • init : useState의 인수로 함수를 넘겨줄 때처럼 초깃값을 지연해서 생성시키고 싶을 때 사용하는 함수

useImperativeHandle

  • forwardRef : ref를 전달하는 데 있어서 일관성을 제공하기 위해 등장
  • uselmperativeHandle : 부모에게서 넘겨받은 ref를 원하는 대로 수정할 수 있는 혹

useLayoutEffect

  • 이 함수의 시그니처는 useEffect와 동일하나 모든 DOM의 변경 후에 동기적으로 발생

    (여기서 말하는 DOM 변경이란 렌더링을 의미함)

  • 실행 순서

    1. 리액트가 DOM을 업데이트
    2. useLayoutEffect를 실행
    3. 브라우저에 변경 사항을 반영
    4. useEffect를 실행
  • useLayoutEffect가 useEffect보다는 먼저 실행된다.

    ← useLayoutEffect : 브라우저에 변경 사항이 반영되기 전에 실행

    ← useEffect : 브라우저에 변경 사항이 반영된 이후에 실행

  • 리액트는 useLayoutEffect의 실행이 종료될 때까지 기다린 다음에 화면을 그린다.

    ⇒ 즉, 동기적으로 발생함

    ⇒ 성능에 문제가 발생할 수 있음

  • DOM은 계산됐지만 이것이 화면에 반영되기 전에 하고 싶은 작업이 있을 때와 같이 반드시 필요할 때만 사용하는 것이 좋다.

useDebugValue

  • 디버깅하고 싶은 정보를 이 훅에 사용하면 리액트 개발자 도구에서 볼 수 있음
  • 오직 다른 훅 내부에서 실행할 수 있음
  • 공통 훅을 제공하는 라이브러리나 대규모 웹 애플리케이션에서 디버깅 관련 정보를 제공하고 싶을 때 유용하게 사용할 수 있음

훅의 규칙

  1. 최상위에서만 훅을 호출해야한다.

    ← 순서에 아주 큰 영향을 받는 훅이기 때문

    ← 훅에 대한 정보 저장은 리액트 어딘가에 있는 index와 같은 키를 기반으로 구현됨

    ⇒ 예측 불가능한 순서가 아닌, 항상 실행 순서를 보장받을 수 있는 컴포넌트 최상단에 선언돼야함

  2. 훅을 호출할 수 있는 것은 리액트 함수형 컴포넌트, 혹은 사용자 정의 훅의 두 가지 경우 뿐이다.


사용자 정의 훅

  • 서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때 주로 사용
  • 이름이 반드시 use로 시작하는 함수로 만들어야함

사용자 정의 훅이 필요한 경우

  • 리액트에서 제공하는 훅(useEffect, useState 등)으로만 공통 로직을 격리할 수 있을 경우

  • 단순히 컴포넌트의 전반에 걸쳐 동일한 로직으로 값을 제공하거나 특정한 훅의 작동을 취하게 하고 싶을 경우

    ← 고차 컴포넌트는 렌더링에 영향을 미치는 로직이 존재하므로, 사용자 정의 훅에 비해 예측하기 어렵기 때문

고차 컴포넌트

  • HOC, Higher Order Component
  • 컴포넌트 자체의 로직을 재사용하기 위한 방법
  • 다양한 최적화나 중복 로직 관리를 할 수 있음
  • 가장 유명한 고차 컴포넌트는 React.memo

React.memo

  • 리액트 컴포넌트는 자식 컴포넌트의 props 변경 여부와 관계없이 부모 컴포넌트가 새롭게 렌더링될 때 자식 컴포넌트도 렌더링된다.

    ⇒ props의 변화가 없음에도 컴포넌트의 렌더링을 방지하기 위해 만들어진 리액트 고차 컴포넌트

  • 렌더링하기에 앞서 props를 비교해 이전과 props가 같다면 렌더링 자체를 생략하고 이전에 기억해둔(memozation) 컴포넌트를 반환한다.

    ⇒ 불필요한 렌더링 작업 생략 가능 (클래스형 컴포넌트의 PureComponent와 매우 유사)

고차 컴포넌트

  • 고차 함수(함수를 인수로 받거나 반환하는 함수)의 특징에 따라 개발자가 만든 또 다른 함수를 반환할 수 있음

  • 컴포넌트 전체를 감쌀 수 있다는 점에서 사용자 정의 훅보다 더욱 큰 영향력을 컴포넌트에 미칠 수 있다.

    단순히 값을 반환하거나 부수 효과를 실행하는 사용자 정의 훅과는 다르게, 고차 컴포넌트는 컴포넌트의 결과물에 영향을 미칠 수 있는 다른 공통된 작업을 처리할 수 있다.

  • 고차 컴포넌트 생성 시, 부수 효과를 최소해야한다.

    → 고차 컴포넌트는 반드시 컴포넌트를 인수로 받는데, 반드시 컴포넌트의 props를 임의로 수정, 추가, 삭제하는 일이 없어야 한다.

  • 고차 컴포넌트의 사용은 최소한으로

    ← 여러 개의 고차 컴포넌트로 컴포넌트를 감쌀 경우 복잡성이 커지기 때문

고차 컴포넌트를 사용해야 하는 경우

  • 함수형 컴포넌트의 반환값, 즉 렌더링의 결과물에도 영향을 미치는 공통 로직

    ← 사용자 정의 훅은 해당 컴포넌트가 반환하는 렌더링 결과물에까지 영향을 미치기는 어렵기 때문

  • 예시 1. 로그인되지 않은 어떤 사용자가 컴포넌트에 접근하려고 할 때 애플리케이션 관점에서 컴포넌트를 감추고 로그인을 요구하는 공통 컴포넌트를 노출하려고 할 경우

  • 예시 2. 에러 바운더리와 비슷하게 어떠한 특정 에러가 발생했을 때 현재 컴포넌트 대신 에러가 발생했음을 알릴 수 있는 컴포넌트를 노출하는 경우