Skip to content
This page has been auto-translated and may contain errors.View in English

Effects

렌더링 후에도 타이머가 계속 실행되어야 하는 컴포넌트를 생각해 봅시다. 아래의 Timer 컴포넌트가 바로 그런 경우입니다. 컴포넌트의 대부분의 작업은 렌더링 중에 일어납니다. props와 state를 읽고 JSX를 반환하는 것이죠. 하지만 타이머를 계속 실행하는 것은 다릅니다. subscription을 열거나, 문서 제목을 동기화하거나, 서버에서 데이터를 가져오는 것도 마찬가지입니다. 이런 작업들은 React가 관리하지 않는 외부 세계에 손을 뻗어야 합니다. useEffect는 이런 상황을 위한 hook입니다. 컴포넌트가 렌더링 후 코드를 실행할 수 있게 해주므로, 외부 세계와 자신을 동기화할 수 있습니다.

다음은 타이머를 한 번 시작하고 매초 카운터를 증가시키는 effect입니다:

jsx
import { useState, useEffect } from 'react'

function Timer() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const id = setInterval(() => setCount(c => c + 1), 1000)
    return () => clearInterval(id)
  }, [])

  return <p>{count}</p>
}

여기서는 세 가지 부분이 작동하고 있으며, 모든 effect가 다루는 세 가지 것들과 대응됩니다: 무엇을 할 것인지, 어떻게 정리할 것인지, 그리고 언제 실행할 것인지입니다.

useEffect에 전달하는 함수가 바로 effect 자체입니다. 이 예제에서는 setInterval을 호출해서 매초 count를 1씩 증가시키는 반복 타이머를 시작합니다.

의존성 배열

두 번째 인자인 끝의 []는 **의존성 배열**입니다. 이것이 effect가 실행될 시기를 제어합니다.

  • 빈 배열 []는 컴포넌트가 화면에 처음 나타난 후 effect가 한 번 실행됨을 의미합니다. 위의 타이머는 이를 사용합니다: interval을 한 번 시작하고 계속 실행시킵니다.
  • [a, b]처럼 값이 들어있는 배열은 첫 렌더링 후 effect가 실행되고, 그 이후로는 이 값 중 하나라도 렌더링 사이에 변경될 때마다 다시 실행됨을 의미합니다. effect가 의존하는 무언가가 변경되었을 때 다시 동기화하는 방법입니다.
  • 배열을 완전히 생략하면 모든 렌더링 후에 effect가 실행됩니다. 이것은 거의 원하는 경우가 아니며, effect가 state를 업데이트할 때 무한 루프의 흔한 원인이 됩니다.

따라서 배열은 "이 코드가 언제 다시 실행되어야 하나?"라는 질문에 대한 답입니다. effect가 읽고 의존하는 값들을 나열하면, React는 이들 중 하나라도 변경될 때 다시 실행합니다.

정리 함수

위의 effect는 함수를 반환합니다:

jsx
return () => clearInterval(id)

이 반환된 함수가 정리 함수입니다. React는 effect가 다시 실행되기 전에 이를 실행하고, 컴포넌트가 화면에서 제거될 때 한 번 더 실행합니다. 이 함수의 역할은 effect가 설정한 것을 되돌리는 것입니다.

정리가 중요한 이유는 effect가 종종 자체적으로 계속 실행되는 무언가를 시작하기 때문입니다: interval, event listener, 열린 연결. 컴포넌트가 제거되었을 때 아무것도 그 interval을 멈추지 않으면, 그것은 영원히 계속 발생하고 더 이상 존재하지 않는 컴포넌트에 대한 참조를 유지합니다. 이를 여러 번 반복하면 느린 메모리 누수가 발생하고 더 이상 존재하지 않는 것들에 업데이트가 도달합니다. 경험의 법칙: effect가 무언가를 시작하거나, 무언가를 구독하거나, 무언가를 열면, 정리 함수는 그것을 멈추거나, 구독을 해제하거나, 닫아야 합니다.

effect가 필요하지 않을 수도 있습니다

Effects는 외부 세계와 동기화하기 위한 것이므로, useEffect의 후보처럼 보이는 많은 코드들이 실제로는 다른 곳에 속합니다. 두 가지 경우가 자주 나타납니다.

첫 번째는 파생된 값입니다. 이미 가지고 있는 props와 state에서 뭔가를 계산할 수 있다면, 렌더링 중에 바로 그 자리에서 계산하세요:

jsx
function Cart({ items }) {
  const total = items.reduce((sum, item) => sum + item.price, 0)
  return <p>Total: {total}</p>
}

