React 10. Keeping Components Pure

뚜루미·2024년 3월 2일

React

목록 보기
10/39
post-thumbnail

일부 JavaScript 함수는 순수 함수로 계산만 수행하며 그 이상은 수행하지 않습니다. 컴포넌트를 순수 함수로만 작성하면 코드 베이스가 커짐에 따라 당황스러운 버그와 예측할 수 없는 동작을 방지할 수 있습니다. 하지만 이러한 이점을 얻으려면 따라야 할 몇가지 규칙이 있습니다.

Purity: Components as formulas

컴퓨터 과학에서 순수 함수는 다음과 같은 특성을 가진 함수입니다.

  • 자신의 것에만 신경을 씁니다. 호출되기 이전에 존재했던 어떤 객체나 변수를 변경하지 않습니다.
  • 같은 입력, 같은 출력. 같은 입력이 주어지면 순수 함수는 항상 같은 결과를 반환해야 합니다.
function double(number) {
  return 2 * number;
}

위 예시는 순수 함수 double로 3을 전달하면 항상 6이 반환됩니다.

React는 이 개념을 중심으로 설계되었습니다. React는 여러분이 작성하는 모든 컴포넌트가 순수함수라고 가정합니다. 이는 작성하는 React 컴포넌트가 동일한 입력이 주어지면 항상 동일한 JSX를 반환해야 함을 의미합니다.

function Recipe({ drinkers }) {
  return (
    <ol>    
      <li>Boil {drinkers} cups of water.</li>
      <li>Add {drinkers} spoons of tea and {0.5 * drinkers} spoons of spice.</li>
      <li>Add {0.5 * drinkers} cups of milk to boil and sugar to taste.</li>
    </ol>
  );
}

export default function App() {
  return (
    <section>
      <h1>Spiced Chai Recipe</h1>
      <h2>For two</h2>
      <Recipe drinkers={2} />
      <h2>For a gathering</h2>
      <Recipe drinkers={4} />
    </section>
  );
}

drinker={2}Recipe에 전달하면 2 cups of water 를 포함한 JSX를 반환합니다.

drinker={4}Recipe에 전달하면 4 cups of water 를 포함한 JSX를 반환합니다.

마치 수학 공식처럼 동작하고 컴포넌트는 레시피와 유사하기도 합니다. 컴포넌트를 따르고 요리 과정에서 새로운 자료를 추가하지 않으면 매번 동일한 요리르 얻게 됩니다. 해당 “접시”는 컴포넌트가 렌더링을 위해 React에 제공되는 JSX입니다.

Side Effects: (un)intended consequences

React의 렌더링 프로세스는 항상 순수해야 합니다. 컴포넌트는 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 />
    </>
  );
}

이 컴포넌트는 외부에서 선언된 변수 guest를 읽고 쓰고 있습니다. 즉, 이 컴포넌트를 여러 번 호출하면 다른 JSX가 생성됩니다. 게다가 다른 컴포넌트가 guest를 읽으면 렌더링된 시점에 따라 다른 JSX가 생성되고 예측할 수 없습니다.

아래 코드와 같이 guest를 props로 전달하며 컴포넌트를 수정할 수 있습니다.

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는 guest prop에만 의존하므로 컴포넌트는 순수하게 됩니다.

일반적으로 컴포넌트가 특성 순서로 렌더링될 것이라고 기대해서는 안됩니다. 각 컴포넌트는 “스스로 생각”해야 하며 렌더링 중 다른 컴포넌트와 조정하거나 의존하려고 해서는 안됩니다. 렌더링 학교 시험같으며 각 컴포넌트는 각각의 JSX를 자체적으로 계산해야 합니다.

Detecting impure calculations with StrictMode

React에는 렌더링하는 동안 읽을 수 있는 세 가지 종류의 입력(props, state 및 context)가 있습니다. 이러한 입력은 항상 읽기 전용으로 처리해야 합니다.

사용자 입력에 대한 응답으로 무언가를 변경하려면 변수에 쓰는 대신 setState를 이용해야 합니다. 컴포넌트가 렌더링되는 동안 기존 변수나 객체를 변경하면 안 됩니다.

