본문 바로가기
Develop/React

React component의 순수성이란

by 지오는 짱짱걸 입니다 2023. 8. 18.
반응형

Component를 순수하게 유지하자

순수 함수(Pure function)는 계산만 수행합니다. 그러므로 component를 순수 함수로만 엄격하게 작성하면, 코드 베이스가 커짐에 따라 함께 올 수 있는 각종 버그들과 예측 불가능한 동작을 피할 수 있습니다.

순수 함수 효과를 얻기 위해서 따라야할 몇 가지 규칙이 있으며, 지금부터 알아보겠습니다.

순수성
함수형 프로그래밍에서는 순수 함수가 다음의 특성을 따릅니다.

  • 호출되기 전에 존재했던 객체나 변수는 변경하지 않습니다. 즉 자기 자신에 대해서만 생각합니다.
  • Input이 같으면 output도 같습니다. Input으로 같은 값이 주어지면, 순수 함수는 항상 같은 결과를 반환해야 합니다.

이미 우리는 수학에서 순수 함수와 비슷한 것을 본 적이 있습니다.

y=2x 일 때,
만약 x=2 라면, y는 언제나 4입니다.
만약 x=3 이라면, y는 언제나 6입니다.

만약 우리가 JavaScript 함수로 위의 방정식을 만들어 본다면 다음과 같습니다.

function double(number) {
    return 2 * number;
}

위의 예제를 보면, double은 순수 함수입니다. 왜냐하면 우리가 3을 input으로 주면, 항상 6을 반환하기 때문입니다.

React는 이러한 컨셉으로 설계 되었습니다. React는 작성된 모든 component가 순수 함수라고 가정합니다. 이말은 작성된 react component들은 동일한 input에 대해서는 항상 동일한 JSX를 반환해야 한다는 말과 같습니다.

Side Effects: 의도하지 않은 결과
React의 렌더링 프로세스는 언제나 순수해야 합니다. Component들은 그들의 JSX만을 반환해야하며, Component가 렌더링되기 전에 존재했던 객체나 변수들을 변경해서는 안 됩니다. 이러한 특징이 지켜지지 않으면, component는 순수하지 못하게 됩니다.

아래는 이러한 규칙을 지키지 않은 component의 예시입니다.

let guest = 0;
function Cup() {
  // Bad: 기존 변수를 변경함!
  guest = guest + 1;
  return <h2>Tea cup for guest #{guest}</h2>;
}export default function TeaSet() {
  return (
    <>
      <Cup />
      <Cup />
      <Cup />
    </>
  );
}

결과(strict mode 때문에 두 번씩 실행되어 원하는 결과가 나오지 않음)

Tea cup for guest #2
Tea cup for guest #4
Tea cup for guest #6

결과가 좀 이상하죠? 왜 이런 결과가 나타난 것일까요? 이것에 대해서 알아보겠습니다. 먼저 위 component는 component 바깥에서 선언된 guest 변수를 읽고 씁니다. 이는 “이 component를 여러 번 호출하면, 항상 다른 JSX를 반환할거야!” 라는 말과 같습니다. 또한 다른 component가 guest 변수를 읽는 경우 렌더링 된 시점에 따라 다른 JSX를 생성합니다. 이런 이유에서 이것은 predictable 하지 않습니다.

그럼 이제 위와 같은 문제를 지닌 component를 고쳐볼까요? 간단하게 고칠 수 있습니다. 변수 guest를 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} />
    </>
  );
}

결과

Tea cup for guest #1
Tea cup for guest #2
Tea cup for guest #3

이제 우리의 component는 순수 함수라고 할 수 있게 됐습니다! 왜냐하면 JSX가 guest prop에 의존해서 항상 같은 결과를 반환하기 때문입니다.

