SPACE RUMI

Hi, I am rumi. Let's Splattack!

[STUDY] 스터디/React Docs

React docs beta 한글 번역 : Keeping Components Pure - 컴포넌트 순수성 유지

백루미 2023. 2. 21. 01:32
반응형

UI 묘사 : 컴포넌트 순수성 유지

일부 자바스크립트 함수는 순수 함수입니다. 순수 함수는 계산만 수행하고 그 이상은 수행하지 않습니다. 컴포넌트를 순수 함수로만 엄격하게 작성하면 코드베이스가 커짐에 따라 당황스러운 버그와 예측할 수 없는 동작을 피할 수 있습니다. 하지만 이러한 이점을 얻으려면 몇 가지 규칙을 준수해야 합니다.

 

학습 내용

  • 순수성이란 무엇이며 버그를 방지하는 데 도움이 되는 방법
  • 렌더링 단계에서 변경 사항을 제외하여 컴포넌트를 순수하게 유지하는 방법
  • 엄격 모드를 사용하여 컴포넌트에서 실수를 찾는 방법

 

순도: 수식으로서의 컴포넌트

컴퓨터 과학(특히 함수형 프로그래밍의 세계)에서 순수 함수는 다음과 같은 특징을 가진 함수입니다:

자기 일만 합니다. 함수가 호출되기 전에 존재했던 객체나 변수를 변경하지 않습니다.
동일한 입력, 동일한 출력. 동일한 입력이 주어지면 순수 함수는 항상 동일한 결과를 반환해야 합니다.
순수 함수의 한 가지 예로 수학의 공식을 이미 잘 알고 계실 것입니다.

다음 수학 공식을 생각해 보겠습니다. 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을 반환합니다. 언제나.

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

 

drinkers={2}를 Recipe에 전달하면 물 2컵이 들어있는 JSX를 반환합니다. 항상.
drinkers={4}를 전달하면 물 4컵이 들어있는 JSX를 반환합니다. 항상.

수학 공식처럼 말이죠.

컴포넌트를 를 레시피라고 생각할 수 있습니다. 
레시피를 따르고 요리 과정에서 새로운 재료를 추가하지 않으면 매번 같은 요리를 얻을 수 있습니다. 
그 "요리"는 컴포넌트가 React가 렌더링하는 데 사용하는 JSX입니다.

 

사이드이펙트: (의도하지 않은) 결과 

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

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

y = 2x라는 공식으로 돌아가서, 이제 x = 2라고 해도 y = 4라고 믿을 수 없습니다. 테스트가 실패하고, 사용자가 당황하고, 비행기가 하늘에서 떨어질 수 있습니다. 이것이 어떻게 혼란스러운 버그로 이어지는지 알 수 있습니다!

이 컴포넌트를 대신 props로 게스트(guest)를 전달하여 수정할 수 있습니다:

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는 게스트 props에만 의존하므로 순수합니다.

일반적으로 컴포넌트가 특정 순서로 렌더링될 것이라고 예상해서는 안 됩니다. y = 2x를 y = 5x보다 먼저 호출하든 뒤에 호출하든 상관없습니다. 두 수식은 서로 독립적으로 해결되기 때문입니다. 마찬가지로 각 컴포넌트는 렌더링 중에 다른 컴포넌트와 조율하거나 의존하지 말고 "스스로 생각"해야 합니다. 렌더링은 학교 시험과 같아서 각 컴포넌트가 스스로 JSX를 계산해야 합니다!

 

Deep Dive - StrictMode로 불순한 계산 감지하기

아직 다 사용해보지는 않았겠지만, React에서는 렌더링하는 동안 읽을 수 있는 세 가지 종류의 입력이 있습니다: props, state, context. 이러한 입력은 항상 읽기 전용으로 취급해야 합니다.

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

React는 개발 중에 각 컴포넌트의 함수를 두 번 호출하는 "Strict 모드"를 제공합니다. 컴포넌트 함수를 두 번 호출함으로써 엄격 모드는 이러한 규칙을 위반하는 컴포넌트를 찾는 데 도움이 됩니다.

원래 예제에서 "Guest #1", "Guest #2", "Guest #3" 대신 "Guest #2", "Guest #4", "Guest #6"이 어떻게 표시되었는지 주목하세요. 원래 함수는 불완전했기 때문에, 두 번 호출하면서 함수가 손상되었습니다. 하지만 수정된 순수 버전은 함수를 매번 두 번 호출해도 작동합니다. 순수 함수는 계산만 하므로 double(2)를 두 번 호출해도 반환되는 값이 바뀌지 않고, y = 2x를 두 번 풀어도 y가 바뀌지 않는 것처럼 두 번 호출해도 아무 것도 변경되지 않습니다. 같은 입력, 같은 출력. 항상 그렇습니다.

