고도연
Study

정리하기 파일

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

함수형 컴포넌트가 상태를 사용하거나 클래스형 컴포넌트의 생명주기 메서드를 대체하는 등 다양한 작업을 하기 위해 훅이 추가되었다.

3.1 리액트의 모든 훅 파헤치기

클래스형 컴포넌트에서만 가능했던 state, ref 등 핵심 기능을 함수에서도 가능하게 만들었다.

3.1.1 useState

  • 함수형 컴포넌트 내부에서 상태를 정의하고, 이 상태를 관리할 수 있게 해주는 훅이다.
  • 리액트의 렌더링은 함수형 컴포넌트에서 반환한 결과물인 return의 값을 비교해 실행된다. 즉, 매번 렌더링이 발생될 때마다 함수는 다시 새롭게 실행되고, 새롭게 실행되는 함수에서 state는 매번 초기화된다.
  • 함수형 컴포넌트는 매번 함수를 실행해 렌더링이 일어나고, 함수 내부의 값은 함수가 실행될 때마다 다시 초기화된다.

게으른 초기화

  • useState에 변수 대신 함수를 넘기는 것을 게으른 초기화라고 한다.
  • 게으른 초기화는 오로지 state가 처음 만들어질 때만 사용된다. 리렌더링이 발생될 경우 이 함수의 실행은 무시된다.
  • 무거운 연산(localStorage, sessionStorage에 대한 접근, map, filter, find 같은 배열에 대한 접근)을 포함해 비용이 많이 드는 경우에 게으른 초기화를 사용하는 것이 좋다.
// 일반적인 useState 
coonst [count, setCount] = useState(
	Number.parseInt(window.localStorage.getItem(cachekey)),
)
 
// 게으른 초기화 (함수를 실행해 값을 반환)
coonst [count, setCount] = useState(() => 
	Number.parseInt(window.localStorage.getItem(cachekey)),
)

3.1.2 useEffect

  • useEffect는 컴포넌트가 렌더링된 후에 어떠한 부수 효과를 일으키도 싶을 때 사용하는 훅이다.
function Component() {
	// ...
	useEffect(() => {
		// do something
	}, [props, state])
	// ...
}
  • 첫 번째 인수 : 실행할 부수 효과가 포함된 함수
  • 두 번째 인수 : 의존성 배열
    • 의존값 배열이 비어 있으면 최초 마운트 시에만 이펙트를 호출한다.
    • 의존값이 존재하지 않으면 컴포넌트가 렌더링될 때마다 이펙트를 호출한다.
  • 의존성 배열이 변경될 때마다 useEffect의 첫 번째 인수인 콜백을 실행한다

📍 클린업 함수의 목적

  • 일반적으로 클린업 함수는 이벤트를 등록하고 지울 때 사용해야 한다.
  • 이벤트를 추가하기 전에 이번에 등록했던 이벤트 핸들러를 삭제하는 코드를 클린업 함수에 추가한다. 이렇게 함으로써 특정 이벤트의 핸들러가 무한 추가되는 것을 방지할 수 있다.
import React, {useEffect} from 'react'
 
  useEffect(
    (이펙트 함수)
    return {
      (클린업 함수)
    }
  }, [의존값]);
  • 이펙트 함수 호출시점 : 컴포넌트가 처음 마운트될 때, 의존값으로 주어진 값이 변경될 때
  • 클린업 함수 호출시점 : 이펙트 함수가 호출되기 전, 컴포넌트가 언마운트될 때 각각 클린업이 호출된다.

📍 의존성 배열

  • 빈 배열 : 최초 렌더링 직후에 실행된 다음부터는 더 이상 실행되지 않는다.
  • 아무 값도 넘겨주지 않으면 : 렌더링할 때마다 실행
  • 사용자가 원하는 값
// 1
function Component() {
	console.log('렌더링 됨')
}
 