일반적으로, react에서는 component들이 특정 순서대로 렌더링 될 것으로 기대해서는 안 됩니다. 그리고 순수성을 잘 지켰다면, 각각의 component들은 렌더링 순서에 상관없이 동일한 input에 따른 동일한 값을 항상 반환할 것입니다. 같은 말로 “자기 자신에 대해서만 생각” 해야 하며, 렌더링 중에 다른 component에 의존하려고 시도하면 안 됩니다.

순수하지 않은 계산을 StrictMode에서 발견할 수 있습니다.
React에서는 렌더링동안 읽을 수 있는 세 가지 유형의 input(props, state and context)이 있습니다. 이러한 input은 항상 read-only로 취급해야 합니다. 이 말은 다음의 말과도 일맥상통 합니다. 만약 input에 대한 응답으로 무언가를 변경하려면 변수에 쓰는 대신 상태 자체를 set 해야 합니다(즉, 위 세 가지 유형의 input은 읽기만 가능해야하는 거예요). Component 렌더링 중에 절대로 기존 변수나 객체를 변경해서는 안 됩니다(이것이 함수의 순수성을 보장해요).

React는 Strict Mode 라는 것을 제공합니다. 이는 각 component 함수가 두 번씩 호출되게 만드는 기능인데, 개발 중 일 때만 제공됩니다. 이렇게 component 함수를 두 번씩 호출하면, component 함수의 순수성이 위배되는 것을 찾는데 도움을 줍니다.

위의 전역 guest 변수를 사용한 예시에서 보았듯이 strict mode를 사용해서, component 함수를 호출할 때마다 짝수 값이 출력되는 side effect를 확인할 수 있었습니다.

Strict mode는 production 환경에서는 실행되지 않습니다. 그러므로 실제 product 환경에서는 성능 저하를 걱정하지 않아도 괜찮습니다. Strict mode를 사용하려면 root component를 <React.StrictMode> 로 감싸면 됩니다. 몇몇의 프레임워크는 이러한 것들을 기본으로 제공합니다.

Local mutation: Component의 작은 비밀
위 예시에서의 문제는 component가 렌더링 될 때 기존에 존재하던 변수를 변경시킨다는 것이었습니다. 이러한 것을 “mutation” 이라고 합니다. 순수 함수는 범위 밖의 변수나 호출 전에 생성된 객체를 변경(mutate)하지 않습니다. 이러한 것들은 순수하지 않기 때문입니다.

그러나 렌더링 중에 생성한 변수와 객체를 변경하는 것은 전혀 문제가 없습니다. 아래의 예제에서는 [] 배열을 만들고, cups 변수에 할당한 다음 12개의 cup을 배열에 push 합니다.

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;
}

결과

Tea cup for guest #1
Tea cup for guest #2
Tea cup for guest #3
Tea cup for guest #4
Tea cup for guest #5
Tea cup for guest #6
Tea cup for guest #7
Tea cup for guest #8
Tea cup for guest #9
Tea cup for guest #10
Tea cup for guest #11
Tea cup for guest #12

만약 cup 변수 또는 [] 배열TeaGathering 함수 외부에서 만들어졌다면, 이것은 큰 문제입니다. 왜냐하면 component가 호출되기 전에 존재한 기존 객체를 변경하게 되기 때문입니다.

그러나 이 예제에서는 TeaGathering 내부에서 렌더링 시점에 생성했기 때문에 괜찮습니다. TeaGathering component 외부의 어떤 코드도 이러한 일이 발생했음을 알 수 없습니다. 이것을 “local mutation” 이라고 부르며, 이것이 component의 작은 비밀입니다.

Side Effect를 일으켜도 되는 곳
함수형 프로그래밍은 순수성에 크게 의존하지만 react에서는 어떠한 시점에 무언가가 변경되어야 합니다. 이것이 프로그래밍의 핵심입니다! 즉, 다시 말해 화면 변화, 애니메이션 시작, 데이터 변경 과 같은 side effect는 어떠한 시점에는 반드시 실행되어야 합니다. 이것들은 모두 렌더링 도중에 아니라 말 그대로 “on the side”에서 발생하는 것입니다.