엄격 모드는 프로덕션 환경에서는 아무런 영향을 미치지 않으므로 사용자의 앱 속도가 느려지지 않습니다. Strict 모드를 선택하려면 루트 컴포넌트를 <React.StrictMode>로 감싸면 됩니다. 일부 프레임워크는 기본적으로 이 작업을 수행합니다.

 

로컬 변이: 컴포넌트의 작은 비밀 

위의 예시에서는 컴포넌트가 렌더링하는 동안 기존 변수를 변경하는 것이 문제였습니다. 이를 좀 더 무섭게 들리기 위해 '변이'라고 부르기도 합니다. 순수 함수는 함수의 범위를 벗어난 변수나 호출 전에 생성된 객체를 변경하지 않으므로 불순합니다!

하지만 렌더링하는 동안 방금 생성한 변수와 객체를 변경하는 것은 완전히 괜찮습니다. 이 예제에서는 [] 배열을 생성하여 컵 변수에 할당하고 컵 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;
}

컵 변수나 [] 배열이 TeaGathering 함수 외부에서 생성되었다면 큰 문제가 될 수 있습니다! 해당 배열에 항목을 밀어 넣음으로써 기존 객체를 변경하게 될 테니까요.

하지만 동일한 렌더링 중에 TeaGathering 내부에서 생성했기 때문에 괜찮습니다. TeaGathering 외부의 어떤 코드도 이런 일이 발생했다는 사실을 알 수 없습니다. 이를 "로컬 변이"라고 하는데, 컴포넌트의 작은 비밀과도 같습니다.

 

사이드이펙트(부작용)를 일으킬 수 있는 곳 

함수형 프로그래밍은 순수성에 크게 의존하지만, 언젠가는 어딘가에서 무언가를 변경해야 합니다. 이것이 바로 프로그래밍의 핵심입니다! 화면 업데이트, 애니메이션 시작, 데이터 변경 등 이러한 변경 사항을 부작용이라고 합니다. 렌더링하는 동안이 아니라 "부수적으로" 일어나는 일들입니다.

React에서 사이드 이펙트는 보통 이벤트 핸들러 안에 속합니다. 이벤트 핸들러는 사용자가 어떤 액션을 수행할 때(예를 들어 버튼을 클릭할 때) React가 실행하는 함수입니다. 이벤트 핸들러가 컴포넌트 내부에 정의되어 있더라도 렌더링 중에는 실행되지 않습니다! 따라서 이벤트 핸들러는 순수할 필요가 없습니다.

다른 모든 옵션을 다 사용했는데도 사이드 이펙트에 적합한 이벤트 핸들러를 찾을 수 없는 경우에도 컴포넌트에서 useEffect 호출을 통해 반환된 JSX에 이벤트 핸들러를 첨부할 수 있습니다. 이렇게 하면 나중에 렌더링 후 부작용이 허용될 때 React가 이를 실행하도록 지시합니다. 하지만 이 접근법은 최후의 수단이어야 합니다.

가능하면 렌더링만으로 로직을 표현하는 것이 좋습니다. 이렇게 하면 얼마나 멀리 갈 수 있는지 놀랄 것입니다!

 

Deep Dive - React가 순수성을 중요하게 생각하는 이유는 무엇인가요?

순수 함수를 작성하려면 약간의 습관과 훈련이 필요합니다. 하지만 이는 또한 놀라운 기회를 열어줍니다:

컴포넌트를 다른 환경(예: 서버)에서 실행할 수 있습니다! 
동일한 입력에 대해 동일한 결과를 반환하므로 하나의 컴포넌트로 여러 사용자 요청을 처리할 수 있습니다.
입력이 변경되지 않은 컴포넌트의 렌더링을 건너뛰면 성능을 향상시킬 수 있습니다. 
순수 함수는 항상 동일한 결과를 반환하므로 캐싱해도 안전합니다.


딥 컴포넌트 트리를 렌더링하는 도중에 일부 데이터가 변경되는 경우, React는 오래된 렌더링을 완료하기 위해 시간을 낭비하지 않고 렌더링을 다시 시작할 수 있습니다. 순수함수는 언제든 계산을 중단해도 안전합니다.
우리가 구축하는 모든 새로운 React 기능은 순수성을 활용합니다. 데이터 불러오기부터 애니메이션, 성능에 이르기까지 컴포넌트를 순수하게 유지하면 React 패러다임의 힘을 발휘할 수 있습니다.

 

