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

컴퓨터 과학(특히 함수형 프로그래밍의 세계)에서 순수 함수는 다음과 같은 특성을 가진 함수이다.
순수 함수의 한 예인 수학 공식에 이미 익숙할 것이다.
다음 수학 공식을 고려해봐라: y = 2x
x = 2 이면 항상 y = 4.
x = 3 이면 항상 y = 6.
x = 3 이면 시간이나 주식 시장에 따라 y = 9 또는 -1 또는 2.5가 될 수 없다.
y = 2x 에서 x = 3 일때 y는 항상 6이다.
이것을 JavaScript 함수로 만들면 다음과 같다
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>
);
}
drinkers={2} 를 Recipe 에 전달하면 언제나 물 2컵이 포함된 JSX가 반환된다.
drinkers={4} 를 전달하면 언제나 물 4컵이 포함된 JSX가 반환된다.
마치 수학 공식처럼 작동한다.
컴포넌트를 레시피로 생각할 수 있다. 컴포넌트를 따르고 요리 과정에서 새로운 재료를 도입하지 않으면 매번 같은 요리를 얻을 수 있다. 그 “접시”는 컴포넌트가 렌더링을 위해 리액트에서 제공하는 JSX이다.
리액트의 렌덜이 프로세스는 항상 순수해야 한다. 컴포넌트는 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를 신뢰할 수 없다. 테스트가 실패하고 사용자가 당혹스러워하고 비행기가 하늘에서 떨어질 수 있다. 혼란스러운 버그에 이게 어떻게 이어질지 알 수 있다.
passing guest as a prop instead로 이 컴포넌트를 수정할 수 있다.
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 props에만 의존하므로 컴포넌트는 순수하다.
일반적으로 컴포넌트가 특정 순서로 렌더링될 것으로 기대해서는 안된다. y = 5x 이전이나 이후에 y = 2x를 호출하는 것은 중요하지 않는다. 두 공식은 독립적으로 작동한다. 같은 방식으로 컴포넌트는 “스스로 생각”해야 하며 렌더링 중에 다른 컴포넌트와 조정하거나 의존하려고 시도해서는 안된다. 렌더링은 학교 시험과 같다. 각 컴포넌트는 자체적으로 JSX를 계산해야 한다.
💡 DEEP DIVEStrict Mode로 비순수 함수 감지
아직 모두 사용하지는 않았지만 리액트에는 렌더링하는 동안 읽을 수 있는 세 가지 유형의 입력(props,state 및 context)이 있다. 이러한 입력은 항상 읽기 전용으로 취급해야 한다.
사용자 입력에 대한 응답으로 무언가를 변경하려면 변수를 쓰는 대신 set state를 해야한다. 컴포넌트가 렌더링되는 동안 기존 변수나 개체를 변경해서는 안된다.
리액트는 개발 중에 각 컴포넌트의 기능을 두 번 호출하는 “Strict Mode”를 제공한다. 컴포넌트 함수를 두 번 호출함으로써 엄격 모드는 이러한 규칙을 위반하는 구성 요소를 찾는 데 도움이 된다.
원래 예에서 “Guest #1”, “Guest #2”, 그리고 “Guest #3” 대신 “Guest #2”, “Guest #4”, 그리고 “Guest #6”이 어떻게 표시되었는지 확인해라. 원래 함수는 순수하지 않았으므로 두 번 호출하면 중단되었다. 하지만 고정된 순수 버전은 함수가 매번 두 번 호출되더라도 작동한다. 순수 함수는 계산만 하므로 두 번 호출해도 아무 것도 변경되지 않는다. double(2) 을 두 번 호출해도 반환되는 내용이 변경되지 않고 y = 2x를 두 번 풀면 y가 변경되지 않는 것과 같다. 언제나 동일한 입력, 동일한 출력이다.
엄격 모드는 프로덕션에 영향을 미치지 않으므로 사용자를 위해 앱 속도를 저하시키지 않는다. 엄격 모드를 선택하려면 루트 구성 요소를 <React.StrictMode> 로 래핑하면 된다. 일부 프레임워크는 기본적으로 이 작업을 수행한다.
위의 예에서 문제는 컴포넌트가 렌더링하는 동안 기존 변수를 변경했다는 것이다. 이것은 좀 더 무섭게 들리도록 종종 “mutation”이라고 한다. 순수 함수는 함수 범위 밖의 변수나 호출 전에 생성된 객체를 변경하지 않는다.
그러나 렌더링하는 동안 방금 만든 변수와 객체를 변경하는 것은 전혀 문제가 없다. 이 예제에 [] 배열을 만들고 cups 변수에 할당한 다음 12개의 컵을 배열에 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;
}
cups 변수나 [] 배열이 TeaGathering 함수 외부에서 생성된 경우 큰 문제가 된다. 항목을 해당 배열로 푸시하여 기존 개체를 변경하게 된다.
하지만 TeaGathering 내에서 동일한 렌더링 중에 생성햇기 때문에 괜찮다. TeaGathering 외부의 어떤 코드도 이러한 일이 발생했음을 알 수 없다. 이것을 “local mutation”이라고 한다. 컴포넌트의 작은 비밀과 같다.
함수형 프로그래밍은 순수에 크게 의존하지만 어느 시점, 어딘가에는 무언가가 변경되어야 한다. 그것이 프로그래밍의 요점이다. 이러한 변경사항(화면 업데이트, 애니메이션 시작, 데이터 변경)을 side effects 라고 한다. 렌더링 도중이 아니라 “측면에서” 발생하는 것이다.
리액트에서 부작용은 일반적으로 event handlers 내부에 속한다. 예를 들어 이벤트 핸들러는 버튼을 클릭할 때와 같이 어떤 작업을 수행할 때 리액트가 실행하는 함수다. 이벤트 햄들러는 컴포넌트 내부에 정의되어 있지만 렌더링 중에는 실행되지 않는다. 따라서 이벤트 핸들러는 순수할 필요가 없다.
다른 모든 옵션을 사용했고 부작용에 대한 올바른 이벤트 핸들러를 찾을 수 없는 경우 컴포넌트에서 useEffect 호출을 사용하여 반환된 JSX에 연결할 수 있다. 이렇게 하면 부작용이 허용될 때 렌더링 후 나중에 실행하도록 리액트가 지시한다. 그러나 이 방법은 최후의 수단이어야 한다.
가능하면 렌더링만으로 로직을 표현해라. 이것이 얼마나 멀리 데려갈 수 있는지 놀라게 될 것이다.
💡 DEEP DIVE리액트가 순수를 중요하게 생각하는 이유는 무엇인가?
순수 함수를 작성하려면 약간의 습관과 규율이 필요하다. 하지만 다음과 같은 놀라운 기회도 열어준다.
우리가 만들고 있는 모든 새로운 리액트 기능은 순수를 활용한다. 데이터 가져오기에서 애니메이션, 성능에 이르기까지 컴포넌트를 순수하게 유지하면 리액트 패러다임의 힘이 발휘된다.
useEffect 를 사용할 수 있다.