본문 바로가기
React

[번역] useEffect 단순화

by Vintz 2022. 9. 12.
반응형

Photo by Christopher Burns

 

원문 : https://tkdodo.eu/blog/simplifying-use-effect

글에서 오타나 오역을 발견하신 경우, 댓글로 알려주시면 감사하겠습니다. 의역이 포함되어 있습니다. :D

 

useEffect. 모두에게 필요한 훅이지만 누구도 원하지 않습니다. 리액트 공식 문서에 따르면 이것은 "리액트의 순수 함수형 세계에서 명령형 세계로 탈출하는 창구"입니다. 리덕스 원작자이자 리액트의 핵심 팀원인 댄 아브라모프(Dan Abramov)가 작성한 useEffect 완벽 가이드는 49분 분량이며, 실제로 이해하는 데 최소 두 배의 시간이 걸립니다.

 

useEffect는 리액트에서 얻을 수 있는 만큼 복잡하고, 이것 없이 애플리케이션을 작성할 수 있을 가능성은 거의 없습니다. 따라서 useEffect 작업을 보다 쉽게 관리할 수 있도록 몇 가지 좋은 원칙을 적용해 보겠습니다.

1. 적은 수의 이펙트(effect) 쓰기

저는 이미 useState 함정 시리즈에서 이펙트의 양을 줄이는 몇 가지 방법에 대해 글을 썼습니다. 

  • 파트 1에서, 일부 이펙트는 useMemo 또는 일반 함수 실행조차 대체가 될 수 있음을 확인했습니다.
  • 파트 2에서, useEffect를 사용하여 다른 상태를 동기화하는 것이 안티패턴일 가능성이 높은 이유와 대신 할 수 있는 일에 대해 설명을 했습니다.

데이터 페칭(fetching)

데이터 페칭은 useEffect로 관리되는 일반적인 사이드 이펙트입니다. 결국 대부분의 앱은 어딘가에서 데이터를 가져와야 합니다. 이 시나리오는 매우 일반적이어서 복잡한 로직을 보다 선언적으로 만드는 데 도움이 될 뿐만 아니라, 훌륭한 추가 기능들을 많이 제공하는 아주 좋은 라이브러리가 있습니다.

 

저는 분명히 제가 가장 좋아하는 오픈소스 라이브러리인 react-query를 추천할 것입니다.(이것을 언급하지 않고 다른 글을 쓸 수 있을지 의문이네요. 😅) 하지만 SWR, Apollo 그리고 RTK-Query 또한 훌륭합니다. 요점은 다음과 같습니다. 바퀴를 재발명하려고 하지 마세요. 일부 문제는 이전에 해결되었으며 추상화할 가치가 있습니다. 제가 react-query를 사용하고 있기 때문에 작성해야 했던 useEffect의 양이 크게 줄어들었습니다.

2. 단일 책임 원칙(Single-responsibility principle) 따르기

함수 또는 클래스는 하나에 한 가지만 수행해야 합니다. processPayment 함수는 결제 처리 기능 외에 추가로 사용자를 다른 곳으로 이동시키지(redirecting) 않기를 바랍니다. 이는 해당 함수의 책임이 아니기 때문입니다. useEffect에 전달하는 함수에도 동일한 원칙이 적용됩니다. 하나의 useEffect에 모든 것을 넣을 필요가 없습니다.

// 하나의 이펙트에 두 가지 책임이 있습니다.
React.useEffect(() => {
  document.title = 'hello world'
  trackPageVisit()
}, [])

여기서 우리는 컴포넌트가 '마운트'될 때 문서의 타이틀을 설정하고, 약간의 분석 도구를 사용하여 페이지 방문을 추적하는 것과 같은 작업을 수행하려고 합니다. 언뜻 보기에는 사소한 것처럼 보일 수도 있지만 우리는 하나의 이펙트에서 매우 다른 두 가지의 일을 하고 있으며 , 이것은 쉽게 두 가지의 이펙트로 나눌 수 있습니다. 이에 따른 이점은 이펙트의 의존성이 변경됨에 따라 더욱 분명해집니다.

 

이제 지역 상태를 문서 타이틀과 동기화하는 기능을 추가하려고 합니다.

// 타이틀을 동기화합니다.
const [title, setTitle] = React.useState('hello world')

React.useEffect(() => {
  document.title = title
  trackPageVisit()
}, [title])

버그를 발견 할 수 있나요? 타이틀이 변경될 때마다 페이지 방문도 추적하고 있습니다. 이는 아마도 우리가 의도한 것과 다를 것입니다. 두 개의 이펙트로 나누면 문제가 해결되며, 저는 처음부터 우리가 이렇게 했어야 한다고 생각합니다.

// 단일 책임 원칙을 따릅니다.
const [title, setTitle] = React.useState('hello world')

React.useEffect(() => {
  document.title = title
}, [title])

React.useEffect(() => {
  trackPageVisit()
}, [])

코드는 이제 버그가 줄어들 뿐만 아니라 추론하기도 더 쉽습니다. 이펙트의 크기가 절반으로 줄어들기 때문에 각 이펙트를 개별적으로 살펴보며 이펙트를 보다 정확하게 파악할 수 있습니다.

3. 커스텀 훅 쓰기

