컴포넌트 순수하게 유지하기
자바스크립트에서 일부 함수는 순수합니다. 순수 함수는 오직 연산만을 수행합니다. 컴포넌트를 엄격하게 순수 함수로 작성하면 코드 베이스가 커져도 예상 밖의 동작이나 버그를 피할 수 있습니다. 이러한 이점들을 취하기 위해선 몇가지 규칙이 필요합니다.
- 자신의 일에 집중한다.
- 함수가 호출 되기 전에 존재했던 어떤 객체나 변수는 변경하지 않는다.
- 같은 입력, 같은 출력
- 같은 입력이 주어졌따면 순수함수는 같은 결과를 반환해야 한다.
이 수학 공식을 생각해봅시다.
만약 x = 2 이라면 항상 y = 4 입니다.
만약 x = 3 이라면 항상 = 6 입니다.
만약 x = 3 이라면, 그날의 시간이나 주식 시장의 상태에 따라서 y가 9이거나 -1이거나 2.5가 되지 않습니다.
위 내용들을 자바스크립트 함수로 만든다면 아래와 같습니다.
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} /> // 2가 들어가면 2와 연산된 값이 jsx와 함께 나온다. <h2>For a gathering</h2> <Recipe drinkers={4} /> // 4가 들어가면 4와 연산된 값이 jsx와 함께 나온다. </section> );}Recipe에 drinkers={2}를 넘기면 항상 2 cups of water를 포함한 JSX를 반환합니다.
drinkers={4}를 넘기면 항상 4 cups of water를 포함한 JSX를 반환합니다.
수학 공식처럼 말입니다.
컴포넌트를 마치 레시피처럼 생각할 수 있습니다. 만약 레시피를 그대로 따르고 요리하는 동안 새로운 재료를 도입하지 않는 이상 매번 새로운 요리를 만들 수 있겠죠. 그 요리는 React가 렌더하는데 컴포넌트가 제공하는 JSX라고 생각할 수 있습니다.
사이드 이펙트: 의도하지 않은 결과
React의 렌더링 과정은 항상 순수해야 합니다. 컴포넌트는 렌더링 전에 있었던 객체나 변수들을 변경하지 않고 JSX만 반환 해야 합니다.
let guest = 0;
function Cup() { // 나쁜 지점: 이미 존재했던 변수를 변경하고 있다! guest = guest + 1; return <h2>Tea cup for guest #{guest}</h2>;}
export default function TeaSet() { return ( <> <Cup /> <Cup /> <Cup /> </> );}이 컴포넌트는 외부에 선언된 guest라는 변수를 읽고 수정하고 있습니다. 이건 컴포넌트가 여러번 호출되면 매번 다른 JSX를 생성한다는 것을 의미합니다! 그리고 더욱이 다른 컴포넌트가 guest를 읽었다면 언제 렌더링 되었는지에 따라 그 컴포넌트 또한 다른 JSX를 생성할 겁니다. 이건 예측 불가한 영역입니다.
guest변수를 대신 프로퍼티로 넘겨 이 사이드 이펙트를 고칠 수 있습니다.
function Cup({ guest }) { return <h2>Tea cupt for guest #{guest}</h2>;}
export default function TeaSet() { return ( <> <Cup guest={1} /> <Cup guest={2} /> <Cup guest={3} /> </> );}지역 변형: 컴포넌트의 작은 비밀
위의 예시에서 문제는 컴포넌트가 렌더링 되는 동안 기존 변수를 변경했다는 것입니다. 순수 함수는 함수 스코프 밖의 변수나 호출 전에 생성된 객체를 변경하지 않아야 합니다.
그러나, 렌더링하는 동안 컴포넌트 안에서 만든 변수와 객체를 변경하는 것은 전혀 문제가 되지 않습니다. 다음 예시에선, [] 배열을 만들고, cups변수에 할당하고, 컵 한 묶음을 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안에 동일한 렌더링 영역 안에서 생성되었기 때문에 괜찮습니다. 이 현상은 “지역 변형” 이라 불립니다.
부작용을 일으킬 수 있는 지점
함수형 프로그래밍은 순수성에 의존하지만 결국 언젠가는 무언가가 어딘가에서 바뀌어야 합니다. 그것이 우리가 프로그래밍을 하는 이유이자 요점입니다! 이러한 변화들 -화면을 업데이트 하고, 애니메이션을 시작하고, 데이터를 변경하는 것을 사이드 이펙트 라고 합니다. 렌더링중에 발생하는 것이 아니라 “사이드에서” 발생하는 현상입니다.
React는 이러한 사이드 이펙트를 보통 이벤트 핸들러에 포함합니다. 이벤트 핸들러는 컴포넌트 내부에 정의되어도 렌더링 중에는 실행되지 않습니다. 그래서 이벤트 핸들러는 순수할 필요가 없습니다!
모든 옵션을 고려해봐도 사이드 이펙트에 적합한 이벤트 핸들러를 못찾은 경우, useEffect를 사용하여 반환된 JSX에 해당 이벤트 핸들러를 연결할 수 있습니다. 이것은 React가 사이드 이펙트가 허용될 때 렌더링 후 나중에 실행하도록 지시합니다. 그러나 이 접근 방식은 마지막 수단이 되어야 합니다.
가능하면 렌더링만으로 로직을 표현해야 합니다. 이것이 React를 멋지게 사용하는 방법입니다.
요약
- 컴포넌트는 순수해야 한다.
- 자신의 일에 집중한다. 컴포넌트 외부에 객체나 변수를 변경하지 않아야 한다.
- 같은 입력, 같은 결과물을 보여줘야 한다. 입력이 같으면 항상 같은 JSX를 반환해야 한다.
- 렌더링은 언제든 발생할 수 있으므로 컴포넌트는 서로의 렌더링 순서에 의존하지 말아야 한다.
- 컴포넌트가 렌더링을 위해 사용되는 입력을 변형해선 안된다. 여기엔 Props, State, Context가 포함된다. 화면을 업데이트 하려면 기존 객체를 변환하는 대신 상태를 설정하자.
- 반환하는 JSX에서 컴포넌트의 로직을 표현하기 위해 노력하자. 무언가를 변경할때 최후의 수단으로
useEffect를 사용하자 - 순수 함수를 작성하는 건 어렵고 힘들다. 하지만 React의 컴포넌트 패러다임에 가장 적절한 방식이다.
참조
- React 공식 문서