React 컴포넌트에서 상태를 관리하거나 이벤트 핸들러를 통해 사용자와 상호작용하는 경우, Side Effects가 발생하게 된다. 일반적으로 이벤트 핸들러는 사이드 이펙트를 포함한다. 그러나, 렌더링 자체로 인해 발생하는 사이드 이펙트를 처리해야 할 때는 useEffect 훅이 필요하다. 이 글에서는 useEffect의 개념, 동작 방식, 그리고 효과적인 사용 방법에 대해 설명하겠다.
사이드 이펙트란?
사이드 이펙트란 프로그램의 상태를 변경하거나 외부 시스템과 상호작용하는 것을 의미한다. 예를 들어, 사용자가 버튼을 클릭하여 메시지를 전송하는 것은 이벤트에 의한 사이드 이펙트다. 반면, 컴포넌트가 렌더링될 때 외부 API로부터 데이터를 가져오는 작업은 렌더링으로 인해 발생하는 사이드 이펙트다. 이러한 렌더링 사이드 이펙트는 useEffect 훅을 통해 처리할 수 있다.
useEffect의 기본 개념
useEffect 훅은 렌더링 후 특정 코드가 실행되도록 하는 역할을 한다. 이 훅은 React 컴포넌트가 Commit phase 이후에 실행된다. 즉, 화면이 업데이트된 후에 사이드 이펙트를 동기화하는 데 이상적인 시점이다. React에서 렌더링은 순수한 계산이어야 하므로, DOM을 직접 수정하거나 외부 데이터를 수정하는 등의 사이드 이펙트는 렌더링 단계에서 수행하지 말아야 한다. 이를 위해, React는 useEffect를 제공하여 렌더링과 사이드 이펙트를 분리한다.
useEffect(() => {
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data));
}, []);
위 예시에서 useEffect는 컴포넌트가 처음 렌더링된 후 데이터를 가져오는 작업을 수행한다. 의존성 배열에 빈 배열 []을 전달했기 때문에 이 효과는 한 번만 실행된다. 이를 통해 초기 렌더링 후 데이터를 가져오고, 컴포넌트가 다시 렌더링되지 않도록 설정할 수 있다.
의존성 배열과 React의 동작
useEffect는 의존성 배열을 통해 사이드 이펙트가 언제 실행될지를 제어한다. 의존성 배열은 useEffect가 실행될지 여부를 결정하는 중요한 요소로, React는 배열에 있는 값을 비교해 변경 사항이 있는 경우에만 useEffect를 실행한다. 이 비교는 Object.is 알고리즘을 사용하며, 이는 엄격한 비교를 의미한다.
그러나 mutatable 값(예: location.pathname이나 ref.current)을 의존성 배열에 포함시키는 것은 적절하지 않다. mutatable 값들은 React의 렌더링 데이터 흐름과 독립적이기 때문에, 값이 변경되어도 리렌더링이 트리거되지 않는다. 따라서 이러한 경우에는 useSyncExternalStore와 같은 외부 스토어를 사용해 값을 동기화하는 방법이 적절하다.
반면, Props, State, 그리고 컴포넌트 내부에서 선언된 변수는 reactive values로, 이러한 값들만 의존성 배열에 포함될 수 있다. useEffect에서 참조되는 모든 reactive values은 의존성 배열에 명시해야 하며, 이를 통해 useEffect가 최신 상태의 props와 state에 맞춰 항상 동기화되도록 해야 한다.
- 빈 배열 []: 컴포넌트가 마운트될 때 한 번만 실행.
- 상태나 props를 포함한 배열: 배열에 있는 값이 변경될 때마다 useEffect 실행.
- 의존성 배열 생략: 컴포넌트가 리렌더링될 때마다 매번 실행.
useEffect(() => {
console.log('Counter updated:', count);
}, [count]);
위 예시에서 count 값이 변경될 때마다 useEffect가 실행된다. 의존성 배열에 있는 값이 변경되었는지를 기준으로 사이드 이펙트가 트리거되는 것이다.
useEffect를 사용할 때 두 번 실행되는 문제
개발 모드에서는 useEffect가 의도치 않게 두 번 실행되는 경우가 있을 수 있다. 이를 방지하기 위해 일부 개발자들은 useRef를 사용하여 한 번만 실행되도록 제어하려 하지만, 이는 적절한 해결책이 아니다. 대신, 문제를 해결하려면 사이드 이펙트가 재마운트 시에도 적절히 동작하도록 해야 하며, 필요한 경우 정리(clean-up) 작업을 처리해야 한다.
Clean-up 함수
useEffect에서 반환되는 함수는 정리 작업(clean-up)을 수행하는 데 사용된다. 이 함수는 컴포넌트가 언마운트되거나 다음 렌더링이 발생하기 전에 실행된다. 이를 통해 이벤트 리스너를 제거하거나 타이머를 정리하는 등의 작업을 수행할 수 있다.
useEffect(() => {
const handleResize = () => console.log('Resizing...');
window.addEventListener('resize', handleResize);
// Clean-up 함수: 컴포넌트가 언마운트되면 이벤트 리스너 제거
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
위 코드에서는 resize 이벤트 리스너가 설정되고, 컴포넌트가 언마운트될 때 해당 리스너가 정리된다.
동기화의 독립성: 각 Effect는 독립적이어야 한다
useEffect를 사용할 때, 각각의 사이드 이펙트는 독립적인 동기화 프로세스를 나타내야 한다. 이는 복잡한 애플리케이션에서 각 사이드 이펙트를 개별적으로 관리하고 동기화할 수 있게 한다. 여러 사이드 이펙트를 하나의 useEffect에서 처리하려 하지 말고, 독립적으로 정의하는 것이 바람직하다.
잘못된 접근: 여러 사이드 이펙트를 한 곳에서 처리
useEffect(() => {
// 여러 동작을 한 번에 처리하는 잘못된 패턴
fetchData();
handleResize();
addEventListeners();
}, [dependency]);
이런 방식으로 하나의 useEffect에 여러 동작을 묶으면 관리가 복잡해지고, 사이드 이펙트 간의 의존성이 생길 수 있다. 대신 코드가 길어지더라도 각각의 사이드 이펙트를 독립된 useEffect로 분리하는 것이 더 나은 방법이다.
올바른 접근:
- 단일 효과에 집중: 각 useEffect는 하나의 동작에 집중하고 독립적으로 작동해야 한다.
- 정리 작업 포함: 외부 리소스와 동기화하거나 이벤트 리스너를 사용하는 경우, 반드시 정리 작업을 포함시켜야 한다.
- 의존성 배열 관리: 모든 의존성을 정확히 배열에 명시해 최신 상태의 props와 state를 반영하도록 해야 한다.
잘못된 접근: 애플리케이션 초기화 로직을 useEffect에서 처리하기
애플리케이션이 시작될 때 한 번만 실행되어야 하는 로직은 useEffect 내부에 넣는 것이 적절하지 않다. 이러한 로직은 컴포넌트 외부에 배치하여 렌더링과는 무관하게 처리하는 것이 좋다. 예를 들어, 인증 토큰을 확인하거나 로컬 스토리지에서 데이터를 불러오는 작업은 컴포넌트 외부에서 실행할 수 있다.
// 애플리케이션이 시작될 때 한 번만 실행되어야 하는 로직
if (typeof window !== 'undefined') {
// 브라우저 환경에서 실행 중인지 확인
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// 컴포넌트 코드
return (
<div>
{/* ... */}
</div>
);
}
'React' 카테고리의 다른 글
React Hook 이해하기 (4): useMemo, useCallback (0) | 2024.09.10 |
---|---|
React Hook 이해하기 (3): useRef (0) | 2024.09.10 |
React Hook 이해하기 (1): useState (0) | 2024.09.10 |
React에서의 Event Handler (0) | 2024.09.10 |
React와 JSX (0) | 2024.09.05 |