요약

  • 컴포넌트는 순수해야 합니다:
    • 컴포넌트는 자기 일에만 신경 써야 합니다. 렌더링 전에 존재했던 객체나 변수를 변경하지 않아야 합니다.
    • 동일한 입력, 동일한 출력. 동일한 입력이 주어지면 컴포넌트는 항상 동일한 JSX를 반환해야 합니다.
  • 렌더링은 언제든지 발생할 수 있으므로 컴포넌트는 서로의 렌더링 순서에 의존해서는 안 됩니다.
  • 컴포넌트가 렌더링에 사용하는 입력을 변경해서는 안 됩니다. 여기에는 props, state 및 context가 포함됩니다.
    화면을 업데이트하려면 기존 오브젝트를 변경하는 대신 state를 'set'하세요.
  • 컴포넌트의 로직을 반환하는 JSX을 표현하기 위해 노력하세요. "무언가를 변경"해야 할 때는 일반적으로 이벤트 핸들러에서 이 작업을 수행해야 합니다. 최후의 수단으로Effect를 사용할 수 있습니다.
  • 순수 함수를 작성하는 데는 약간의 연습이 필요하지만, React 패러다임의 힘을 발휘할 수 있습니다.

 

과제 1/3: 고장난 시계 수정하기 

이 컴포넌트는 자정부터 아침 6시간까지 시간대에는 <h1>의 CSS 클래스를 "night"로, 그 외 시간대에는 "day"로 설정하려고 합니다. 하지만 작동하지 않습니다. 이 컴포넌트를 수정할 수 있나요?

컴퓨터의 표준 시간대를 일시적으로 변경하여 솔루션이 작동하는지 확인할 수 있습니다. 현재 시간이 자정에서 아침 6시 사이인 경우 시계의 색상이 반전되어야 합니다!

 

과제 2/3: 깨진 프로필 수정 

두 개의 프로필 컴포넌트가 서로 다른 데이터로 나란히 렌더링됩니다. 첫 번째 프로필에서 '접기'를 누른 다음 '펼치기'를 누릅니다. 이제 두 프로필에 동일한 사람이 표시되는 것을 확인할 수 있습니다. 이것은 버그입니다.

버그의 원인을 찾아서 수정하세요.

 

과제 3/3: 고장난 스토리 트레이 수정하기 

회사 CEO가 온라인 시계 앱에 "스토리"를 추가해 달라고 요청하는데 거절할 수 없습니다. 스토리 목록을 받아들이는 스토리 트레이 구성 요소와 그 뒤에 "스토리 만들기" 플레이스홀더를 작성했습니다.

props로 받은 스토리 배열의 끝에 가짜 스토리를 하나 더 푸시하여 "Create Story" 플레이스홀더를 구현했습니다. 하지만 어떤 이유에서인지 "스토리 만들기"가 두 번 이상 표시됩니다. 문제를 해결합니다.

 

발표 요약)

1. 리액트의 엄격 모드(Strict mode)는 함수를 2번 호출하는데, 이는 규칙을 위반하는 컴포넌트를 찾는데 도움이된다. 
엄격모드를 쓰려면 루트 컴포넌트를 <React.StrictMode>로 감싸면 된다.

2. props, state, context는 항상 읽기전용으로 사용해야한다. 사용자 응답에 의해 바뀌는 값이 있다면 setState를 쓴다.

3. 컴포넌트 내에 선언한 변수나 객체는, 컴포넌트가 렌더링하는동안 생성하고 사용되기때문에 변경해도 된다. 왜냐면 렌더링은 각자 도니까 외부의 코드는 이런 일이 발생했다는것을 알 수 없다(로컬 변이 허용)

4. 이벤트 핸들러는 렌더링중에 실행되지 않는다. 따라서 이벤트핸들러는 순수할필요가 없다. 사용자 응답에 의해 뭔가를 변경해야할때는 일반적으로 이벤트 핸들러에서 작업을 수행한다. useEffect를 사용할 수 있는데 이는 최후의 수단이므로 지양한다. 

5. 순수함수로 컴포넌트를 작성하면, 렌더링을 중단하고 재렌더링을 해도 안전하며, 항상 동일한 결과를 리턴하므로 캐싱해도 안전하다.
반응형