티스토리 뷰
순수 함수
순수 함수는 두 가지 조건을 만족해야 한다.
- 참조 투명성(Referential Transparency)
- 부작용 없음(No Side Effects)
아래 함수는 순수 함수라고 할 수 있다.
function add(a, b) {
return a + b;
}
동일한 인자(`a`, `b`)가 주어지면 항상 동일한 결과를 반환한다. 또 이 함수의 결과가 외부의 상태를 변경하지 않는다.
반면 아래 함수는 순수 함수라고 할 수 없다.
let count = 0;
function increase() {
return ++count;
}
함수 외부의 변수 `count`에 의존하고 있기 때문에 동일한 인자가 주어지더라도 다른 값을 반환할 수 있다. 다른 함수에서 `counter`에 접근할 수 있기 때문이다.
따라서 순수 함수는 입력에만 의존하고, 외부의 상태를 변경하지 않는다. 항상 동일한 값을 반환하기 때문에 코드의 동작을 예측할 수 있고, 테스트하기 쉽다. 병렬 처리가 가능하고, 불변성을 유지하는 데 도움이 된다.
순수한 컴포넌트
React는 이와 같은 컨셉을 기반으로 설계되었다. 따라서 React는 작성되는 모든 컴포넌트가 순수 함수일 것으로 가정한다. 즉, 같은 입력이 주어진다면 반드시 같은 JSX를 반환함을 의미한다.
React에서 컴포넌트의 순수성이 중요한 이유는 다음과 같다.
- 순수 함수는 동일한 결과를 반환하므로 캐시 처리하기 안전하다. 입력이 변경되지 않았다면 동일한 결과를 반환하므로 렌더링을 건너뛸 수 있다.
- 컴포넌트 트리를 렌더링하는 도중 일부 데이터가 변경되는 경우 기존의 렌더링을 중간하고 렌더링을 다시 시작할 때 컴포넌트의 순수성이 보장된다면 연산을 중단하더라도 안전하다.
- 컴포넌트가 서버에서 실행된다면(SSR) 동일한 props에 대해 항상 동일한 UI(HTML)를 생성하게 된다. 따라서 클라이언트에서 동일한 props로 컴포넌트를 재사용할 때 일관된 결과를 보장할 수 있다.
공식 문서 챌린지
Keeping Components Pure 파트의 챌린지를 풀어보자.
Fix a broken clock
type ClockProps = {
time: Date
}
export default function Clock({ time }: ClockProps): React.ReactElement {
let hours: number = time.getHours();
if (hours >= 0 && hours <= 6) {
document.getElementById('time').className = 'night';
} else {
document.getElementById('time').className = 'day';
}
return (
<h1 id="time">
{time.toLocaleTimeString()}
</h1>
);
}
자정부터 아침 6시까지는 `h1` 요소의 class를 `night`로 지정하고, 그 외의 시간에는 `day`로 설정하려 한다. 그러나 이 컴포넌트는 동작하지 않는다. 기본적으로 렌더링은 연산이지 무언가를 실행하려 해서는 안된다. 즉, 렌더링 과정은 순수 계산으로 이루어져야 한다. 문제의 코드에서 발생할 수 있는 사이트 이펙트는 무엇일까?
우선 React는 가상 DOM 기반의 렌더링 매커니즘을 바탕으로 한다. 여기에 `document.getElementById('time').className = ''`과 같이 실제 DOM에 직접적으로 접근하게 되면 리렌더링을 하는 동안 예상하지 못한 동작이나 성능적인 문제를 일으킬 수 있다.
또 컴포넌트를 초기 렌더링할 때 DOM 요소가 없다. 컴포넌트의 반환값이 아직 DOM에 적용되지 않은 시점에 `document.getElementById('time')`을 호출하면 해당 요소는 아직 존재하지 않으므로 `null`을 반환하고 그 이후의 `.className`과 같은 접근은 오류를 발생시킨다.
위 코드를 React의 패러다임에 맞게 수정하려면 우선 className이 연산에 포함되어야 한다.
export default function Clock({ time }: ClockProps): React.ReactElement {
let hours: number = time.getHours();
let className: string;
if (hours >= 0 && hours <= 6) {
className = "night";
} else {
className = "day";
}
return (
<h1 className={className}>
{time.toLocaleTimeString()}
</h1>
);
}
그러면 `className`은 `time`이라는 props에만 의존한다. 즉 동일한 `time`에 대해서 동일한 `className`을 반환한다. 또, 직접 DOM에 접근하는 것이 아니라 계산이 완료된 `className`이 DOM 요소의 속성이 되기 때문에 외부 상태에 대한 의존성이 제거된다.
Fix a broken profile
function App() {
return( <>
<Profile person={{
imageId: 'lrWQx8l',
name: 'Subrahmanyan Chandrasekhar',
}} />
<Profile person={{
imageId: 'MK3eW3A',
name: 'Creola Katherine Johnson',
}} />
</>)
}
export default App;
type PersonType = {
imageId: string,
name: string
}
type ProfileProps = {
person: PersonType
}
let currentPerson: PersonType;
export default function Profile({ person }: ProfileProps) {
currentPerson = person;
return (
<Panel>
<Header />
<Avatar />
</Panel>
)
}
function Header() {
return <h1>{currentPerson.name}</h1>;
}
function Avatar() {
return (
<img
className="avatar"
src={getImageUrl(currentPerson)}
alt={currentPerson.name}
width={50}
height={50}
/>
);
}
두 개의 `Profile` 컴포넌트가 서로 다른 데이터로 나란히 렌더링된다. 첫 번째 프로필에서 Collapse를 누른 다음 Expand를 누르면 두 프로필에 동일한 프로필이 표시된다. 버그의 원인은 무엇일까?
작성된 컴포넌트의 하이라키를 보면 최상위 컴포넌트인 `App.tsx`에서 `Profile` 컴포넌트를 나란히 호출하고 있다. 따라서 기대되는 결과는 화면에서 두 개의 `Profile` 컴포넌트가 독립적으로 동작하는 것이다. `App.tsx`의 코드를 보면 각각의 `Profile` 컴포넌트에 서로 다른 데이터를 넘겼음에도 렌더링이 다시 수행되면서 동일한 데이터를 넘겨 받은 것처럼 동작한다.
`Profile` 컴포넌트에서 참조하고 있는 `currentPerson`는 `Profile` 컴포넌트 외부에서 글로벌 변수로 선언되어 있다. 따라서 `Profile` 컴포넌트가 렌더링 될 때마다 `currentPerson`에는 새로운 `person` 객체가 할당된다. 즉 `Profile` 컴포넌트가 여러 개 있다면 각각의 컴포넌트가 렌더링 될 때마다 `currentPerson`은 덮어씌워지고, 마지막으로 렌더링된 `Profile`의 `person` 데이터를 들고 있게 된다. 결과적으로 문제의 코드는 모든 `Profile` 컴포넌트가 하나의 상태를 공유하고 있는 것과 마찬가지다.
이 문제를 해결하며면 모든 `Profile` 컴포넌트가 자신의 props를 독립적으로 관리하도록 수정해야 한다.
export default function Profile({ person }: ProfileProps) {
return (
<Panel>
<Header person={person}/>
<Avatar person={person}/>
</Panel>
)
}
function Header({person}: ProfileProps) {
return <h1>{person.name}</h1>;
}
function Avatar({person}: ProfileProps) {
return (
<img
className="avatar"
src={getImageUrl(person)}
alt={person.name}
width={50}
height={50}
/>
);
}
그러면 각 `Profile` 컴포넌트는 독립적으로 자신이 받은 props인 `person`을 바탕으로 렌더링된다.
Fix a broken story tray
export default function StoryTray({ stories }: StoryTrayProps) {
stories.push({
id: 'create',
label: 'Create Story'
});
return (
<ul>
{stories.map(story => (
<li key={story.id}>
{story.label}
</li>
))}
</ul>
);
}
let initialStories: StoryType[] = [
{id: 0, label: "Ankit's Story" },
{id: 1, label: "Taylor's Story" },
];
function App() {
let [stories, setStories] = useState<StoryType[]>([...initialStories])
let time = useTime();
// HACK: Prevent the memory from growing forever while you read docs.
// We're breaking our own rules here.
if (stories.length > 100) {
stories.length = 100;
}
return (
<div
style={{
width: '100%',
height: '100%',
textAlign: 'center',
}}
>
<h2>It is {time.toLocaleTimeString()} now.</h2>
<StoryTray stories={stories} />
</div>
);
}
문제의 코드는 시계가 업데이트 될 때마다 Create Story 카드를 두 개씩 추가한다. 의도했던 동작은 Create Story 카드가 한 개만 추가되는 것이다.
`stories`는 상위 컴포넌트인 `App.tsx`에서 전달된 props다. 이를 하위 컴포넌트인 `StoryTray` 내에서 직접적으로 수정하고 있다. 즉, `stories.push()` 함수는 `StoryTray` 컴포넌트가 렌더링되기 전에 호출되고, 매 초마다 `App.tsx`가 리렌더링 될 때, 하위 컴포넌트인 `StoryTray`도 리렌더링 된다. 그러나 `StoryTray`가 참조하는 `stories` 배열은 동일한 배열로 메모리 주소가 같다. 하지만 `StoryTray` 컴포넌트 내에서 원본 배열을 직접적으로 수정하고 있다. 따라서 리렌더링이 진행될 때마다 `stories` 배열(원본)에는 계속해서 같은 데이터가 추가된다.
따라서 문제 코드를 정상 동작하도록 수정하려면 `stories` 배열의 불변성을 유지해야 한다.
export default function StoryTray({ stories }: StoryTrayProps) {
let display = stories.slice();
display.push({
id: 'create',
label: 'Create Story'
});
return (
<ul>
{display.map(story => (
<li key={story.id}>
{story.label}
</li>
))}
</ul>
);
}
기존 배열을 복사해 `StoryTray` 컴포넌트 내부에서만 사용하는 배열을 생성해 이를 참조하도록 하면 `StoryTray` 컴포넌트가 리렌더링 되더라도 상위 컴포넌트의 `stories`에는 변경이 일어나지 않기 때문에 계속해서 데이터가 추가되는 것을 막을 수 있다.
'Note' 카테고리의 다른 글
| Lean React: Adding Interactivity (0) | 2024.05.31 |
|---|---|
| Spring Security OAuth 2.0 인증 과정 자세히 살펴보기 (0) | 2024.05.24 |
| Spring Data JPA 기본 구현체 분석 (0) | 2024.05.24 |
| Spring은 DB Transaction을 어떻게 알아서 처리할까? (0) | 2024.05.24 |
| Java Optional (0) | 2024.05.24 |
- Total
- Today
- Yesterday
- Database
- CSRF
- sql injection
- React
- oauth2
- Dreamhack
- linux
- PS
- webgoat
- java
- DP
- Spring
- Misc
- Transaction
- sqli
- Spring Security
- WEB
- JPA
- Bandit
- SEO
- 회고
- askers
- math
- XSS
- opengraph
- test
- Framework
- WarGame
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |