Life is connecting the dots .

React 컴포넌트를 순수 함수로 작성하는 이유 본문

Programming/React

React 컴포넌트를 순수 함수로 작성하는 이유

soyeori 2024. 4. 2. 13:06

지난 프로젝트를 진행하면서 Next.js에 기본값으로 StrictMode 값이 true로 설정되어 있어서 버그를 발견하고 예방할 수 있었다. 그때의 상황을 대략 설명하자면 컴포넌트 안에서 useEffect를 실행했었고, useEffect의 콜백함수로 API를 호출하는 중이었다. 단순히 데이터를 불러오는 API라면 여러 번 호출해도 이상 없겠지만, 해당 API는 로그인 인증 관련된 부분이어서 동일한 데이터를 두 번 호출했기 때문에 에러가 발생하는 상황이었다.

 

결론적으로 컴포넌트가 다시 마운트 될 때, 이전 API요청을 취소하는 방법으로 해결했고, 개발환경에서만 React StrictMode가 동작하므로 배포환경에서는 문제가 안 되는 것을 확인했지만 혹시 모를 상황에 대비하여 코드를 제대로 작성할 수 있었다. 

 

최근 리액트 공식문서를 읽으면서 리액트 StrictMode가 왜 필요한지, 즉 개발환경에서 컴포넌트를 두 번 호출하는 이유가 무엇인 지 알게 되었고, 더불어 리액트에 대해 더 자세히 알게 되어 글을 작성하게 되었다.

 

📍 컴포넌트를 순수하게 유지하는 것

컴퓨터 프로그래밍에서 순수 함수란 동일한 input에 대해서는 동일한 output이 반환되는 함수를 의미한다. 또한, 외부 상태에 의존하지 않으며, 지역 변수를 변경하는 등의 mutation과 같은 사이드 이팩트를 가지지 않는 함수를 의미한다.

 

이 의미를 리액트에 대입해 보면, 리액트는 모든 컴포넌트를 pure function으로 가정한다. 즉, 리액트는 동일한 input에 대해 동일한 JSX를 반환한다. 리액트는 오직 JSX만을 리턴하며, 렌더링 하기 전에 존재하는 객체나 변수를 변경하지 않는다.

 

다음은 리액트 공식문서에 나와 있는 예시이다.

let guest = 0;

function Cup() {
  // Bad: changing a preexisting variable!
  guest = guest + 1;
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup />
      <Cup />
      <Cup />
    </>
  );
}

 

위의 예시에서 알 수 있듯이, Cup 컴포넌트가 렌더링 될 때마다 이전에 존재하는 변수를 변경하면서 JSX는 계속해서 다른 컴포넌트를 반환한다. 이는 formulas로서 컴포넌트를 유지할 수 없게 되고, 예측할 수 없는 오류를 만든다.

 

이 컴포넌트를 pure 하게 만들기 위해서는 TeaSet 컴포넌트에서 guest를 Cup 컴포넌트의 prop으로 전달함으로써 Cup 컴포넌트에서 값을 prop에 의존하도록 변경해야 한다.

function Cup({ guest }) {
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup guest={1} />
      <Cup guest={2} />
      <Cup guest={3} />
    </>
  );
}

 

위 예제의 핵심은 컴포넌트는 "스스로 생각"해야 하며, 렌더링 중에 다른 컴포넌트와 조정하거나 의존해서는 안된다. 즉, 각 컴포넌트는 자신만의 JSX를 계산해야 한다.

 

📍 StrictMode로 순수하지 않은 컴포넌트 탐지하기

위에서 살펴봤듯이 컴포넌트는 pure function으로 유지해야 하고, 그 규칙을 깨는 컴포넌트를 발견하기 위한 장치로 컴포넌트를 두 번 호출하는 Strict Mode를 둔 것이다. 순수 함수는 계산만 하므로 두 번 호출해도 컴포넌트 안의 어떤 것도 변경시키지 않는다. 

