Describing the UI - Keeping Components Pure

hyocho·2023년 5월 28일
0

React

목록 보기
19/24
post-thumbnail

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

몇 자바스크립트 함수는 순수하다. 순수 함수는 오직 계산을 수행할 뿐이다. 컴포넌트를 엄격하게 순수함수로만 작성하면 코드 베이스가 증가함에 따라 전체 클래스의 혼란스러운 버그와 예측할 수 없는 동작을 방지할 수 있다. 그러나 이러한 이점을 얻기 위해서는 몇 가지 룰을 따라야만 한다.

배울 것

  • 순수성이란 무엇이고 그것이 어떻게 버그를 피하게 하는지
  • 변경 사항을 렌더 단계에서 제외하여 컴포넌트를 순수하게 유지하는 방법
  • 엄격 모드를 사용하여 컴포넌트의 오류를 찾는 방법

Purity: Components as formulas

컴퓨터 사이언스에서 (특히 프로그래밍 세계에서) 순수 함수는 다음의 특성을 갖는 함수이다.

  • 자신의 일만 신경쓴다. 호출되기 전에는 존재하는 어떠한 객체나 변수도 변경되지 않는다.
  • 입력이 같으면 출력도 같다. 같은 입력이 주어지면 순수 함수는 항상 같은 결과를 반환해야 한다.

여러분은 이미 순수 함수의 한 예인 수학 공식에 익숙할 것이다.

y=2x라는 계산식을 생각해보자.
x=2이면 항상 y=4이다.
x=3이면 항상 y=6이다.
x=3인 경우, y는 때때로 시간 또는 주식 시장의 상태에 따라 9, -1 또는 2.5가 아니다.

y=2x 그리고 x=3이면 y는 항상 6이다.
만약 이것을 자바스크립트 함수로 만들면 이런 형태일 것이다.

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

위의 예시에서 double순수 함수이다. 3을 넘겨주면 항상 6을 반환할 것이다.

리액트는 이 컨셉을 기초로 설계되었다. 리액트는 당신이 만든 컴포넌트는 모두 순수 함수라고 가정한다. 이것은, 컴포넌트는 같은 입력값을 받으면 같은 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>
  );
}

Receiptdrinkers={2}를 전달하면 항상 2 cups of water가 포함된 JSX가 반환될 것이다.

drinkers={4}를 전달하면 항상 4 cups of water가 포함된 JSX가 반환될 것이다.

마치 수학 공식과 같다.

컴포넌트를 조리법이라고 생각할 수 있다 : 만약 그것들을 따르고 요리 과정에서 새로운 재료를 도입하지 않는다면, 매번 같은 요리를 얻을 것이다. 이 "요리"는 컴포넌트가 리액트에 렌더링하는 JSX이다.

Side Effects: (un)intended consequences

리액트의 렌더링 과정은 언제나 순수해야 한다. 컴포넌트는 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도 생성된다. 그것은 예측 불가능하다.

y=2x의 공식으로 돌아가서, 이제 x=2이더라도 y=4인 것을 믿을 수 없다. 우리의 테스트는 실패할 수도 있고, 유저들은 당황할 수도 있고, 비행기가 하늘에서 떨어질 수도 있다. 이것이 어떻게 혼란스러운 버그로 이어질지 알 수 있다.

이 컴포넌트는 대신 guestprop으로 전달해주는 것으로 고칠 수 있다.

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에 따라서만 반환되므로 컴포넌트는 순수하다.

일반적으로, 컴포넌트가 어떤 특정한 순서에 따라 렌더링된다고 생각하면 안된다. y=2x 이전이나 이후에 y=5x를 실행하는 것은 아무런 문제가 되지 않는다. 두 공식은 독립적으로 실행된다. 마찬가지로 각 컴포넌트는 "각각 생각"해야 하며 렌더링 중에 다른 컴포넌트와 조정하거나 의존하지 않아야 한다. 렌더링은 학교 시험과 같다. 각 컴포넌트가 스스로 JSX를 계산해야 한다.

DEEP DIVE

Detecting impure calculations with StrictMode

아직 모두 사용하지 않았을 수 있지만 리액트에서는 렌더링하는 동안 읽을 수 있는 세 가지 유형의 입력이 있다. (props, state, context) 이러한 입력은 항상 읽기 전용으로 처리해야 한다.

유저 입력에 따른 응답으로 무언가를 바꾸고 싶다면 변수를 사용하는 것 대신에 set state를 사용해야 한다. 컴포넌트가 렌더링 되기 이전에 존재했던 변수나 객체를 변화시키면 절대로 안된다.

