순수 함수란 함수가 호출되기 이전에 존재했던 어떤 객체나 변수도 변경하지 않는 것을 말한다. 핵심은 같은 입력이 주어졌을 때 항상 같은 결과를 반환한다는 것이다. 리액트 컴포넌트도 결국에는 하나의 함수이고, 리액트는 기본적으로 모든 컴포넌트를 순수 함수로 간주한다.
따라서 우리는 컴포넌트가 항상 JSX만을 반환하고, 이전에 존재했던 객체나 변수를 변경하지 않도록 해야 한다. 그렇다면 이전에 존재했던 객체나 변수란 뭘까? 간단히 함수(즉, 컴포넌트) 외부에 선언한 변수가 되겠다.
순수성을 유지하면 두 가지 이득을 얻을 수 있다.
하지만 '변화'란 프로그래밍의 본질이기도 하기 때문에, 이러한 변화를 어디에선가는 다뤄줘야 한다.
DOM을 조작하거나 데이터를 저장하거나, 애니메이션을 실행하는 등의 모든 변화를 사이드 이펙트라고 한다. 리액트는 렌더링 이후에 사이드 이펙트를 수행함으로써 렌더링 중 순수성을 유지한다.
사이드 이펙트는 다음 시점에서 정의될 수 있다. 공통점은 모두 렌더링이 완료된 후에 실행된다는 것이다. 보통 이벤트 핸들러로 처리하기 어려운 사이드 이펙트를 useEffect 내부에서 처리한다.
이때, 앞선 예시의 DOM 조작은 리렌더링과 다르다.
import { useState, useEffect } from 'react';
const Example = () => {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `현재 카운트: ${count}`; // ✅ 사이드 이펙트 (렌더링 후 수행)
}, [count]); // count가 변경될 때마다 실행됨
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
위 코드에서 클릭 이벤트에 따라 count를 업데이트하는 과정은 리렌더링을 유발하고, useEffect의 콜백함수 내에서 count의 업데이트에 따라 DOM 요소가 수정되는 과정은 사이드 이펙트이다. 구분이 되는가? 리액트의 리렌더링에 대해 더 살펴보자.
리액트에서 렌더링이란 컴포넌트를 호출하여 화면에 표시할 내용을 확인하는 것을 말한다. 리액트는 상태 변화가 일어나면 상태 변화가 일어난 컴포넌트부터 더 이상 중첩된 컴포넌트가 없을 때까지 재귀적인 호출을 반복한다.
즉, 리액트는 상태 변화가 일어나면 리렌더링한다. 이때 상태 변화는 얕은 비교를 기반으로 참조가 변경되었는지를 확인한다. 따라서 직접 변수나 객체를 수정하면 인식하지 못하고, 새 변수나 객체를 생성해야 한다.
그러니까 let으로 선언된 변수를 수정해도 리액트는 리렌더링하지 않는다. 대신에 useState를 사용해 새 객체를 반환하면 상태 변화를 감지하고 리렌더링한다.
setCount -> useState를 사용해 새 객체를 반환하여 리렌더링 유발
document.title -> (리)렌더링이 완료되고 useEffect 내부에서 실행되는 사이드 이펙트
리액트는 순수성을 유지하기 위해 기본적으로 strict mode(엄격 모드)를 제공한다. 최상단 컴포넌트를 <React.StrictMode>로 감싸 strict mode를 설정하면 모든 컴포넌트를 두 번씩 호출해 의도와 다르게 동작하는 컴포넌트를 확인할 수 있다.
이렇게 리액트 공식 문서를 기반으로 리액트가 순수성을 유지하는 방식에 대해 살펴보았다. 다음 포스팅에서는 리액트의 렌더링을 조금 더 구체적으로 살펴보자!