리액트에서는 렌더링 하는 동안 읽을 수 있는 3가지 inputs이 있는데, 이 props, state, context는 항상 읽기 전용(read-only)으로 취급되며, 무언가를 변경하고 싶으면 변수를 작성하는 것 대신 set state를 사용해야 한다. 즉, 컴포넌트가 렌더링 하는 동안 이전에 존재하는 변수나 객체를 변경해서는 안된다. 

 

그렇다면 데이터를 변경하고 싶을 때, 즉, 사이드 이팩트는 어디에서 실행해야 하는 것일까? 리액트에서 사이드 이팩트는 이벤트 핸들러 안에서 이루어진다. 예를 들어, 버튼을 클릭하는 것과 같이 어떠한 행위로 인해 동작하는 이벤트 핸들러 함수는 컴포넌트 내부에 있어도 렌더링 하는 동안 실행되지 않는다. 그러므로 이벤트 핸들러는 순수함수일 필요가 없다. 

 

📍 컴포넌트를 순수함수로 작성하는 방법의 이점

리액트 컴포넌트에서 순수함수 원칙을 적용하는 것은 여러 가지 장점을 가진다.

 

- 다양한 환경에서 실행의 이점

컴포넌트가 동일한 inputs에 대해 동일한 결과를 반환하므로 하나의 컴포넌트에서 많은 사용자의 요청을 처리할 수 있다. 예를 들어, 서버사이드 렌더링으로 동작하는 블로그 포스팅을 보여주는 컴포넌트가 있다고 가정하면, 컴포넌트가 서버에서 실행될 때도 동일한 결과를 반환하므로 동일한 블로그 포스팅에 대해서는 동일한 JSX를 리턴한다. 즉, 클라이언트뿐만 아니라 SSR과 같은 다른 환경에서도 일관된 결과를 제공하고, 다수의 요청 처리가 가능하다.

 

- 성능 향상의 이점

변경되지 않은 input에 대해서는 렌더링 하지 않고, 동일한 결과를 반환하므로 캐싱에 안전하다. 

 

- 컴포넌트의 효율적인 렌더링 

렌더링 과정 중 데이터가 변경되더라도 순수 함수로 작성된 컴포넌트일 경우, 리액트는 안전하게 현재 진행 중인 렌더링을 중단하고, 변경된 데이터를 기반으로 새로운 렌더링 프로세스를 시작할 수 있다. 그 이유는 순수함수로 작성된 컴포넌트가 외부 상태 변화에 의존하지 않고, 주어진 input에 대해서만 출력을 결정하기 때문이다. 즉, 리액트는 데이터 변경 전과 후를 효율적으로 비교하고, 진행 중인 렌더링에 대해 리소스를 낭비하지 않는다.

 

👩🏻‍💻 마무리

리액트 컴포넌트를 순수 함수로 작성하는 것은 코드베이스가 커짐에 따라 발생할 수 있는 버그를 예방하고, 그 장치로 strict mode를 제공한다. 최종적으로 리액트의 렌더링 과정에서 불필요한 작업을 최소화하고, 데이터가 변경되었을 때 신속히 대응할 수 있게 해 준다. 또한, 리액트에서 개발 중인 모든 새로운 기능은 컴포넌트의 순수성을 활용하므로 이를 잘 이해하고 적용하는 것은 리액트 패러다임을 이해하는 것과 같다.

 

리액트에서 계속해서 강조하는 것처럼 가능한 한, 다른 컴포넌트 JSX 안에서 논리를 표현하는 것이 이 글의 핵심 주제이다. 리액트의 패러다임을 잘 이해하고, 이후 이벤트 핸들러, useEffect를 적재적소에 활용해서 순수함수로서 컴포넌트를 작성하고, 이 과정을 인지하며 개발해 나가야겠다.

 


참고

https://react.dev/learn/keeping-components-pure