리액트는 각 컴포넌트의 함수들을 개발 단계에서 두 번 호출하는 "엄격 모드"를 제공한다. 함수를 두 번 호출함으로써, 이 규칙을 위반하는 컴포넌트를 찾는 것을 도와준다.

엄격 모드는 운영에 영향을 미치지 않으므로 사용자의 앱 속도를 늦추지 않는다. 엄격 모드로 설정하려면 루트 구성 요소를 <React.StrictMode>로 감쌀 수 있다. 일부 프레임워크는 기본적으로 이 작업을 수행한다.

Local mutation: Your component’s little secret

위의 예시에서, 컴포넌트가 이미 존재하는 변수를 렌더링하는 동안 변경시킨 것이 문제였다. 이것은 종종 그것을 조금 더 무섭게 들리게 하기 위해 "mutation" 라고 불린다. 순수 함수는 함수나 객체의 스코프 바깥에서 호출되기 이전에 만들어진 변수를 mutate하지 않는다. 그것은 불순하게 만들기 때문이다.

그러나 렌더링동안 생성된 변수나 객체를 변경시키는 것은 전혀 상관없다. 이 예시에서 빈 배열 []을 생성하고, cups 변수에 할당한 다음 12개의 cup을 밀어넣는다.

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 effects라고 한다. 렌더링 동안이 아닌 측면(on the side)에서 발생하는 작업이다.

리액트에서 side effects는 대개 이벤트 핸들러 내부에 속해있다. 이벤트 핸들러는 버튼을 클릭한다든지, 어떤 액션을 수행했을 때 리액트를 실행하는 함수이다. 이벤트 핸들러는 컴포넌트 내부에서 정의되지만 렌더링 동안 실행되지 않는다! 따라서 이벤트 핸들러는 순수할 필요가 없다.

다른 옵션을 모두 사용했지만 side effects에 적합한 이벤트 핸들러 요소를 찾을 수 없다면, 컴포넌트에서 useEffect 호출을 사용하여 반환된 JSX에 연결할 수 있다. 이것은 리액트가 렌더링 후에 실행하도록 한다. 하지만 이 접근은 마지막 수단이 되어야 한다.

DEEP DIVE

Why does React care about purity?

순수 함수를 작성하는 것은 약간의 습관과 훈련이 필요하지만 그것은 놀라운 이점이 있기도 하다.

  • 컴포넌트는 서버와 같이 다른 환경에서 실행될 수 있다. 순수 함수는 같은 입력에는 같은 결과를 반환하므로 한 컴포넌트는 많은 유저 요청을 처리할 수 있다.
  • 입력이 변경되지 않은 컴포넌트의 렌더링을 생략하여 성능을 향상시킬 수 있다. 순수함수는 항상 동일한 결과를 반환하므로 캐시에 안전하다.
  • 깊은 컴포넌트 트리 중간에서 데이터가 변화했다면, 리액트는 오래된 렌더링을 완료하는 데 시간 낭비를 하지않고, 렌더링을 다시 시작할 수 있다. 순수성은 언제든지 계산을 중단하는 것을 안전하게 한다.

우리가 구축하는 모든 새로운 리액트 기능은 순수성을 활용한다. 데이터 페칭에서 퍼포먼스를 위한 애니메이션까지 컴포넌트를 순수하게 유지하는 것은 리액트 패러다임의 힘을 해제한다.

Recap

  • 컴포넌트는 순수해야 한다.
    • 자신의 일만 신경쓴다. 호출되기 전에는 존재하는 어떠한 객체나 변수도 변경되지 않는다.
    • 입력이 같으면 출력도 같다. 같은 입력이 주어지면 컴포넌트는 항상 같은 JSX를 반환한다.
  • 렌더링은 어느 때나 일어날 수 있는데 컴포넌트는 서로의 렌더링 시퀀스에 의존하면 안된다.
  • 렌더링하기 위한 어떠한 입력(props, state, context)도 mutate시키면 안된다. 화면을 업데이트 하기 위해서는 존재하는 객체를 mutate하는 대신 "set" state를 사용해야 한다.
  • 반환하는 JSX에서 컴포넌트의 논리를 표현하도록 노력해야 한다. '변경'이 필요한 경우, 일반적으로 이벤트 핸들러에서 변경하기를 원할 것이다. 최후의 수단으로 useEffect를 사용할 수 있다.
  • 순수 함수를 쓰는 것은 약간의 연습이 필요하지만 리액트의 패러다임의 힘을 해제시킨다.
profile
기록하는 습관을 기르고 있습니다.

0개의 댓글