📌 순수 함수란?
- 호출되기 전에 존재했던 기존의 객체나 변수를 변경하지 않는 함수
- 동일한 입력이 주어지면 항상 동일한 결과를 반환하는 함수
- 순수 함수는 수학 공식, 요리 레시피와 같다.
- 같은 숫자 또는 같은 재료가 들어가면 같은 연산 결과, 같은 요리(= JSX) 완성
// 예: 3을 전달하면 언제나 6을 반환
function double(number) {
return 2 * number;
}
- React는 모든 컴포넌트가 순수 함수라고 가정
- 즉, 동일한 입력이 주어졌을 때 항상 동일한 JSX를 반환해야
📌 Side Effect에 관해
- 컴포넌트는 오직 JSX만을 반환해야 함
- JSX 계산 시 렌더링 전에 존재했던 기존의 객체나 변수를 변경해선 안 됨
예시
순수하지 않은 컴포넌트
let guest = 0;
function Cup() {
// 🚫 Bad: 기존에 존재하던 변수를 변경
guest = guest + 1;
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup />
<Cup />
<Cup />
</>
);
}
- 외부에서 선언된 guest 변수를 읽고 변경하는 컴포넌트
- 호출할 때마다(= 렌더링 시점에 따라) JSX가 다르게 생성됨
- 다른 컴포넌트가 guest를 읽거나 변경할 가능성 배제할 수 없음
컴포넌트가 순수하지 않으면 생기는 부작용
- 테스트 코드 결과를 믿을 수 없음
- 사용자가 사용하다가 어떤 일이 발생할 지 개발자가 예측할 수 없음
순수한 컴포넌트
- 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} />
</>
);
}
- 이제 JSX의 변경은 guest prop에만 의존 (= 순수, 예측 가능)
- 일반적으로 컴포넌트는 호출 시점에 상관 없이 렌더링 결과가 일정해야 함
- 마치 수학 공식처럼 각각의 연산 과정이 서로에게 영향을 주지 않아야 함
🌊 Deep Dive: StrictMode
- React 렌더링에 필요한 값: props, state, context
- 무언가를 변경하려면 항상 state를 설정하고 setState로 변경
- 컴포넌트가 렌더링되는 동안에는 기존 변수나 객체를 절대 변경해선 안 됨
React가 제공하는 Strict Mode
- 개발 환경에서 각 컴포넌트의 함수를 두 번씩 호출하게 하는 모드
- 첫 호출과 두 번째 호출의 결과를 비교하여 순수하지 않은 함수(컴포넌트)를 찾아냄
- guest 예제에서 원래 함수는 불완전했기 때문에 두 번 호출하면 함수가 손상되었음
- 하지만 수정된 순수한 버전은 함수를 두 번씩 호출해도 잘 동작함
- 순수 함수는 같은 입력에 같은 결과만을 반환하므로 두 번씩 호출해도 아무 것도 바뀌지 않음
🙋♀️ Good to know
- Strict Mode는 사용자가 실제 사용하는 배포 환경에서는 동작하지 않음
- Strict Mode 설정: root 컴포넌트를 <React.StrictMode>로 감싸기
📌 컴포넌트의 지역 변이 (local mutation)
- 변이(mutation)
- 순수 함수는 외부 변수나 호출 전에 생성된 객체를 '변이'하지 않음
그러나 '렌더링 동안 생성된' 변수와 객체를 변경하는 것은 가능
(스코프의 차이)
예시
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 바깥의 어떤 컴포넌트도 TeaGathering 내부의 일을 알 수 없음 => 지역 변이(local mutation)
🙋♀️ 대표적인 사이드 이펙트: 이벤트 핸들링
- 화면 업데이트, 애니메이션 등
- 렌더링 중에 일어나는 것이 아니라 (렌더링 후에) “부수적으로” 일어나는 일 => 사이드 이펙트
- React에서 사이드 이펙트는 보통 이벤트 핸들러에 속함
- 이벤트 핸들러는 사용자가 특정 동작을 수행할 때 실행하는 함수
- 컴포넌트 내부에 정의되어 있긴 하지만 렌더링 중에는 실행되지 않음
=> 이벤트 핸들러는 순수할 필요가 없음
- useEffect를 통해 반환된 JSX에 이벤트 핸들러를 첨부할 수 있지만 이 방법은 되도록 안 쓰는 게 좋음 (최후의 수단)
- 가능하면 렌더링만으로 로직을 표현하고자 노력해 보자
🌊 Deep Dive: React는 왜 순수성을 중시할까?
순수 함수의 장점
- 컴포넌트를 다른 환경(예: 서버)에서 실행할 수 있음
- 하나의 컴포넌트가 많은 사용자 요청을 처리할 수 있음
- '렌더링 건너뛰기(캐싱)'를 통해 성능을 향상시킬 수 있음
- 항상 동일한 결과를 반환하므로 캐싱을 해도 안전
React 렌더링의 동작 방식
- 렌더링하는 도중에 일부 데이터가 변경되면 React는 오래된 렌더링을 완료하지 않고 렌더링을 다시 시작
- 이 과정은 컴포넌트의 순수성 덕분에 언제든지 계산을 중단해도 안전
📌 요약
- 렌더링 중에 컴포넌트의 외부 변수가 변경되면 안 됨
- 기존 객체를 변이하는 대신 setState 사용
- 컴포넌트 '내부'에서 렌더링 '중에' 생성 및 변경 (= 지역 변이)
- 대표적 사이드 이펙트인 이벤트 핸들러는 렌더링 '후에' 발생하므로 순수할 필요 없음
- 사이드 이펙트를 위해 useEffect를 사용할 수도 있으나 최후의 수단