// 2
function Component() {
	useEffect(() => {
		console.log('렌더링 됨')
	})
}
  1. 서버 사이드 렌더링 관점에서 useEffect는 클라이언트 사이드에서 실행되는 것을 보장해 준다. useEffect 내부에서는 window 객체의 접근에 의존하는 코드를 사용해도 된다.
  2. useEffect는 컴포넌트 렌더링의 부수 효과, 컴포넌트의 렌더링이 완료된 이후에 실행된다. 반면 직접 실행은 컴포넌트가 렌더링되는 도중에 실행된다.

📍 useEffect를 사용할 때 주의할 점

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

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

    useEffect의 수가 적거나 복잡성이 낮다면 익명 함수를 사용해도 문제가 없지만, 코드가 복잡해질수록 무슨일을 하는지 파악하기 어려워진다.

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

    만약 부득이하게 큰 useEffect를 만들어야 한다면 여러 개의 useEffect로 분리하는 것이 좋다.

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

📍 왜 useEffect의 콜백 인수로 비동기 함수를 바로 넣을 수 없을까?

useEffect에서 비동기로 함수를 호출할 경우 경쟁 상태가 발생할 수 있기 때문이다.

useEffect 내부에서 비동기 함수를 선언해 실행하거나, 즉시 실행 비동기 함수를 만들어서 사용하는 것은 가능하다.

💡 race condition (경쟁 조건 / 경쟁 상태)
다중 프로세스 환경에서 두 개 이상의 프로세스가 동시에 수행될 때 발생되는 비정상적인 상태를 의미합니다. 동시 수행은 실행 결과의 일관성을 보장하기 어렵습니다.

3.1.3 useMemo

  • useMemo는 비용이 큰 연산에 대한 결과를 저장해두고, 저장된 값을 반환하는 훅이다.
import { useMemo } from 'react'
 
const memoizedValue = useMemo(() => expensiveComputation(a, b), [a, b])
  • 첫 번째 인수 : 어떠한 값을 반환하는 생성 함수
  • 두 번째 인수 : 해당 함수가 의존하는 값의 배열을 전달

3.1.4 useCallback

  • useCallback은 특정 함수를 새로 만들지 않고 재사용한다.
  • 해당 의존성이 변경됐을 때만 함수가 재생성된다. 리렌더링을 방지하고 싶을 때 사용한다.
  • useMemo와 useCallback의 차이는 메모이제이션을 하는 대상이 변수냐 함수냐이다.
  const toggle1 = useCallback(
		// 기명함수를 넣어준 이유 : 크롬 메모리 탭에서 디버깅을 용이하게 하기 위해
    function toggle1() {
      setStatus1(!status1)
    },
    [status1],
  )
  • 첫 번째 인수 : 함수
  • 두 번째 인수 : 의존성 배열

3.1.5 useRef

  • useRef는 useState와 동일하게 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장한다.
  • useRef는 반환값인 객체 내부에 있는 current로 값에 접근 또는 변경할 수 있다.
  • useRef는 그 값이 변하더라도 렌더링을 발생시키지 않는다.
function RefComponent() {
	const count = useRef(0)
 
	function handleClick() {
		count.current += 1
	}
 
	// 버튼을 아무리 눌러도 변경된 count 값이 렌더링되지 않는다.
	return <button onClick={handleClick}>{count.current}</button>
}

🔎 함수 외부에서 값을 선언해서 관리할 수도 있는데 useRef를 사용하는 이유는? (함수 외부에서 값을 선언할 경우의 단점)

  1. 컴포넌트가 실행되어 렌더링되지 않았음에도 value라는 값이 기본적으로 존재하게 된다.
  2. 만약 컴포넌트가 여러 번 생성되면 컴포넌트에서 가리키는 값이 모두 value로 동일하다.
  • 렌더링을 발생시키지 않고 원하는 상태값을 저장할 수 있다는 특징을 활용해 useState의 이전 값을 저장하는 usePrevious() 같은 훅을 구현할 때 사용한다.

3.1.6 useContext

Context란?

prop 내려주기를 극복하기 위해 context 사용한다. 명시적인 props 전달 없이도 선언한 하위 컴포넌트 모두에서 자유롭게 원하는 값을 사용할 수 있다.

💡 prop 내려주기
A 컴포넌트에서 제공하는 데이터를 A > B > C > D 컴포넌트로 필요한 위치까지 계속해서 넘기는 기법

