정리하기 파일
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('렌더링 됨')
})
}
- 서버 사이드 렌더링 관점에서 useEffect는 클라이언트 사이드에서 실행되는 것을 보장해 준다. useEffect 내부에서는 window 객체의 접근에 의존하는 코드를 사용해도 된다.
- useEffect는 컴포넌트 렌더링의 부수 효과, 컴포넌트의 렌더링이 완료된 이후에 실행된다. 반면 직접 실행은 컴포넌트가 렌더링되는 도중에 실행된다.
📍 useEffect를 사용할 때 주의할 점
-
eslint-disable-line react-hooks/exhausitive-deps 주석은 최대한 자제하라
-
useEffect의 첫 번째 인수에 함수명을 부여하라
useEffect의 수가 적거나 복잡성이 낮다면 익명 함수를 사용해도 문제가 없지만, 코드가 복잡해질수록 무슨일을 하는지 파악하기 어려워진다.
-
거대한 useEffect를 만들지 마라
만약 부득이하게 큰 useEffect를 만들어야 한다면 여러 개의 useEffect로 분리하는 것이 좋다.
-
불필요한 외부 함수를 만들지 마라
📍 왜 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를 사용하는 이유는? (함수 외부에서 값을 선언할 경우의 단점)
- 컴포넌트가 실행되어 렌더링되지 않았음에도 value라는 값이 기본적으로 존재하게 된다.
- 만약 컴포넌트가 여러 번 생성되면 컴포넌트에서 가리키는 값이 모두 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의 실행 순서
- 리액트가 DOM을 업데이트
- useLayoutEffect를 실행
- 브라우저에 변경 사항을 반영
- useEffect를 실행
- useLayoutEffect가 브라우저에 변경사항이 반영되기 전에 실행되는 반면 useEffect는 브라우저에 변경 사항이 반영된 이후에 실행된다.
- DOM은 계산됐지만 이것이 화면에 반영되기 전에 하고 싶은 작업이 있을 때 사용한다.
3.1.10 useDebugVlue
- 리액트 애플리케이션을 개발하는 과정에서, 디버깅하고 싶은 정보를 이 훅에다 사용하면 리액트 개발자 도구에서 볼 수 있다.
- 사용자 정의 훅 내부의 내용에 대한 정보를 남길 수 있는 훅이다.
3.1.11 훅의 규칙
리액트에서 제공하는 훅은 사용하는 데 몇 가지 규칙이 존재하는데, 이러한 규칙을 rules-of-hooks라고 한다.
- 최상위에서만 훅을 호출해야 한다. 반복문이나 조건문, 중첩된 함수 내에서 훅을 실행할 수 없다.
- 훅을 호출할 수 있는 것은 리액트 함수형 컴포넌트, 혹은 사용자 정의 훅의 두 가지 경우뿐이다.
3.2 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?
리액트에서는 재사용할 수 있는 로직을 관리할 수 있는 두 가지 방법이 있다.
- 훅(custom hook)
- 고차 컴포넌트(higher order component)
3.2.1 사용자 정의 훅
- 서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때 주로 사용되는 것이 사용자 정의 훅이다.
- 리액트 훅의 이름은 use로 시작한다는 규칙이 있다.
- 사용자 정의 훅의 유명한 저장소로는 use-Hooks (opens in a new tab), react-use (opens in a new tab), ahooks (opens in a new tab) 등이 있다.
3.2.2 고차 컴포넌트
- 고차 컴포넌트는 컴포넌트 자체의 로직을 재사용하기 위한 방법이다.
- 고차 컴포넌트는 고차 함수의 일종으로, 자바스크립트의 일급 객체, 함수의 특징을 이용하므로 리액트 말고도 자바스크립트 환경에서 널리 쓰인다.
- 다양한 최적화나 중복 로직 관리를 할 수 있다.
- 가장 유명한 고차 컴포넌트는 React.memo다.
3.2.3 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?
단순히 useEffect, useState와 같이 리액트에서 제공하는 훅으로만 공통 로직을 격리할 수 있다면 사용자 정의 훅을 사용하는 것이 좋다.
함수형 컴포넌트의 반환값, 즉 렌더링의 결과물에도 영향을 미치는 공통 로직이라면 고차 컴포넌트를 사용하는 것이 좋다.