JiSoo's Devlog

[모던 리액트 Deep Dive] 3장 본문

Frontend/React

[모던 리액트 Deep Dive] 3장

지숭숭숭 2024. 5. 3. 15:00

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

훅은 state, ref 등 리액트의 핵심 기능을 함수에서도 가능하게 만들었고 클래스 컴포넌트를 간결하게 작성할 수 있게 했다

 

useState

함수 컴포넌트 내부에서 상태를 정의하고 이 상태를 관리할 수 있게 해주는 훅

import { useState } from 'react'

const [state, setState] = useState(초깃값)

useState의 인수로는 state의 초깃값을 넘겨준다

아무런 값을 넘겨주지 않으면 초깃값은 undefiend

반환 값은 배열이고 첫 번째 원소로 state 값 자체, 두 번째 원소인 setState 함수로 state의 값을 변경할 수 있다

 

함수 컴포넌트는 매번 함수를 실행해 렌더링이 일어나고 함수 내부의 값은 함수가 실행될 때마다 다시 초기화한다

useState는 클로저에 의존해 구현되어 있기에 외부에 해당 값을 노출시키지 않고 오직 리액트에서만 쓸 수 있고 함수 컴포넌트가 매번 실행되더라도 useState에서 이전의 값을 정확하게 꺼내 쓸 수 있게 되었다

 

useState의 인수로 특정값을 넘기는 함수를 인수로 넣어줄 수도 있다

게으른 초기화

// 일반적인 useState
const [count, setCount] = useStae(
  Number.parseInt(window.localStorage.getItem(cacheKey)),
)

// 게으른 초기화
const [count, setCount] = useState(() =>
  Number.parseint(window.localStorage.getItme(cacheKey)),
)

게으른 초기화 함수는 오로지 state가 처음 만들어질 때만 사용되고 만약 이후 리렌더링이 발생되면 이 함수의 실행은 무시된다

 

localStorage나 sessionStorage에 대한 접근, map, filter, find 같은 배열에 대한 접근, 혹은 초깃값 계산을 위해 함수 호출이 필요할 때와 같이 무거운 연산을 포함해 실행 비용이 많이 드는 경우 게으른 초기화를 사용하는 게 좋다

 

 

useEffect

애플리케이션 내 컴포넌트의 여러 값들을 활용해 동기적으로 부수 효과를 만드는 메커니즘

이 부수 효과가 언제 일어나는지보다 어떤 상태값과 함께 실행되는지 살펴보는 것이 중요

function Component() {
  // ...
  useEffect(() => {
    //..
  }, [props, state])
  //
}

첫 번째 인수로는 실행할 부수 효과가 포함된 함수, 두 번째 인수로는 의존성 배열

의존성 배열은 어느 정도 길이를 가진 배열일 수도, 빈 배열일 수도 있고 생략도 가능하다

의존성 배열이 변경될 때마다 useEffect의 첫 번째 인수인 콜백을 실행한다

useEffect는 렌더링할 때마다 의존성에 있는 값을 보면서 이 의존성 값이 이전과 다른 게 하나라도 있으면 부수 효과를 실행하는 함수

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

 

useEffect 는 클린업 함수라고 불리는데 이벤트를 등록하고 지울 때 사용해야 한다

