[모던 리액트 Deep Dive] 03. 리액트 훅 깊게 살펴보기
useState
리액트의 렌더링은 함수형 컴포넌트가 반환한 return의 값을 비교해 실행된다. 매번 렌더링이 발생 될 때마다 함수는 다시 새롭게 실행되고, 새롭게 실행되는 함수에서 변수는 초기화 되기 때문에 내부적으로 클로저를 사용해 지역변수 값을 저장하는 useState를 사용한다.
- 아무런 값을 넘겨주지 않으면 초기값은 undefined
- useState 구현체의 내부에서 index를 클로저로 가둬놔서 이후에도 동일한 Index에 접근해 state 값을 가져올 수 있도록 한다.
- 실제 리액트 코드에는 useState는 useReducer로 구현되어 있다.
- useSate의 게으른 초기화
- useState의 인수로 특정한 값을 넘기는 함수를 인수로 넣을 수 있다.
- 게으른 초기화는 초깃값이 복잡하거나 무거운 연산을 포함할 때 효과적
- 리렌더링이 발생할 때 이 함수의 실행이 무시되기 때문이다.
- useState의 내부에 클로저가 존재하며, 초깃값은 최초에만 사용된다.
- 게으른 초기화를 이용하면 리렌더링이 발생할 때마다 많은 비용을 요구하는 작업을 실행시키지 않는다.
useEffect
useEffect는 컴포넌트의 여러값을 활용해 동기적으로 부수 효과를 만드는 매커니즘 렌더링 시 의존성에 있는 값들을 확인하며 이 의존성의 값이 이전과 다른 것이 하나라도 있다면 부수 효과를 실행하는 함수 useEffect는 state와 props의 변화 속에서 일어나는 렌더링 과정에서 실행되는 부수 효과 함수
-
클린업 함수의 목적
- 일반적으로 이벤트를 등록하고 지울 때 사용해야 된다고 알려져 있다.
- 클린업 함수는 렌더링 뒤에 실행되지만, 렌더링이 실행되고 변경된 값을 읽는 것이 아니라 함수가 정의됐을 당시 선언됐던 이전 값을 바라보고 실행된다.
- useEffect는 콜백이 실행될 때마다 이전의 클린업 함수가 존재하면 그 클린업 함수를 실행하고 콜백을 실행한다.
- 이전에 등록했던 이벤트 핸들러를 삭제하는 코드를 클린업 함수에 추가해서 => 특정 이벤트 핸들러가 무한히 추가 되는 것을 방지
- 클린업 함수는 언마운트 개념이 아닌, 이전 상태를 청소해 주는 개념
-
직접 실행과 차이점
function Component() {
console.log("직접 실행");
}
function Component() {
useEffect(() => {
console.log("useEffect 실행");
}, []);
}
-
직접 실행은 컴포넌트가 렌더링 되는 도중에 실행된다. 함수형 컴포넌트의 반환을 지연시키는 행위이기 때문에 무거운 작업일 경우 렌더링을 방해하므로 성능에 악영향을 미친다.
-
useEffect는 컴포넌트의 렌더링이 완료된 이후에 실행된다.
-
의존성배열
- useEffect의 의존성배열 비교는 이전 값과 현재 값의 얕은 비교
-
useEffect 사용 시 주의점
- useEffect는 의존성 배열로 전달한 값의 변경에 의해 실행돼야 하는 훅
- []과 같은 빈 비열을 의존성으로 할 때, 가급적이면 사용하면 안된다.
- useEffect의 부수 효과가 실제로 관찰해서 실행돼야 하는 값과는 별개로 작동한다는 것을 의미
- 컴포넌트의 state나 props와 같은 값의 변경과 useEffect의 부수 효과가 별개로 작동한다는 뜻이기 때문이다.
- useEffect의 콜백 함수의 실행과 내부에서 사용한 값의 실제 변경 사이에 연결고리가 끊어져 있다는 뜻
// ❌ 버그의 위험성을 안고 있는 코드
// log가 아무리 변하더라도 useEffect의 부수 효과가 일어나지 않는다.
// useEffect의 흐름과 컴포넌트의 props.log의 흐름이 맞지 않는다.
// ⭕ 오히려 부모 컴포넌트에서 logging 작업을 하고
// 부모 컴포넌트에서 Component가 렌더링 되는 시점을 결정해
// 의존성 배열에 log 값을 넘겨는 것이 useEffect의 주석을 제거해도 동일한 결과를 만들 수 있다.
function Component({ log }: { log: string }) {
useEffect(() => {
logging(log)
}, []) // es1int-disab1e-line react-hooks/exhaustive-deps
useMemo, useCallback
-
useMemo
- 값의 메모이제이션
- useMemo로 컴포넌트도 감쌀 수 있지만 React.memo를 쓰는 것을 추천
-
useCallback
- 함수의 메모이제이션
- useCallback은 useMemo를 사용해 구현할 수 있다.
export function useCallback(callback, args) {
currentHook = 8;
return useMemo(() => callback, args);
}
useRef
- useState와 공통점
- 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장
- useState와 차이점
- useRef는 반환값인 객체 내부에 있는 current에 접근, 변경할 수 있다.
- useRef는 그 값이 변하더라도 렌더링을 발생시키지 않는다.
- useRef의 최초 기본 값은 return문에 정의해 둔 DOM이 아닌 useRef()로 넘겨받은 인수
- useRef의 실제 값은
{current: value}
와 같은 객체 형태
// 렌더링에 영향을 미치면 안되기에 의존성 배열에 [] 선언
// => 각 렌더링마다 동일한 객체를 가리키는 결과
export function useRef(initialValue) {
currentHook = 5;
return useMemo(() => ({ current: initialValue }), []);
}
useContext
useContext는 상태 관리를 위한 API가 아닌, 상태를 주입해주는 API 부모 컴포넌트가 렌더링되면 하위 컴포넌트가 모두 리렌더링되기 때문에 context의 provider 하위 컴포넌트의 렌더링을 방지하기 위해서 React.memo를 써야한다.
-
상태관리 API란?
- 어떠한 상태를 기반으로 다른 상태를 만들어 낼 수 있어야 한다.
- 필요에 따라 상태를 최적화할 수 있어야 한다.
-
반면 useContext는?
- 단순히 props 값을 하위로 전달하는 개념
- 사용한다고 해서 렌더링 최적화가 되지 않는다.
useReducer
useReducer는 useState의 심화 버전으로, 복잡한 형태의 state를 사전 정의된 dispatcher로만 수정할 수 있게하여 state에 대한 접근을 제한한다.
import React, { useReducer } from "react";
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</div>
);
}
export default Counter;
useLayoutEffect
리액트는 useLayoutEffect의 실행이 종료될 때까지 기다린 다음에 화면을 그린다. 리액트 컴포넌트는 useLayoutEffect가 완료될 때까지 기다리기 때문에 컴포넌트가 잠시 동안 일시 중지되는 것과 같은 일이 발생하게 된다.
- 리액트가 DOM을 업데이트
- useLayoutEffect 실행
- 브라우저에 변경 사항을 반영
- useEffect를 실행
- DOM은 계산됐지만 화면에 반영되기 전에 진행하고 싶은 작업이 있을 때 사용
- DOM 요소를 기반으로 하는 애니메이션
- 스크롤 위치를 제어
훅 사용 규칙
컴포넌트 최상위에서만 훅을 호출해야한다. 반복문, 조건문, 중첩된 함수 내에서 훅을 실행할 수 없다. 이는 컴포넌트가 렌더링 될 시에 항상 동일한 순서로 훅이 호출되는 것을 보장한다.
- 리액트 훅은 파이버 객체의 링크드 리스트의 호출 순서에 따라 저장
- 고정된 순서에 의존에 훅과 관련된 정보가 저장된다.
- 이에 이전 값에 대한 비교와 실행이 가능
- 만약 조건문이 필요하다면 반드시 훅 내부에서 수행해야한다.
사용자 정의 훅 vs 고차 컴포넌트
공통된 코드를 하나로 만들어 재사용할 수 있는 로직을 관리할 수 있는 두가지 방법
-
사용자 정의 훅
- 서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때 주로 사용되는 것
-
고차 컴포넌트
- 리액트에서 가장 유명한 고차 컴포넌트는 React.memo
- with로 시작하는 이름을 사용하는 것이 관습
-
사용자 정의 훅이 필요한 경우
- 단순히 useEffect, useState와 같이 리액트에서 제공하는 훅으로만 공통 로직을 격리할 수 있는 경우
-
고차 컴포넌트가 필요한 경우
- 함수형 컴포넌트가 반환하는 렌더링 결과물에 영향을 미쳐야 하는 공통 로직