
컴포넌트들 순수하게 유지한다는 것은 무슨 말일까?
순수하지 않은 컴포넌트는 불순한 컴포넌트라도 되는걸까?
함수형 프로그래밍을 학습하다보면 매우 중요하게 여겨지는 '함수의 순수성',
불순한 함수가 만들어내는 '사이드 이팩트',
그리고 함수의 순수성을 바탕으로 한 리액트의 '불변성'에 대해 간단히 알아보고자 한다.
순수함수(pure function) : 함수형 프로그래밍에서 어떤 외부 상태에 의존하지도 않고 변경하지도 않는(부수 효과가 없는) 함수
비순수 함수(impure function) : 외부 상태에 의존하거나 변경하는(부수 효과가 있는) 함수
이러한 비순수 함수가 외부 상태를 변경하는 것을 '사이드 이팩트', 즉 부수 효과라고 한다.
간단한 예시를 살펴보자.
// example
let num = 0;
const count = () => {
return (num += 1); // 외부 상태에 의존하며 외부 상태를 변경
};
count();
console.log(num); // 1
count();
console.log(num); // 2
이처럼 외부의 전역 변수에 의존하며, 해당 변수에 할당된 값을 변화시키는 사이드 이팩트가 발생한다.
이외의 추가적인 예시는 5번 공식문서 챌린지 코드 부분에서 다루도록 하겠다.
리액트의 불변성에 대해 이야기 하기 위해, 우선 자바스크립트 원시 타입의 불변성에 대해 알아보자.
한번 생성된 원시 값은 읽기 전용 값으로서 변경할 수 없다.
자바스크립트의 number, bigint, string, boolean, symbol, null, undefined는 원시 타입으로, 해당 값을 변경할 수 없다.
변수에 새로운 값을 할당하면 기존 값을 덮어쓰는 것이지, 원래 메모리에 저장된 값 자체를 바꾸는 것이 아니다.
이에 반해 객체(참조) 타입의 값은 변경 가능하다. 또한 객체를 변수에 할당하면 변수에는 참조 값이 저장된다.
마찬가지로, 리액트에서의 불변성은 메모리 영역에서 값을 변하게 하지 않는 것이다.
그렇다면 리액트에서의 불변성은 왜 중요한 것일까?
리액트에서 불변성이 필요한 이유
리액트는 참조 비교를 통해 상태를 확인함. 따라서 참조값이 아닌, 객체 내부 자체를 변경할 경우 리액트가 감지할 수 없음.
리액트가 상태가 변화된 것을 감지하지 못할 경우, 추적(예측)하기 어려운 단점이 있음.
이처럼 리액트에서 순수한 컴포넌트를 만들어야하는 이유는 다음과 같다.
순수 함수 : 외부 상태에 의존하지 않고 동일한 입력에 대해 항상 동일한 출력을 반환하여 예측 가능한 동작을 보장
사이드 이팩트 : 사이드 이팩트를 피하여 디버깅과 유지보수가 용이하고 코드의 안정성을 높일 수 있음
불변성 : 리액트의 상태 변경을 효율적으로 감지하고 성능을 최적화할 수 있음
순수성을 유지하기 위한 방법
1. 외부 상태에 의존하지 않기
// 순수하지 않은 컴포넌트 예시(외부 상태가 아닌 props를 활용)
let externalData = 42;
function ImpureComponent() {
return <div>{externalData}</div>;
}
// 순수한 컴포넌트 예시
function PureComponent({ data }) {
return <div>{data}</div>;
}
2. 사이드 이팩트 피하기
// 사이드 이팩트를 가진 컴포넌트 예시
function ImpureComponent() {
console.log("Component rendered");
return <div>Hello, World!</div>;
}
// 사이드 이팩트를 분리한 컴포넌트 예시
function PureComponent() {
useEffect(() => {
console.log("Component rendered");
}, []);
return <div>Hello, World!</div>;
}
3. 함수형 업데이트 사용
// 비순수 상태 업데이트 예시
function Counter() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1); // 비순수, 이전 상태에 직접 의존
}
return <button onClick={increment}>{count}</button>;
}
// 순수 상태 업데이트 예시
function Counter() {
const [count, setCount] = useState(0);
function increment() {
setCount((prevCount) => prevCount + 1); // 순수, 이전 상태를 안전하게 업데이트
}
return <button onClick={increment}>{count}</button>;
}
// 1번 문항
export default function Clock({ time }) {
let hours = time.getHours();
const nightDayClassToggle = hours >= 0 && hours <= 6 ? "night" : "day";
return (
<h1 id="time" className={nightDayClassToggle}>
{time.toLocaleTimeString()}
</h1>
);
}
// h1 태그가 렌더링 되기 전에 getElementById 수행 -> 에러 발생
// 외부(DOM)에 존재하는 값을 변경하려함
// 내부에서 연산 이후 JSX 렌더링에 해당 className을 포함해서 렌더링하도록 수정
// 2번 문항
import Panel from "./Panel.js";
import { getImageUrl } from "./utils.js";
export default function Profile({ person }) {
return (
<Panel>
<Header currentPerson={person} />
<Avatar currentPerson={person} />
</Panel>
);
}
function Header({ currentPerson }) {
return <h1>{currentPerson.name}</h1>;
}
function Avatar({ currentPerson }) {
return (
<img
className="avatar"
src={getImageUrl(currentPerson)}
alt={currentPerson.name}
width={50}
height={50}
/>
);
}
// 전역 변수로 관리되던 값에 직접 접근 -> 여러 컴포넌트에서 수정될 때마다 다른 컴포넌트에 해당 값을 공유하는 사이드 이팩트 발생
// 해당 값을 전역 변수가 아닌, props로 내려주는 방식으로 수정
// 3번 문항
export default function StoryTray({ stories }) {
const newStories = [...stories];
newStories.push({
id: "create",
label: "Create Story",
});
return (
<ul>
{newStories.map((story) => (
<li key={story.id}>{story.label}</li>
))}
</ul>
);
}
// props로 내려받은 stories array 객체에 직접 push를 하게 될 경우, StoryTray가 재렌더링 될 때마다 새로운 값을 push 하게 됨
// stories array 객체를 딥카피한 새로운 array 객체를 만들고, 해당 객체에 원하는 값을 push 해주어 렌더링해줌으로써 사이드 이펙트를 제거
순수하지 않은 렌더링으로 인해 발생하는 버그를 찾기 위해 컴포넌트가 추가로 다시 렌더링됩니다.
Effect 클린업이 누락되어 발생하는 버그를 찾기 위해 컴포넌트가 추가로 Effect를 다시 실행합니다.
더 이상 사용되지 않는 API의 사용 여부를 확인하기 위해 컴포넌트를 검사합니다.