totalitems에서 파생되므로 모든 렌더링에서 계산되고 항상 동기화되어 있습니다. 이를 별도의 state에 저장하고 effect에서 업데이트하는 것은 코드가 더 많고 추가 렌더링을 초래하면서 얻는 것이 없습니다.

두 번째는 사용자 작업에 반응하는 것입니다. 사용자가 클릭하거나 입력했을 때 무언가가 일어나야 한다면, 그 코드는 그 작업에 대한 event handler에 들어갑니다:

jsx
function BuyButton({ product }) {
  function handleClick() {
    buyProduct(product)
  }
  return <button onClick={handleClick}>Buy</button>
}

구매는 handleClick에서, 클릭이 처리되는 바로 그 자리에서 일어납니다. 이를 effect를 통해 라우팅하면 불필요한 간접화가 추가되어 흐름을 따라가기가 어려워집니다. 좋은 테스트는 이렇습니다: 특정 상호작용 때문에 코드가 실행된다면 event handler를 선택하세요. 외부 시스템과 컴포넌트를 동기화하기 위해 실행된다면 effect를 선택하세요.

모든 effect에서 실질적인 질문은 의존성 배열에 무엇이 들어가야 하는가입니다. 답은 기계적입니다: effect가 읽는 모든 reactive value, 즉 effect 함수 내에 나타나는 모든 prop, state 변수, 또는 그들로부터 파생된 값이 배열에 속합니다. 하나를 빠뜨리면 effect는 변경 사항을 받아들이지 못하고 오래된 값의 복사본을 계속 사용합니다.

jsx
function SearchResults({ query }) {
  const [results, setResults] = useState([])

  useEffect(() => {
    let active = true
    fetch(`/api/search?q=${query}`)
      .then(r => r.json())
      .then(data => { if (active) setResults(data) })
    return () => { active = false }
  }, [query])

  return <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>
}

이 effect는 query를 읽으므로, query가 배열에 들어갑니다. 스코프 내에서 다른 것이 effect의 동작을 변경하지 않으므로 다른 것은 들어갈 필요가 없습니다. 이를 암기해서 작동하게 할 필요는 없습니다. eslint-plugin-react-hooks lint 규칙인 exhaustive-deps는 effect 함수를 읽고 배열에 없는 사용된 값들을 플래그합니다. 이 경고들을 해결해야 할 실제 버그로 취급하세요. 무시해야 할 소음이 아닙니다. 여기서 경고를 무시하는 것이 정확히 오래된 값 버그가 프로덕션에 들어가는 방식이기 때문입니다.

effect가 두 개의 관련 없는 일을 하기 시작하면, 더 긴 의존성 배열을 가진 하나의 effect 대신 두 개의 effect로 분리하세요. 사용자의 데이터를 가져오고 그 사용자를 기반으로 문서 제목을 설정하는 컴포넌트는 두 가지 관심사를 모두 처리하는 하나의 effect보다, 각각 고유의 초점이 맞춰진 의존성 배열을 가진 두 개의 useEffect 호출로 이해하기가 더 쉽습니다. 각 effect는 외부 세계의 한 가지 것과 동기화해야 합니다.

code가 effect에 속하는지 판단하는 빠른 테스트: 뭔가를 가져오거나, 뭔가를 구독하거나, DOM이나 React 외부의 다른 시스템에 직접 손을 뻗나요? 그것이 effect 영역입니다. 이미 가지고 있는 props나 state에서 값을 계산하나요? 그것은 effect가 아니라 렌더링 본문에 속합니다. 값을 파생시키기 위해 useEffect에 손을 뻗는 것이 effect가 과다 사용되는 가장 흔한 방식입니다.

Effect 타이밍은 정확할 가치가 있습니다. 여러분의 effect는 렌더링 중에 실행되지 않습니다. React가 컴포넌트를 렌더링하고, DOM 업데이트를 커밋하고, 브라우저가 페인트하고, 그 이후에야 effect가 실행됩니다. 이것은 의도적입니다: effect는 이미 현재 렌더링을 반영하는 화면을 보고, 절대 브라우저가 페인트하는 것을 막지 않습니다. 또한 첫 번째 페인트 전에 사용자가 보는 것을 만들기 위해 effect에 의존할 수 없음을 의미합니다. 페인트 전에 DOM을 측정하거나 변경해야 하는 더 드문 경우를 위해 useLayoutEffect가 있으며, 이것은 커밋 후 브라우저가 페인트하기 전에 동기적으로 실행됩니다.