// 최초 실행
useEffect(() =>{
  function addMouseEvent() {
    console.log(1)
  {
  
  window.addEventListener('click', addMouseEvent)
  
  // 클린업 함수
  // 다음 렌더링이 끝난 뒤 실행
  return () => {
    console.log('클린업 함수 실행', 1)
    window.removeEventListener('click', addMouseEvent)
  }
}, [counter])

// 이후 실행
useEffect(() =>{
  function addMouseEvent() {
    console.log(2)
  {
  
  window.addEventListener('click', addMouseEvent)
  
  // 클린업 함수
  return () => {
    console.log('클린업 함수 실행', 2)
    window.removeEventListener('click', addMouseEvent)
  }
}, [counter])

useEffect는 콜백이 실행될 때마다 이전의 클린업 함수가 존재하면 그 함수를 실행한 뒤에 콜백을 실행한다

따라서 이벤트를 추가하기 전에 이전에 등록했던 이벤트 핸들러를 삭제하는 코드를 클린업 함수에 추가하는 것

→ 특정 이벤트의 핸들러가 무한히 추가되는 것 방지

 

언마운트는 특정 컴포넌트가 DOM에서 사라진다는 것을 의미

클린업 함수는 언마운트라기보다 함수 컴포넌트가 리렌더링 됐을 때 의존성 변화가 있었을 당시 이전 값을 기준으로 실행되는, 이전 상태를 청소해 준다

 

의존성 배열

보통 빈 배열을 두거나 아무런 값도 넘기지 않거나 사용자가 원하는 값을 넣어줄 수 있다

빈 배열을 둔다면 비교할 의존성이 없다고 판단해 최초 렌더링 직후에 실행된 다음부터는 더 이상 실행 X

아무 값도 넘겨주지 않는다면 의존성 비교할 필요 없이 렌더링할 때마다 실행이 필요하다고 판단해 렌더링이 발생할 때마다 실행되는데 이건 보통 컴포넌트가 렌더링 됐는지 확인하기 위한 방법으로 사용

 

// 1
function Component() {
  console.log('렌더링됨')
}

// 2
function Component() {
  useEffect(() =>{
    console.log('렌더링됨')
  })
}

1번에서처럼 함수 내부에서의 직접 실행은 컴포넌트가 렌더링되는 도중에 실행된다

2번과 달리 서버 사이드 렌더링의 경우 서버에서도 실행되고 이 작업은 함수 컴포넌트의 반환을 지연시킨다

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

useEffect  내부에서는 window 객체의 접근에 의존하는 코드를 사용해도 된다

 

useEffect의 effect는 컴포넌트의 사이드 이펙트, 즉 부수 효과를 의미한다!

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

이전 의존성 배열과 현재 의존성 배열의 값에 하나라도 변경 사항이 있다면 콜백으로 선언한 부수 효과를 실행한다

 

useEffect를 잘못 사용하면 예기치 못한 버그가 발생할 수 있고 무한 루프에 빠지기도 한다

 

useEffect 사용할 때 주의할 점

eslint-disable-line react-hooks/exhaustive-deps 주석 최대한 자제하기

ESLint 룰은 useEffect 인수 내에서 사용하는 값 중 의존성 배열에 포함돼 있지 않은 값이 있을 때 경고를 발생시킨다

useEffect(() => {
  console.log(props)
}, []) // eslint-disable-line react-hooks/exhaustive-deps

빈 배열을 의존성으로 하는 건 클래스 컴포넌트의 생명주기 메서드인 componentDidMount에 기반한 접근법으로 가급적 사용하면 안 된다

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

정말 의존성으로 []가 필요하다면 최초에 함수 컴포넌트가 마운트됐을 시점에만 콜백 함수 실행이 필요한지를 다시 한번 물어봐야 한다

빈 배열이 아니더라도 만약 특정 값을 사용하지만 해당 값의 변경 시점을 피할 목적이라면 메모이제이션을 적절히 활용해 해당 값의 변화를 막거나 적당한 실행 위치를 다시 고민해 보는 것이 좋다

 

useEffect의 첫 번째 인수에 함수명 부여하기

useEffect의 코드가 복잡하고 많아질수록 무슨 일을 하는 코드인지 파악하지 어려워지기 때문에 익명 함수가 아닌 기명 함수로 바꾸는 게 좋다

useEffect(
  function logActiveUser() {
    logging(user.id)
  },
  [user.id],
)

 

거대한 useEffect 만들지 말기

useEffect는 의존성 배열을 바탕으로 렌더링 시 의존성이 바뀔 때마다 부수 효과를 실행하는데 이 부수 효과가 커질수록 성능에 악영향을 미친다가능한 한 useEffect는 간결하고 가볍게 유지하는 게 좋다

 

불필요한 외부 함수 만들지 않기

useEffect가 실행하는 콜백 또한 불필요하게 존재해서는 안 된다

 

 

useMemo

리액트에서 최적화를 떠올릴 때 가장 먼저 언급되는 훅

비용이 큰 연산에 대한 결과를 저장해두고 이 저장된 값을 반환하는 훅

import { useMemo } from 'react'

const memoized = useMemo(() => expensiveComputation(a, b), [a, b])

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

메모이제이션을 활용하면 무거운 연산을 다시 수행하는 것을 막을 수 있다

 

useCallback

특정 함수를 새로 만들지 않고 다시 재사용한다는 의미

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

첫 번째 인수로 함수, 두 번째 인수로 의존성 배열을 집어넣으면 useMemo와 마찬가지로 의존성 배열이 변경되지 않는 한 함수를 재생성하지 않는다

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

기본적으로 useCallback은 useMemo를 사용해 구현 가능하다

export function useCallback(callback, args) {
  currentHook = 8
  return useMemo(() => callback, args)
}

useMemo와의 유일한 차이는 메모이제이션하는 대상이 변수냐 함수냐일 뿐

 

 

useRef

useState와 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장한다는 공통점이 있다

useRef는 반환값인 객체 내부에 있는 current로 값에 접근 또는 변경할 수 있다

useRef는 그 값이 변해도 렌더링을 발생시키지 않는다

컴포넌트가 렌더링될 때만 생성되며 컴포넌트 인스턴스가 여러 개라도 각각 별개의 값을 바라본다

useRef를 사용할 수 있는 유용한 경우는 렌더링을 발생시키지 않고 원하는 상태값을 저장할 수 있다는 특징을 활용해 useState의 이전 값을 저장하는 훅을 구현할 때이다

개발자가 원하는 시점의 값을 렌더링에 영향을 미치지 않고 보관해 두고 싶을 때 사용하는 게 좋다

 

 

Context

부모가 가진 데이터를 자식에서도 사용하고 싶어 props로 데이터를 넘겨주는 것을 prop 내려주기라고 한다

prop 내려주기는 해당 데이터를 제공하는 쪽, 사용하는 쪽 모두 불편하다

이걸 극복하기 위해 Context가 등장했고 명시적인 props 전달 없이도 선언한 하위 컴포넌트 모두에게 원하는 값을 사용할 수 있다

 

useContext

상위 컴포넌트에서 만들어진 Context를 함수 컴포넌트에서 사용할 수 있도록 만들어진 훅

다수의 Provider와 useContext를 사용할 때 별도 함수로 감싸서 사용하는 것이 좋다

 

주의할 점

useContext를 함수 컴포넌트 내부에서 사용할 때는 항상 컴포넌트 재활용이 어려워진다는 점을 염두해둬야 한다

useContext를 사용하는 컴포넌트를 최대한 작게 하거나 재사용되지 않을 만한 컴포넌트에서 사용해야 한다

콘택스트는 단순히 상태를 주입할 뿐 그 이상의 기능도, 그 이하의 기능도 하지 않는다

 

 

useReducer

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

반환값을 useState와 동일하게 길이가 2인 배열

◦ state : 현재 useReducer이 가지고 있는 값

◦ dispatcher : state를 업데이트하는 함수로 단순히 값을 넘겨주는 게 아니라 state를 변경할 수 있는 action을 넘겨준다

 

3개의 인수가 필요하다

◦ reducer : 기본 action을 정의하는 함수

initialState : useReducer의 초깃값

◦ init : 초깃값을 지연해서 생성시키고 싶을 때 사용하는 함수, 필수 X

 

 

userImperativeHandle

ref는 useRef에서 반환한 객체forwardRef를 사용하면 ref를 props로 전달할 수 있고 전달받은 컴포넌트에서도 ref라는 이름을 그대로 사용 가능useImperativeHandle은 부모에게서 넘겨받은 ref를 원하는 대로 수정할 수 있는 훅

 

 

useLayoutEffect

useEffect와 형태나 사용 예제가 동일하지만 모든 DOM의 변경 후에 동기적으로 발생한다

DOM의 변경은 렌더링이지 브라우저에 실제로 변경 사항이 반영되는 시점을 의미하는 것은 아니다

리액트가 DOM 업데이트 → useLayoutEffect 실행 브라우저에 변경 사항 반영 useEffect 실행

useLayoutEffect가 브라우저에 변경 사항이 반영되기 전에 실행되는 반면 useEffect는 브라우저에 변경 사항이 반영된 이후에 실행된다

DOM은 계산됐지만 이게 화면에 반영되기 전에 하고 싶은 작업이 있을 때 사용하는 게 좋다

 

 

useDebugValue

디버깅하고 싶은 정보를 이 훅에 사용하면 리액트 개발자 도구에서 볼 수 있다

두 번째 인수로 포매팅 함수를 전달하면 이에 대한 값이 변경됐을 때만 호출되어 포매팅된 값을 노출한다

useDebugValue를 사용할 때는 오직 다른 훅 내부에서만 실행할 수 있음에 주의해야 한다

공통 훅을 제공하는 라이브러리나 대규모 웹 애플리케이션에서 디버깅 관련 정보를 제공하고 싶을 때 유용하게 사용할 수 있다

 

 

훅의 규칙(rules-of-hooks)

1. 최상위에서만 훅을 호출해야 한다. 중첩된 함수 내에서 훅을 실행할 수 없다. 이 규칙을 따라야만 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 훅이 호출되는 것을 보장할 수 있다

2. 훅을 호출할 수 있는 것은 리액트 함수 컴포넌트, 혹은 사용자 정의 훅의 두 가지 경우뿐이다. 일단 자바스크립트 함수에서는 훅을 사용할 수 없다

 

훅은 절대 조건문, 반복문 등에 의해 리액트에서 예측 불가능한 순서로 실행되게 해서는 안 된다

항상 실행 순서를 보장받을 수 있는 컴포넌트 최상단에 선언돼 있어야 하고 조건문이 필요하다면 반드시 훅 내부에서 수행해야 한다

 


 

재사용 로직 관리 방법 → 사용자 정의 훅, 고차 컴포넌트

 

사용자 정의 훅

서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때 주로 사용된다

리액트에서만 사용할 수 있는 방식

리액트 훅의 이름은 use로 시작한다는 규칙이 있다

 

고차 컴포넌트

컴포넌트 자체의 로직을 재사용하기 위한 방법

고차 함수의 일종으로 자바스크립트의 일급 객체, 함수의 특징을 이용하므로 자바스크립트 환경에서 널리 사용 가능

가장 유명한 고차 컴포넌트는 React.memo

 

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

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

 

고차 함수

함수를 인수로 받거나 결과로 반환하는 함수

ex) Array.prototype.map

const list = [1, 2, 3]
const doubledList = list.map((item) => item * 2)

map을 비롯해 forEach나 reduce도 고차 함수

고차 함수를 활용하면 함수를 인수로 받거나 새로운 함수를 반환해 완전히 새로운 결과를 만들어 낼 수 있다

 

고차 컴포넌트

컴포넌트 전체를 감쌀 수 있기 때문에 사용자 정의 훅보다 더욱 큰 영향력을 컴포넌트에 미칠 수 있다

with로 시작하는 이름을 사용해야 한다

사용시 주의할 점은 부수 효과를 최소화해야 한다는 것이다

 

 

사용자 정의 훅을 사용해야 하는 경우

단순히 useEffect, useState와 같은 훅으로만 공통 로직을 격리할 수 있을 때 사용하는 게 좋다

사용자 정의 훅 자체로는 렌더링에 영향을 미치지 못하기 때문에 사용이 제한적이므로 반환값을 바탕으로 무엇을 할지는 개발자에게 달렸다

컴포넌트 내부에 미치는 영향을 최소화해 개발자가 훅을 원하는 방향으로만 사용할 수 있다는 장점이 있다

컴포넌트 전반에 걸쳐 동일한 로직으로 값을 제공하거나 특정 훅의 작동을 취하게 하고 싶다면 사용하는 게 좋다

 

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

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

공통화된 렌더링 로직을 처리하기에 매우 훌륭한 방법

고차 컴포넌트가 많아질수록 복잡성이 기하급수적으로 증가하기 때문에 신중하게 사용해야 한다

 

 

728x90

'Frontend > React' 카테고리의 다른 글

[모던 리액트 Deep Dive] 5장  (0) 2024.05.18
[모던 리액트 Deep Dive] 4장  (1) 2024.05.09
[모던 리액트 Deep Dive] 2장  (2) 2024.04.27
[모던 리액트 Deep Dive] 1장  (0) 2024.04.08
[React JS 마스터클래스] State Management  (1) 2024.03.22