Effects
렌더링 후에도 타이머가 계속 실행되어야 하는 컴포넌트를 생각해 봅시다. 아래의 Timer 컴포넌트가 바로 그런 경우입니다. 컴포넌트의 대부분의 작업은 렌더링 중에 일어납니다. props와 state를 읽고 JSX를 반환하는 것이죠. 하지만 타이머를 계속 실행하는 것은 다릅니다. subscription을 열거나, 문서 제목을 동기화하거나, 서버에서 데이터를 가져오는 것도 마찬가지입니다. 이런 작업들은 React가 관리하지 않는 외부 세계에 손을 뻗어야 합니다. useEffect는 이런 상황을 위한 hook입니다. 컴포넌트가 렌더링 후 코드를 실행할 수 있게 해주므로, 외부 세계와 자신을 동기화할 수 있습니다.
다음은 타이머를 한 번 시작하고 매초 카운터를 증가시키는 effect입니다:
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는 함수를 반환합니다:
return () => clearInterval(id)이 반환된 함수가 정리 함수입니다. React는 effect가 다시 실행되기 전에 이를 실행하고, 컴포넌트가 화면에서 제거될 때 한 번 더 실행합니다. 이 함수의 역할은 effect가 설정한 것을 되돌리는 것입니다.
정리가 중요한 이유는 effect가 종종 자체적으로 계속 실행되는 무언가를 시작하기 때문입니다: interval, event listener, 열린 연결. 컴포넌트가 제거되었을 때 아무것도 그 interval을 멈추지 않으면, 그것은 영원히 계속 발생하고 더 이상 존재하지 않는 컴포넌트에 대한 참조를 유지합니다. 이를 여러 번 반복하면 느린 메모리 누수가 발생하고 더 이상 존재하지 않는 것들에 업데이트가 도달합니다. 경험의 법칙: effect가 무언가를 시작하거나, 무언가를 구독하거나, 무언가를 열면, 정리 함수는 그것을 멈추거나, 구독을 해제하거나, 닫아야 합니다.
effect가 필요하지 않을 수도 있습니다
Effects는 외부 세계와 동기화하기 위한 것이므로, useEffect의 후보처럼 보이는 많은 코드들이 실제로는 다른 곳에 속합니다. 두 가지 경우가 자주 나타납니다.
첫 번째는 파생된 값입니다. 이미 가지고 있는 props와 state에서 뭔가를 계산할 수 있다면, 렌더링 중에 바로 그 자리에서 계산하세요:
function Cart({ items }) {
const total = items.reduce((sum, item) => sum + item.price, 0)
return <p>Total: {total}</p>
}total은 items에서 파생되므로 모든 렌더링에서 계산되고 항상 동기화되어 있습니다. 이를 별도의 state에 저장하고 effect에서 업데이트하는 것은 코드가 더 많고 추가 렌더링을 초래하면서 얻는 것이 없습니다.
두 번째는 사용자 작업에 반응하는 것입니다. 사용자가 클릭하거나 입력했을 때 무언가가 일어나야 한다면, 그 코드는 그 작업에 대한 event handler에 들어갑니다:
function BuyButton({ product }) {
function handleClick() {
buyProduct(product)
}
return <button onClick={handleClick}>Buy</button>
}구매는 handleClick에서, 클릭이 처리되는 바로 그 자리에서 일어납니다. 이를 effect를 통해 라우팅하면 불필요한 간접화가 추가되어 흐름을 따라가기가 어려워집니다. 좋은 테스트는 이렇습니다: 특정 상호작용 때문에 코드가 실행된다면 event handler를 선택하세요. 외부 시스템과 컴포넌트를 동기화하기 위해 실행된다면 effect를 선택하세요.
버전 노트
클래스 컴포넌트에서는 이 동작이 세 개의 lifecycle 메서드로 나뉘었습니다: 첫 번째 렌더링 후 설정을 위한 componentDidMount, 데이터 변경 시 다시 실행하기 위한 componentDidUpdate, 정리를 위한 componentWillUnmount. 의존성 배열과 정리 함수가 있는 하나의 useEffect는 이 세 가지를 모두 다루므로, 관련된 설정 및 해제 코드가 이제 별도의 메서드에 분산되지 않고 함께 있습니다.
useEffect를 React 외부에 손을 뻗는 코드를 위한 자리라고 생각하세요: 타이머, 문서 제목, 서버로의 요청. 끝의 배열은 언제 실행할지를 말하며, []는 컴포넌트가 나타난 직후 한 번이라는 뜻입니다. 그리고 effect가 무언가를 시작하면, 그것을 멈추는 작은 함수를 반환하세요. 대부분 effect가 필요하지 않을 것이므로, 평범한 계산이나 click handler가 일을 할 수 있는지 먼저 묻고 판단하는 것이 좋은 습관입니다. 다음: 데이터 가져오기. 컴포넌트가 React 외부에 있는 데이터를 가져오는 방법입니다.