React는 개발 중에 각 컴포넌트의 기능을 두 번 호출하는 “Strict Mode”를 제공합니다. 해당 모드는 컴포넌트 함수를 두 번 호출하여 이러한 규칙을 위반하는 컴포넌트를 찾는 데 도움이 됩니다.

순수 함수는 계산만 하기 때문에 두 번 호출해도 아무것도 바뀌지 않습니다. 동일한 입력에는 항상 동일한 출력을 제공합니다.

Strict Mode는 Production에는 영향을 미치지 않으므로 사용자 앱 속도를 늦추지 않습니다. Strict Mode를 선택하려면 루트 컴포넌트를 <React.StrictMode>로 포장하면 됩니다. 일부 프레임워크는 기본적으로 이 작업을 수행합니다.

Local mutation: Your component’s little secret

위 예시에서의 문제점은 컴포넌트가 기존에 존재하는 변수를 렌더링 중 변경했다는 것입니다. 이를 흔히 mutation 이라고 부릅니다. 순수 함수는 함수 범위 밖의 변수나 호출 전에 생성된 객체를 변경하지 않습니다.

그러나 렌더링하는 동안 방금 생성한 변수와 객체를 변경하는 것은 전혀 문제가 되지 않습니다. 아래 예에서는 배열을 만들고 이를 cups 변수에 할당한 다음 그 안에 12개의 컵을 넣습니다.

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

export default function TeaGathering() {
  let cups = [];
  for (let i = 1; i <= 12; i++) {
    cups.push(<Cup key={i} guest={i} />);
  }
  return cups;
}

변수 cups나 배열이 함수 외부에서 생성되었다면 TeaGathering 이는 큰 문제가 될것입니다. 해당 배열에 항목을 푸시하여 기존 객체를 변경하게 됩니다.

그러나 TeaGathering 내부에서 동일한 렌더링 중에 생성했기 때문에 괜찮습니다. TeaGathering 외부의 어떤 코드도 이런 일이 발생했다는 것을 알 수 없습니다. 이것을 “Local Mutation”이라고 부릅니다.

Where you can cause side effects

함수형 프로그래밍은 순수성에 크게 의존하지만, 어느 시점, 어딘가에서는 무언가 변경되어 합니다. 그것이 바로 프로그래밍의 핵심입니다. 화면 업데이트, 애니메이션 시작, 데이터 변경 등 이러한 변경을 side effect라고 합니다. 이는 렌더링 도중이 아닌 side에서 발생하는 일입니다.

React에서 side effect는 일반적으로 이벤트 핸들러 내부에 포함됩니다. 이벤트 핸들러는 버튼을 클릭하는 등의 작업을 수행할 때 React가 실행하는 함수입니다. 이벤트 핸들러가 컴포넌트 내부에 정의되어 있더라고 렌더링 중에는 실행되지 않습니다. 따라서 이벤트 핸들러는 순수할 필요가 없습니다.

다른 모든 옵션을 모두 사용했고 side effect에 적합한 이벤트 핸들러를 찾을 수 없는 경우에도 컴포넌트에서 useEffect호출을 사용하며 반환된 JSX에 연결할 수 있습니다. 이는 렌더링 후 side effect가 허용될 대 나중에 React를 실행하도록 지시합니다. 이 접근 방식은 최후의 수단이 되어야 합니다.

Why does React care about purity?

순수 함수 작성은 약간의 습관과 규율이 필요합니다.

  • 컴포넌트는 서버와 같은 다른 환경에서 실행될 수 있습니다. 동일한 입력에 대해 동일한 결과를 반환하므로 하나의 컴포넌트가 많은 사용자 요청을 처리할 수 있습니다.
  • 입력이 변경되지 않은 렌더링 컴포넌트를 건너뛰면 성능을 향상시킬 수 있습니다. 순수 함수는 항상 동일한 결과를 반환하므로 캐싱해도 안정합니다.
  • 깊은 컴포넌트 트리를 렌더링하는 도중 일부 데이터가 변경되면 React는 오래된 렌더링을 완료하는 데 시간을 낭비하지 않고 렌더링을 다시 시작할 수 있습니다. Purity는 언제든지 계산을 중단해도 안전합니다.

0개의 댓글