일부 JavaScript 함수는 순수하다. 순수 함수는 계산만 수행하고 그 이상은 수행하지 않는다. 컴포넌트를 엄격하게 순수 함수로만 작성하게 된다면 우리는 당황스러운 버그와 예측할 수 없는 동작을 피할 수 있다.
하지만 이러한 이점을 얻으려면 몇 가지 규칙을 준수해야하는데, 이제부터 알아보도록 하자.
순수 함수는 다음과 같은 특징을 갖는다.
예를 들면 수학 공식에서
y = 2x를 생각해보자.
x에 같은 입력을 전달하면 항상 동일한 출력이 반환됨을 우리는 이미 알고 있다.
React는 이 개념을 중심으로 설계되었다. React는 우리가 작성하는 모든 컴포넌트가 순수 함수라고 가정한다. 즉, 우리가 작성하는 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 />
</>
);
}
이 코드의 출력은 아래와 같다.
Tea cup for guest #2
Tea cup for guest #4
Tea cup for guest #6
이 컴포넌트는 외부에서 선언된 guest 변수를 읽고 쓰고 있기 때문에 호출될 때마다 다른 JSX가 생성된다! 게다가 다른 컴포넌트가 guest를 읽게 되면 렌더링된 시점에 따라 JSX도 다르게 생성되기 때문에 우리는 결과를 예측할 수 없게된다.
우리는 guest를 prop으로 전달해 이 컴포넌트를 정상적으로 동작하게 할 수 있다.
이제 컴포넌트가 반환하는 JSX는 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} />
</>
);
}
React에서는 렌더링하는 동안 읽을 수 있는 입력이 props, state, context 세 가지가 있다. 이러한 입력은 항상 읽기 전용으로 취급해야 한다.
사용자 입력에 대한 응답으로 무언가 변경하고자 한다면 변수에 쓰는 대신 state
를 설정해야 한다. 또, 컴포넌트가 렌더링되는 동안에는 기존 변수나 객체를 절대 변경해서는 안된다.
Reace는 개발 환경에서 각 컴포넌트의 함수를 두 번 호출하는 "Strict Mode"
를 제공한다. Strict Mode는 컴포넌트 함수를 두 번 호출함으로써 이러한 규칙을 위반하는 컴포넌트를 찾아내는 데 도움이 된다.
위의 “Guest #2”, “Guest #4”, “Guest #6”으로 출력된 결과는 두 번 호출되었기 때문에 예상과 다른 결과가 나왔던 것이다. 하지만 수정된 순수한 버전은 함수를 매번 두 번씩 호출해도 잘 동작한다. (순수 함수는 계산만 하므로 두 번 호출해도 아무 것도 바뀌지 않는다.)
Strict Mode는 상용 환경에서 아무런 영향을 미치지 않아 사용자의 앱 속도에도 영향을 끼치지 않는다. 만약 Strict Mode가 사용하고 싶다면 루트 컴포넌트를 <React.StrictMode>
로 감싸면 된다.
콘솔이 두번 출력되서 이상함을 느꼈다면 StrictMode가 적용되어있는지 살펴보자.
위의 예시에서는 컴포넌트가 렌더링하는 동안 기존 변수를 변경하는 것이 문제였다. 하지만 '방금' 생성한 변수와 객체를 변경하는것은 괜찮다.
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 외부의 어떤 코드도 이런 일이 일어났다는 것을 알 수 없기 때문에 괜찮다!
React가 순수성을 중시하긴 하지만, 어딘가에서 무언가 변경이 되어야하는 경우가 있다. 화면 업데이트, 애니메이션, 데이터 변경과 같은 이러한 변경을 사이트 이펙트라하며, 렌더링 중에 일어나는 것이 아닌 "부수적으로" 일어나는 일을 말한다.
React에서 사이드 이펙트는 보통 이벤트 핸들러
에 속한다. 이벤트 핸들러는 사용자가 어떤 동작을 수행할 때(버튼을 클릭한다던지) React가 실행하는 함수다. 이벤트 핸들러가 컴포넌트 내부에 정의되어 있긴 하지만 렌더링 중에는 실행되지 않기 때문에 이벤트 핸들러는 순수할 필요가 없다!
다른 모든 옵션을 다 사용했는데도 사이드 이펙트에 적합한 이벤트 핸들러를 찾을 수 없다면, 컴포넌트에서 useEffect
호출을 통해 반환된 JSX에 이벤트 핸들러를 첨부할 수 있다. 이렇게 하면 나중에 렌더링 후 사이드 이펙트가 허용될때 React가 이를 실행하도록 지시한다. 그리고 이 방법은 최후의 수단으로 사용해야한다.
가능하면 렌더링만으로 로직을 표현하고자 노력해보도록하자!