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 사용 시 주의점
eslint--disable-line react-hooks/exhaustive--deps 주석은 최대한 자제하라
useEffect 인수 내부에서 사용하는 값 중 의존성 배열에 포함되지 않은 값이 있을 때 발생하는 경고
의존성 배열을 넘기지 않은 채 콜백 함수 내부에서 특정 값을 사용한다는 것은, 이 부수 효과가 실제로 관찰해서 실행돼야 하는 값과는 별개로 작동한다는 것을 의미
→ 값의 변경과 useEffect의 부수 효과가 별개로 작동하게 됨
⇒ useEffect에 빈 배열을 넘길 경우엔, 이게 최선인지 검토하고
⇒ 특정 값을 사용하지만 해당 값의 변경 시점을 피할 목적이라면 메모이제이션을 적절히 활용해볼 것을 고민하자
useEffect의 첫 번째 인수에 함수명을 부여하라
⇒ 목적을 명확히 하고, 책임을 최소화함
거대한 useEffect를 만들지 마라
불필요한 외부 함수를 만들지 마라
useEffect의 경쟁 상태(rare condition)
useEffect의 콜백 인수로 비동기 함수를 바로 넣을 경우, 비동기 함수의 응답 속도에 따라 이전 state 기반으로 결과가 나와버리는 불상사가 생길 수 있음.
⇒ 비동기 함수를 실행하고자 할 때,
- useEffect 내부에서 비동기 함수를 선언해 실행하거나
- 즉시 실행 비동기 함수를 만들어 사용
비동기 함수가 내부에 존재한다면, useEffect 내부에서 비동기 함수가 생성되고 실행되는 것을 반복하므로 클린업 함수 처리가 필요함.
⇒ 비동기 useEffect는 state의 경쟁 상태를 야기할 수 있고, cleanup 함수의 실행 순서도 보장할 수 없기 때문에 개발자의 편의를 위해 비동기 함수를 인수로 받지 않는다.
useMemo
-
값의 메모제이션
-
비용이 큰 연산에 대해 결과를 저장(메모제이션)해두고, 이 저장한 값을 반환하는 훅
-
첫 번째 인수로는 어떤 값을 반환하는 생성 함수를, 두 번째 인수로는 해당 함수가 의존하는 값의 배열을 전달
-
의존성 배열의 값이 변경되지 않았으면 → 함수를 재실행하지 않고 이전에 기억해둔 해당 값을 반환하고
변경되었다면 → 첫 번째 인수의 함수를 실행한 후에 그 값을 반환하고 그 값을 다시 기억함
-
비용이 많이 드는 연산에 사용을 권장
useCallback
-
함수의 메모이제이션
-
인수로 넘겨 받은 콜백 자체를 기억한다.
→ 특정 함수를 새로 만들지 않고 다시 재사용한다.
-
함수의 재생성을 막아 불필요한 리소스 또는 리렌더링을 방지하고 싶을 때 사용할 수 있음
useRef
- useState와의 공통점 : 컴포넌트의 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장한다.
- useState와의 차이점 :
- useRef는 반환값인 객체 내부에 있는 current로 값에 접근 또는 변경할 수 있다.
- useRef는 그 값이 변하더라도 렌더링을 발생시키지 않는다.
- 렌더링을 발생시키지 않고 원하는 상태값을 저장할 수 있는 특징을 활용해 useState의 이전 값을 저장하는 usePrevious() 같은 훅을 구현할 때 유용
useRef를 렌더링에 영향을 미치지 않는 고정된 값을 관리하기 위해 사용한다면, 그냥 함수 외부에서 값을 선언해서 관리하는 것도 동일한 기능을 수행할 수도 있지 않을까?
⇒ 외부에서 값을 선언할 때의 단점(해당 값을 value라고 가정하여 설명)
컴포넌트가 실행되어 렌더링되지 않았음에도 value라는 값이 기본적으로 존재하게 됨
⇒ 메모리에 불필요한 값을 갖게 함
각 컴포넌트에서 가리키는 값이 모두 value로 동일함
useContext
-
prop 내려주기(props drilling)를 극복하기 위해 등장한 개념
-
명시적인 props 전달 없이도 선언한 하위 컴포넌트 모두에서 자유롭게 원하는 값을 사용할 수 있음
-
useContext 사용 시, 컴포넌트 재활용이 어려워짐
← Provider에 의존성을 가지고 있는 셈이 되기 때문
-
콘텍스트는 상태를 주입해주는 API이지, 상태 관리를 위한 리액트의 API가 아니다.
- 상태 관리 라이브러리가 되기 위해서는
- 어떠한 상태를 기반으로 다른 상태를 만들어 낼 수 있어야 한다.
- 필요에 따라 이러한 상태 변화를 최적화할 수 있어야 한다.
- 상태 관리 라이브러리가 되기 위해서는
-
콘텍스트는 단순히 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 변경이란 렌더링을 의미함)
-
실행 순서
- 리액트가 DOM을 업데이트
- useLayoutEffect를 실행
- 브라우저에 변경 사항을 반영
- useEffect를 실행
-
useLayoutEffect가 useEffect보다는 먼저 실행된다.
← useLayoutEffect : 브라우저에 변경 사항이 반영되기 전에 실행
← useEffect : 브라우저에 변경 사항이 반영된 이후에 실행
-
리액트는 useLayoutEffect의 실행이 종료될 때까지 기다린 다음에 화면을 그린다.
⇒ 즉, 동기적으로 발생함
⇒ 성능에 문제가 발생할 수 있음
-
DOM은 계산됐지만 이것이 화면에 반영되기 전에 하고 싶은 작업이 있을 때와 같이 반드시 필요할 때만 사용하는 것이 좋다.
useDebugValue
- 디버깅하고 싶은 정보를 이 훅에 사용하면 리액트 개발자 도구에서 볼 수 있음
- 오직 다른 훅 내부에서 실행할 수 있음
- 공통 훅을 제공하는 라이브러리나 대규모 웹 애플리케이션에서 디버깅 관련 정보를 제공하고 싶을 때 유용하게 사용할 수 있음
훅의 규칙
-
최상위에서만 훅을 호출해야한다.
← 순서에 아주 큰 영향을 받는 훅이기 때문
← 훅에 대한 정보 저장은 리액트 어딘가에 있는 index와 같은 키를 기반으로 구현됨
⇒ 예측 불가능한 순서가 아닌, 항상 실행 순서를 보장받을 수 있는 컴포넌트 최상단에 선언돼야함
-
훅을 호출할 수 있는 것은 리액트 함수형 컴포넌트, 혹은 사용자 정의 훅의 두 가지 경우 뿐이다.
사용자 정의 훅
- 서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때 주로 사용
- 이름이 반드시 use로 시작하는 함수로 만들어야함
사용자 정의 훅이 필요한 경우
-
리액트에서 제공하는 훅(useEffect, useState 등)으로만 공통 로직을 격리할 수 있을 경우
-
단순히 컴포넌트의 전반에 걸쳐 동일한 로직으로 값을 제공하거나 특정한 훅의 작동을 취하게 하고 싶을 경우
← 고차 컴포넌트는 렌더링에 영향을 미치는 로직이 존재하므로, 사용자 정의 훅에 비해 예측하기 어렵기 때문
고차 컴포넌트
- HOC, Higher Order Component
- 컴포넌트 자체의 로직을 재사용하기 위한 방법
- 다양한 최적화나 중복 로직 관리를 할 수 있음
- 가장 유명한 고차 컴포넌트는
React.memo
React.memo
-
리액트 컴포넌트는 자식 컴포넌트의 props 변경 여부와 관계없이 부모 컴포넌트가 새롭게 렌더링될 때 자식 컴포넌트도 렌더링된다.
⇒ props의 변화가 없음에도 컴포넌트의 렌더링을 방지하기 위해 만들어진 리액트 고차 컴포넌트
-
렌더링하기에 앞서 props를 비교해 이전과 props가 같다면 렌더링 자체를 생략하고 이전에 기억해둔(memozation) 컴포넌트를 반환한다.
⇒ 불필요한 렌더링 작업 생략 가능 (클래스형 컴포넌트의 PureComponent와 매우 유사)
고차 컴포넌트
-
고차 함수(함수를 인수로 받거나 반환하는 함수)의 특징에 따라 개발자가 만든 또 다른 함수를 반환할 수 있음
-
컴포넌트 전체를 감쌀 수 있다는 점에서 사용자 정의 훅보다 더욱 큰 영향력을 컴포넌트에 미칠 수 있다.
단순히 값을 반환하거나 부수 효과를 실행하는 사용자 정의 훅과는 다르게, 고차 컴포넌트는 컴포넌트의 결과물에 영향을 미칠 수 있는 다른 공통된 작업을 처리할 수 있다.
-
고차 컴포넌트 생성 시, 부수 효과를 최소해야한다.
→ 고차 컴포넌트는 반드시 컴포넌트를 인수로 받는데, 반드시 컴포넌트의 props를 임의로 수정, 추가, 삭제하는 일이 없어야 한다.
-
고차 컴포넌트의 사용은 최소한으로
← 여러 개의 고차 컴포넌트로 컴포넌트를 감쌀 경우 복잡성이 커지기 때문
고차 컴포넌트를 사용해야 하는 경우
-
함수형 컴포넌트의 반환값, 즉 렌더링의 결과물에도 영향을 미치는 공통 로직
← 사용자 정의 훅은 해당 컴포넌트가 반환하는 렌더링 결과물에까지 영향을 미치기는 어렵기 때문
-
예시 1. 로그인되지 않은 어떤 사용자가 컴포넌트에 접근하려고 할 때 애플리케이션 관점에서 컴포넌트를 감추고 로그인을 요구하는 공통 컴포넌트를 노출하려고 할 경우
-
예시 2. 에러 바운더리와 비슷하게 어떠한 특정 에러가 발생했을 때 현재 컴포넌트 대신 에러가 발생했음을 알릴 수 있는 컴포넌트를 노출하는 경우