Context를 함수형 컴포넌트에서 사용할 수 있게 해주는 useContext 훅

useContext는 상위 컴포넌트에서 만들어진 Context를 함수형 컴포넌트에서 사용할 수 있도록 만들어진 훅이다.

3.1.7 useReducer

  • useReducer는 useState의 심화 버전이다.
  • 반환값은 useState와 동일하게 길이가 2인 배열이다.
  • useState의 인수와 달리 3개의 인수를 필요로 한다.
    • reducer : useReducer의 기본 action을 정의하는 함수다.
    • initialState : useReducer의 초깃값을 의미한다.
    • init : 초깃값을 지연해서 생성하고 싶을 때 사용하는 함수다. (필수값 X)
  • 목적 : state 값을 변경하는 시나리오를 제한적으로 두고 이에 대한 변경을 빠르게 확인할 수 있게끔 하는 것
  • 복잡한 형태의 state를 사전에 정의된 dispatcher로만 수정할 수 있게 만들어 줌으로써 state 값에 대한 접근을 컴포넌트에서만 가능하게 한다.
  • 여러 개의 state를 관리하는 것보다 비슷한 여러 개의 state를 묶어 useReducer로 관리하는 편이 더 효율적이다.

3.1.8 useImperativeHandle

  • ref의 값에 원하는 값이나 액션을 정의할 수 있다.

3.1.9 useLayoutEffect

  • 이 함수의 시그니처는 useEffect와 동일하나, 모든 DOM의 변경 후에 동기적으로 발생한다.
  • useLayoutEffect의 실행 순서
    1. 리액트가 DOM을 업데이트
    2. useLayoutEffect를 실행
    3. 브라우저에 변경 사항을 반영
    4. useEffect를 실행
  • useLayoutEffect가 브라우저에 변경사항이 반영되기 전에 실행되는 반면 useEffect는 브라우저에 변경 사항이 반영된 이후에 실행된다.
  • DOM은 계산됐지만 이것이 화면에 반영되기 전에 하고 싶은 작업이 있을 때 사용한다.

3.1.10 useDebugVlue

  • 리액트 애플리케이션을 개발하는 과정에서, 디버깅하고 싶은 정보를 이 훅에다 사용하면 리액트 개발자 도구에서 볼 수 있다.
  • 사용자 정의 훅 내부의 내용에 대한 정보를 남길 수 있는 훅이다.

3.1.11 훅의 규칙

리액트에서 제공하는 훅은 사용하는 데 몇 가지 규칙이 존재하는데, 이러한 규칙을 rules-of-hooks라고 한다.

  1. 최상위에서만 훅을 호출해야 한다. 반복문이나 조건문, 중첩된 함수 내에서 훅을 실행할 수 없다.
  2. 훅을 호출할 수 있는 것은 리액트 함수형 컴포넌트, 혹은 사용자 정의 훅의 두 가지 경우뿐이다.

3.2 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?

리액트에서는 재사용할 수 있는 로직을 관리할 수 있는 두 가지 방법이 있다.

  • 훅(custom hook)
  • 고차 컴포넌트(higher order component)

3.2.1 사용자 정의 훅

3.2.2 고차 컴포넌트

  • 고차 컴포넌트는 컴포넌트 자체의 로직을 재사용하기 위한 방법이다.
  • 고차 컴포넌트는 고차 함수의 일종으로, 자바스크립트의 일급 객체, 함수의 특징을 이용하므로 리액트 말고도 자바스크립트 환경에서 널리 쓰인다.
  • 다양한 최적화나 중복 로직 관리를 할 수 있다.
  • 가장 유명한 고차 컴포넌트는 React.memo다.

3.2.3 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?

단순히 useEffect, useState와 같이 리액트에서 제공하는 훅으로만 공통 로직을 격리할 수 있다면 사용자 정의 훅을 사용하는 것이 좋다.

함수형 컴포넌트의 반환값, 즉 렌더링의 결과물에도 영향을 미치는 공통 로직이라면 고차 컴포넌트를 사용하는 것이 좋다.