저는 코드의 50%가 훅을 호출하는 컴포넌트를 좋아하지 않습니다. 이것은 일반적으로 로직과 마크업을 혼합하고 있음을 보여줍니다. 커스텀 훅에 넣는 것은 분명히 '재사용성'을 제외하고도 여러 가지 이점이 있습니다.

이름을 지을 수 있습니다

변수와 함수에 적절한 이름을 지어주는 것은 문서를 작성하는 것과 같으며 훅에도 동일하게 적용됩니다. 타입스크립트를 사용하는 경우 명확하게 정의된 인터페이스의 이점도 얻을 수 있습니다.

// 이름을 가진 훅들
const useTitleSync = (title: string) => {
  React.useEffect(() => {
    document.title = title
  }, [title])
}

const useTrackVisit = () => {
  React.useEffect(() => {
    trackPageVisit()
  }, [])
}

이제 모든 이펙트가 설명이 포함된 이름을 가진 커스텀 훅 안에 멋지게 숨겨졌습니다. 컴포넌트는 6줄이 아닌 2줄의 훅 호출만을 가질 것이며, 이는 마크업 생성이라는 주요 책임에 더 집중한다는 것을 의미합니다.

로직을 캡슐화할 수 있습니다

이것이 아마도 저에게 커스텀 훅의 가장 큰 장점일 것입니다. 함께 속한 것들을 묶을 수 있고, 모든 것을 외부에 노출시킬 필요가 없습니다. 위의 useTitleSync 훅은 이상적이지 않습니다. 이펙트만 다루며 여전히 해당 타이틀을 수동으로 관리해야 합니다. 따라서 커스텀 훅에 타이틀과 관련된 모든 로직을 캡슐화하는 것은 어떨까요?

const useTitle = (initialTitle: string) => {
  const [title, setTitle] = React.useState(initialTitle)

  React.useEffect(() => {
    document.title = title
  }, [title])

  return [title, setTitle] as const
}

한 단계 더 나아가 문서의 타이틀만을 표시하고 다른 곳에는 표시하지 않으려는 경우, 훅에 타이틀 값을 유지하고 세터(setter)만 노출하여 최소한의 인터페이스를 생성할 수 있습니다.

// 캡슐화된 값
const useTitle = (initialTitle: string) => {
  const [title, setTitle] = React.useState(initialTitle)

  React.useEffect(() => {
    document.title = title
  }, [title])

  return setTitle
}

개별적으로 테스트할 수 있습니다

useTitle 훅을 사용하는 컴포넌트를 테스트하지 않는다는 것은 페이지 추적과 같이 해당 컴포넌트에서 진행 중인 다른 모든 작업에 대해 생각할 필요가 없다는 이점이 있습니다. 커스텀 훅을 테스트하는 것은 다른 유틸 함수를 테스트하는 것과 매우 유사합니다.

import { act, renderHook } from '@testing-library/react-hooks'

describe('useTitle', () => {
  test('sets the document title', () => {
    const { result } = renderHook(() => useTitle('hello'))
    expect(document.title).toEqual('hello')

    act(() => result.current('world'))
    expect(document.title).toEqual('world')
  })
})

4. 이름 지어주기

위의 모든 이유들로 인해 한 번만 사용하더라도 커스텀 훅을 쓰고 싶어집니다. 그러나 어떤 이유로든 커스텀 훅으로 뽑아낼 수 없거나 원하지 않는 경우, useEffect에 전달된 함수에 여전히 이름을 가질 수 있으므로 이펙트의 이름을 지어주는 것을 고려해 보세요.

// 이름을 가진 이펙트
const [title, setTitle] = React.useState('hello world')

React.useEffect(fuction syncTitle() {
  document.title = title
}, [title])

5. 의존성(deps)으로 거짓말 하지 않기

심지어, 또는 실제로, 특히 함수의 경우에는 하지 않습니다. 댄의 완벽 가이드에서 댄이 이미 설명한 것보다 더 잘 설명할 수 없기 때문에 댄의 가이드를 따를 것입니다.

 

제가 언급할 가치가 있다고 생각하는 한 가지 추가 사항은 모든 이펙트에 의존성이 필요한 것은 아니라는 것입니다. 8개 이상의 의존성을 가진 이펙트를 본 적이 있는데, 그 중 일부는 메모(역주: 이전의 값이 메모리에 저장)되지 않은 객체이기 때문에 모든 렌더에서 이펙트를 트리거 할 것입니다. 그러므로 신경 쓸 필요 없이, useEffect의 두 번째 인수는 결국 선택 사항입니다. 이것은 이펙트에 조기 반환(early return)을 사용하거나, 조건부로 사이드 이펙트를 실행할 때 유용합니다.

const useInitializePayload = () => {
  const payload = usePayload()
  React.useEffect(() => {
    if (payload === null) {
      performSomeSideEffectThatInitializesPayload(value1, value2, ...valueN)
    }
  })
}

이 이펙트에 대한 의존성 배열은 아마도 상당히 클 수도 있으며, [payload]만 의존성에 넣는 편법을 시도할 수도 있습니다. 저는 두 가지 방법 모두 항상 이펙트를 실행하고 필요하다면 중단하는 것보다 못하다고(inferior) 생각합니다.

 

이러한 팁이 useEffect로 작업할 때 복잡성을 줄여주길 바랍니다. 여러분은 이펙트를 어떻게 구성하는 것을 선호하는지 아래의 댓글로 알려주세요. ⬇

반응형