React에서 side effect는 보통 event handler에 속해있습니다. Event handler는 버튼을 클릭할 때와 같이 어떤 작업을 수행할 때 react가 실행하는 함수입니다. 또한 event handler는 우리의 component 내부에서 정의되며, 렌더링 중에는 실행되지 않습니다. 그러므로 event handler는 순수할 필요가 없습니다.

React의 event handler가 "순수할 필요가 없다" 는 의미는, event handler가 react의 state나 props를 변경하거나 외부 API를 호출하는 등의 사이드 이펙트를 가질 수 있다는 것을 의미합니다.

만약 side effect에 대한 올바른 event handler를 찾을 수 없다면, component 내부에서 useEffect를 호출하여 처리할 수 있습니다. 이렇게 하면 side effect가 렌더링 후 나중에 실행하도록 react에 지시합니다. 그러나 이런 방법은 최후의 수단이어야 합니다.

React팀에서는 가능하면 렌더링만으로 로직을 표현하라고 권장합니다.

왜 react는 순수성을 신경써야하는가?
순수 함수를 작성하려면 약간의 습관과 규율이 필요합니다. 하지만 다음과 같은 장점도 존재합니다.

  • 우리의 component는 다른 환경에서 실행될 수 있습니다(예를 들어 다른 서버). 그렇기 때문에 동일한 input에 대해 동일한 결과를 반환하는 것이 중요합니다. 왜냐하면 component가 동일한 input(props, state and context)에 대해 항상 동일한 결과를 반환한다면, 그 component는 여러 사용자 요청에 대해 일관된 UI를 제공할 수 있습니다. 다시 말해, 여러 다른 상황이나 환경에서 해당 component를 사용하더라도 동일한 props를 전달하면 동일한 UI 결과를 볼 수 있습니다.

  • Input이 변경되지 않은 component는 skipping rendering(memo)기법에 의해서 성능을 향상할 수 있습니다.

  • Deep component tree를 렌더링 하는 도중 일부 데이터가 변경되면, react는 렌더링을 완료하는데 시간을 낭비하지 않고, 렌더링을 다시 시작할 수 있습니다. 순수성은 언제든지 계산을 중단해도 안전합니다. 왜냐하면 동일한 input에 대해 항상 동일한 출력을 기대할 수 있기 때문입니다. 이런 방식은 react가 효율적으로 렌더링을 수행하게 해주며, 특히 react의 concurrent mode와 같은 고급 기능에서 중요한 역할을 합니다.

    Concurrent mode에서 react는 여러 작업을 동시에 수행하면서 필요에 따라 렌더링 작업을 중단하거나 우선순위를 변경할 수 있습니다. 이런 동작은 component의 순수성이 보장될 때만 안전하게 가능합니다.

정리
Component가 순수해야 한 다의 의미는 다음과 같습니다.

  • 렌더링 전에 존재했던 변수나 객체를 변경하면 안 된다.
  • 동일한 input에 대해서는 항상 동일한 JSX가 반환되어야 합니다.

렌더링은 언제든 일어날 수 있으므로 component들은 서로의 렌더링 시퀀스에 의존하면 안 됩니다.

Component가 렌더링에 사용하는 input(props, state and context)을 변경해서는(mutate) 안 됩니다. 화면을 업데이트하기 위해서는 set state를 기존에 존재하던 객체의 mutating 대신 사용합니다.
될 수 있다면 렌더링만으로 로직을 표현하도록 노력합시다. 만약 어떠한 것을 변경하길 원한다면, 일반적으로는 event handler에서 변경하는 것이 좋습니다. 최후의 수단으로 useEffect를 사용할 수 있습니다.

순수 함수를 작성하려면 약간의 연습이 필요하지만 react 패러다임의 진정한 힘을 확인할 수 있습니다.

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

반응형

'Develop > React' 카테고리의 다른 글

Render와 Commit  (0) 2023.07.20
React 상에서 컨디션 렌더링을 위해 사용하는 "&&" 에 대한건  (0) 2022.12.19