개발 중에 effect가 mount 시 두 번 실행되는 것을 알아챌 것입니다. React의 Strict Mode는 의도적으로 각 컴포넌트를 마운트하고, 언마운트한 다음, 다시 마운트합니다. effect를 실행하고, 정리를 실행한 다음, effect를 두 번째로 실행합니다. 이것은 정리에 대한 스트레스 테스트입니다. effect가 무언가를 설정하지만 절대 그것을 해제하지 않으면, 더블 런은 바로 그 버그를 드러냅니다. 왜냐하면 하나 대신 두 개의 interval이나 두 개의 subscription을 볼 것이기 때문입니다. 이것은 개발 중에만 일어납니다. 프로덕션에서 effect는 한 번 실행됩니다.

내재화할 가치가 있는 의존성 배열 함정 두 가지가 있습니다. 첫 번째는 오래된 closure입니다. effect는 자신이 생성된 렌더링의 값들을 캡처합니다. 의존성 배열에서 값을 빠뜨리면, effect는 변경된 후에도 그 렌더링의 오래된 값을 계속 읽고, 새로운 값을 받기 위해 다시 실행되지 않습니다. 해결책은 effect가 읽는 모든 값을 나열하거나, setCount(c => c + 1)처럼 updater 형식을 사용하는 것입니다. 그러면 현재 값을 읽을 필요가 없습니다. 두 번째는 identity입니다. 렌더링 중에 생성된 객체, 배열, 함수는 매번 새로운 값이므로, 하나를 의존성으로 나열하면 참조가 지난번과 절대 같지 않으므로 effect가 모든 렌더링에서 다시 실행됩니다. 하나를 의존성으로 필요로 할 때, effect 내에서 정의하거나, useMemo 또는 useCallback으로 감싸세요 (렌더링 사이에 값이나 함수를 캐시하는 hook들). 그러면 렌더링 사이에 identity가 안정적으로 유지됩니다.

버전 노트

클래스 컴포넌트에서는 이 동작이 세 개의 lifecycle 메서드로 나뉘었습니다: 첫 번째 렌더링 후 설정을 위한 componentDidMount, 데이터 변경 시 다시 실행하기 위한 componentDidUpdate, 정리를 위한 componentWillUnmount. 의존성 배열과 정리 함수가 있는 하나의 useEffect는 이 세 가지를 모두 다루므로, 관련된 설정 및 해제 코드가 이제 별도의 메서드에 분산되지 않고 함께 있습니다.

JunoEffects sync with the outside worlduseEffect를 React 외부에 손을 뻗는 코드를 위한 자리라고 생각하세요: 타이머, 문서 제목, 서버로의 요청. 끝의 배열은 언제 실행할지를 말하며, []는 컴포넌트가 나타난 직후 한 번이라는 뜻입니다. 그리고 effect가 무언가를 시작하면, 그것을 멈추는 작은 함수를 반환하세요. 대부분 effect가 필요하지 않을 것이므로, 평범한 계산이나 click handler가 일을 할 수 있는지 먼저 묻고 판단하는 것이 좋은 습관입니다.
JunoEffects sync with the outside worlduseEffect는 렌더링 후 실행되어 React가 소유하지 않은 것과 동기화합니다. 의존성 배열이 제어 역할을 합니다: []는 한 번 실행하고, [a, b]는 이들이 변경될 때 다시 실행하고, 배열이 없으면 모든 렌더링마다 실행합니다. 시작하거나, 구독하거나, 열은 것에 대해서는 정리 함수를 반환하세요. 하나를 작성하기 전에, 값이 렌더링 중에 파생될 수 있거나 작업이 event handler에 속하는지 확인하세요. 왜냐하면 이들이 effect 영역처럼 보이는 많은 부분을 다루기 때문입니다.
JunoEffects sync with the outside world Effects는 커밋 후 실행되므로 페인트된 화면을 보고 절대 차단하지 않습니다. 페인트 전에 DOM을 변경해야 할 때만 useLayoutEffect에 손을 뻗으세요. Strict Mode의 개발 중 더블 mount는 정리 테스트이므로, 연두 배운 interval이나 subscription을 실제 버그로 취급하세요. 의존성 함정 두 가지는 생략된 값으로 인한 오래된 closure와 모든 렌더링마다 effect를 다시 실행하게 하는 불안정한 객체 또는 함수 identity입니다. updater 형식, useMemo, useCallback이 이를 제거하는 방법입니다.

다음: 데이터 가져오기. 컴포넌트가 React 외부에 있는 데이터를 가져오는 방법입니다.