Skip to content

Commit f1c155d

Browse files
committed
docs: react-transition-group 게시글 이전
1 parent cb082df commit f1c155d

File tree

1 file changed

+302
-21
lines changed

1 file changed

+302
-21
lines changed

contents/posts/React/react-transition-group.md

Lines changed: 302 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,317 @@
22
title: React Transition Group 뜯어보기
33
createdAt: 2024-10-15
44
category: React
5-
description: React Transition Group 은 어떻게 동작할까요? 내부 동작 원리와 사용법에 대해 알아봅니다
5+
description: 디자인학과 졸업전시 사이트를 만들면서 React Transition Group 을 통해 DOM 개수를 줄여보면서 React Transition Group 은 어떻게 동작하는지 살펴보았습니다.
66
comment: true
7+
head:
8+
- - meta
9+
- name: keywords
10+
content: React, React Transition Group, TransitionGroup, CSSTransition, 애니메이션, React 애니메이션
711
---
812

9-
:::warning
10-
아직 작성중이거나 검토중인 글입니다. 내용이 부정확하거나 변경될 수 있습니다
13+
디자인학과 졸업전시 웹사이트를 만들면서, 전체 페이지에 수평 아코디언 전환 애니메이션이 필요했습니다.
14+
15+
<img src="./img/react-transition-group/design-exhibition-site.png" width="800px" alt="디자인학과 졸업전시 웹사이트"/>
16+
17+
처음 접근은 단순히 다른 아코디언 애니메이션처럼 단순하게 접근했습니다. <br/>
18+
모든 페이지 컴포넌트를 한 번에 전부 마운트해두고, 각 섹션의 `max-width` 만 조절하면서 열리고 닫히는 애니메이션을 구현했습니다.
19+
20+
## 🩸 사건 현장
21+
22+
그런데 웬걸.. Lighthouse에서 DOM 요소가 3,495개라는 경고가 뜨고, TBT(Total Blocking Time)도 높고 메모리 사용량도 계속 늘어나는 현상이 발생하는게 아니겠습니까?
23+
24+
<div style="display: flex; width:100%; background:#151515;">
25+
<img src="./img/react-transition-group/problem-1.png" style=" width:50%; object-fit:contain" alt="문제 상황 1"/>
26+
<img src="./img/react-transition-group/problem-2.png" style="width:50%; object-fit:contain" alt="문제 상황 2"/>
27+
</div>
28+
29+
무엇보다 `안 보이는 페이지도 계속 DOM 요소로써 남아있다`는 점이 문제였습니다. <br/>
30+
애니메이션이 끝나도 컴포넌트는 언마운트 되지 않으니까, 브라우저 입장에서는 그냥 페이지 여러 개를 동시에 돌리고 있는 셈이었습니다.
31+
32+
> 애니메이션은 자연스럽게 유지하되, 화면 바깥으로 나간 페이지는 DOM 에서 치워버릴 수 없을까?
33+
34+
`FramerMotion`, `React Spring` 등 여러 애니메이션 라이브러리를 살펴봤지만, 아코디언 애니메이션 하나를 위해서 번들 사이즈가 크기도 했고, 디자이너가 딱 원하는 애니메이션을 구현하기가 어려웠습니다. <br/>
35+
36+
결국 `React Transition Group` 라이브러리를 사용해서 이 문제를 해결할 수 있었습니다. <br/>
37+
하지만, 공식문서가 ~~불친절~~ 해서 내부 동작 방식을 직접 살펴보고, 어떻게 애니메이션이 종료된 이후 컴포넌트가 언마운트 되는지 뒤적거려봤습니다.
38+
39+
## 🕵️‍♀️ React Transition Group
40+
41+
> React Transition Group은 컴포넌트가 들어오고 나갈 때(마운트/언마운트 시점)의 전환을 정의할 수 있도록 여러 간단한 컴포넌트를 제공합니다. <br/><br/>
42+
> React Transition Group은 React-Motion 같은 애니메이션 라이브러리가 아닙니다. 이 라이브러리 자체가 스타일을 직접 애니메이션시키지는 않습니다. <br/><br/>
43+
> 대신에, 이 라이브러리는 전환(transition)의 각 단계 정보를 노출하고, 클래스 이름을 붙이거나 떼고, 요소들을 그룹화하고, DOM을 적절히 조작해 줍니다. 덕분에 실제 시각적인 전환 애니메이션을 구현하는 작업이 훨씬 쉬워집니다.
44+
45+
[React Transition Group](https://reactcommunity.org/react-transition-group/) 공식 문서를 들어가보면 크게 `Transition`, `CSSTransition`, `SwitchTransition`, `TransitionGroup` 네 가지 컴포넌트를 제공한다고 나와있습니다.
46+
47+
이제부터 이 네가지 컴포넌트에 대해 하나씩 살펴보겠습니다.
48+
49+
## 1️⃣ `<Transition/>` : 이 요소를 지금 보여줄꺼야? 숨길꺼야?
50+
51+
`<Transition/>` 컴포넌트는 React Transition Group 의 가장 기본이 되는 컴포넌트 입니다. <br/>
52+
이 컴포넌트는 자식 컴포넌트를 지금 보여줄꺼야? 숨길꺼야? 라는 상태를 `in` prop 으로 받아서, 컴포넌트의 라이프사이클을 상태머신으로 관리합니다.
53+
54+
### ⚙️ `in: boolean`
55+
56+
- `in` prop 은 boolean 타입으로, `true` 면 자식 컴포넌트를 보여주고, `false` 면 숨깁니다.
57+
- 이 값을 기반으로 `<Transition/>` 은 내부적으로 `entering`, `entered`, `exiting`, `exited` 네 가지 상태로 전환됩니다.
58+
59+
### ⚙️ `mountOnEnter: boolean`, `unmountOnExit: boolean`
60+
61+
- `mountOnEnter` : `in` 이 처음으로 `true` 가 되기전에 컴포넌트를 마운트하지 않습니다.
62+
- `unmountOnExit` : `in``false` 가 된 후에 컴포넌트를 언마운트합니다.
63+
64+
> 이 두 옵션을 사용하면, 컴포넌트가 화면에 보일 때만 마운트되고, 화면에서 사라질 때 언마운트 되도록 할 수 있습니다.
65+
66+
### ⚙️ `appear: boolean`
67+
68+
- 처음 마운트될 때도 등장 애니메이션을 적용할지 여부를 결정합니다. (기본값은 `false`)
69+
70+
### ⚙️ `enter: boolean`, `exit: boolean`
71+
72+
- 각 애니메이션을 아예 비활성화 할 수 있습니다
73+
- `enter : false` 인 경우 들어올때 애니메이션 없이 바로 `entered` 상태로 전이됩니다.
74+
- `exit : false` 인 경우 나갈때 애니메이션 없이 바로 `exited` 상태로 전이됩니다.
75+
76+
### ⚙️ `timeout: number``addEndListener`
77+
78+
- React Transition Group 은 애니메이션이 언제 끝났는지 알아야 다음 상태로 전이 할 수 있습니다
79+
- `timeout : number` : 지정된 ms 이후에 애니메이션이 끝났다고 간주합니다.
80+
- `addEndListener(node, done)` : DOM 노드의 실제 애니메이션 완료 이벤트를 잡아서 `done()` 을 호출해주는 방식이빈다.
81+
82+
```tsx
83+
<Transition
84+
in={show}
85+
addEndListener={(node, done) => {
86+
node.addEventListener("transitioned", done);
87+
}}
88+
/>
89+
```
90+
91+
### 🔁 상태 전이
92+
93+
결국 `<Transition/>` 컴포넌트의 핵심은 각 props 에 따른 상태 전이입니다. <br/>
94+
텍스트로만 설명하면 잘 이해가 안되니... 상태 전이 다이어그램을 보면 이해가 쉽습니다
95+
96+
~~사실 프론트같은거 좋아하는 이유도 이렇게 시각적으로 뭔가 정리하는게 이해가 쉽고 재밌어서 그렇습니다 ㅎㅎ~~
97+
98+
<img src="./img/react-transition-group/transition.webp" width="600px" alt="Transition 상태 전이 다이어그램"/>
99+
100+
:::details 🙋‍♂️ 상태 전이 다이어그램에서 `[]` 는 뭔가요? - Guard
101+
`[]` 표시는 가드(Guard) 조건을 의미합니다. <br/>
102+
가드 조건은 상태 전이가 발생하기 위한 추가적인 조건을 나타냅니다. <br/>
103+
예를 들어, 가장 위에 보이는 `[mountOnEnter === true]` 는 초기 상태에서 `mountOnEnter` props 가 `true` 일 때만 `unmounted` 상태로 전이된다는 의미입니다. <br/>
11104
:::
12105

13-
## 서론
106+
## 2️⃣ `<CSSTransition/>` : CSS 클래스 이름 알아서 붙여줄게 ~
107+
108+
`<CSSTransition/>` 컴포넌트는 `<Transition/>` 컴포넌트를 확장한 컴포넌트로, CSS 클래스 이름을 사용하여 애니메이션을 제어할 수 있도록 도와줍니다. <br/>
109+
차이점은 하나입니다. `<Transition/>` 컴포넌트는 타이밍만 제어하고 스타일은 직접 `on*` 콜백 props 를 사용하여 직접 제어해야 하지만, `<CSSTransition/>` 컴포넌트는 각 상태에 맞는 CSS 클래스 이름을 자동으로 추가/제거 해준다는 점입니다.
110+
111+
개발자는 그냥 `classNames` prefix 를 기준으로 CSS 만 정의해두면 됩니다.
112+
113+
```tsx
114+
<CSSTransition in={show} timeout={300} classNames="prefix" mountOnEnter unmountOnExit>
115+
<Component />
116+
</CSSTransition>
117+
```
118+
119+
이렇게만 써두면, 상태 전이에 따라 다음과 같은 클래스들이 자동으로 붙습니다
120+
121+
1. Enter 시
122+
123+
- `prefix-enter`
124+
- 이후 바로 `prefix-enter-active`
125+
- 완료 후 `prefix-enter-done`
126+
127+
2. Exit 시
128+
129+
- `prefix-exit`
130+
- 이후 바로 `prefix-exit-active`
131+
- 완료 후 `prefix-exit-done`
132+
133+
### ⚙️ `appear: boolean` 에 따른 classNames
134+
135+
`appear:true` 를 설정한 경우, 마운트 초기에도 `prefix-appear`, `prefix-appear-active`, `prefix-appear-done` 클래스가 적용 됩니다.
136+
137+
이를 통해 첫 등장 애니메이션은 쪼금 다르게 줄 수도 있습니다.
138+
139+
### 🤨 왜 클래스가 두번에 나눠서 붙지 (reflow 트릭)
140+
141+
애니메이션을 자연스럽게 시작하려면 `초기 상태 클래스``활성 상태 클래스` 를 서로 다른 프레임에 붙여야 합니다.
142+
143+
`<CSSTransition/>` 컴포넌트는 대략 이런식으로 동작합니다
144+
145+
1. `onEnter` 시점에 `prefix-enter` 클래스를 붙여둔다
146+
2. 다음 프레임에서 강제로 한번 reflow 를 발생시킨다 (`node.offset*` 같은 코드로 강제로 layout 을 읽어 브라우저가 현재 스타일을 계산하게 만듭니다)
147+
3. 그 다음에 `prefix-enter-active` 클래스를 붙인다 (최종 스타일)
148+
149+
:::info
150+
두개의 클래스를 동시에 붙이면, 브라우저가 시작 상태를 못 잡고 그냥 최종 상태로 바로 넘어가 버리는 경우가 있어서 일부러 reflow 로 타이밍을 나눠줍니다!
151+
:::
152+
153+
`exit` 도 동일한 방식으로 동작합니다
154+
155+
## 3️⃣ `<TransitionGroup/>` : 여러 컴포넌트의 입장과 퇴장을 관리해줭
156+
157+
`<Transition/>``<CSSTransition/>` 컴포넌트는 하나의 컴포넌트가 들어오고 나가는 순간을 관리합니다.
158+
159+
근데 실제 애니메이션에서는 여러 컴포넌트가 동시에 들어오고 나가는 경우가 많습니다. <br/>
160+
여러 컴포넌트의 진입, 퇴장 타이밍을 관리해줘야 하는거죠
161+
162+
이걸 해결해주는 친구가 `<TransitionGroup/>` 컴포넌트입니다.
163+
164+
```tsx
165+
<TransitionGroup component={null}>
166+
{items.map((item) => {
167+
return (
168+
<CSSTransition
169+
key={item.id}
170+
timeout={300}
171+
classNames="prefix"
172+
mountOnEnter={true}
173+
unmountOnExit={true}
174+
>
175+
<ItemComponent item={item} />
176+
</CSSTransition>
177+
);
178+
})}
179+
</TransitionGroup>
180+
```
181+
182+
### ⚙️ `key`
183+
184+
`<TransitionGroup/>` 컴포넌트는 key 를 기준으로 어떤 컴포넌트가 새로 들어왔는지, 어떤 컴포넌트가 나갔는지를 판단합니다. <br/>
185+
186+
1. TransitionGroup 은 children 의 key 목록을 기억해 둡니다.
187+
2. 다음 렌더링시 key 의 목록이 바뀌었는지 비교합니다.
188+
- 새로 생긴 key : 새로운 children 컴포넌트가 들어온 것으로 판단하고 `in=true` 로 설정해서 enter 애니메이션을 시작합니다.
189+
- 없어진 key : 사라질 children 컴포넌트가 나간 것으로 판단하고 DOM 에서 바로 제거하지 않고, `in=false` 를 주면서 exit 애니메이션을 먼저 실행합니다. (사라지는 애를 바로 안지우고 퇴장 애니메이션을 실행할 시간을 줌)
190+
3. exit 애니메이션이 끝나는 시점에만 state 에서 제거해서 실제 DOM 에서도 제거합니다.
191+
192+
### ⚙️ `component`
193+
194+
기본적으로 `<TransitionGroup/>` 컴포넌트는 `<div>` 래퍼를 사용합니다. <br/>
195+
`null` 을 주면 자식만 렌더링할 수 있습니다.
196+
197+
## 4️⃣ `<SwitchTransition/>` : 한 번에 하나의 컴포넌트만 보여줄꺼양
198+
199+
`<SwitchTransition/>` 컴포넌트는 항상 딱 하나의 자식 컴포넌트가 존재하고 그걸 바꿀 때 애니메이션 하고 싶다는 상황에 쓰이는 컴포넌트입니다.
200+
201+
예를들어, 한장 사라지고 다른 한장이 나타나는 슬라이드쇼 같은 상황이 있겠네요
202+
203+
이 컴포넌트는 `mode` 라는 props 로 전환 순서를 결정합니다.
204+
205+
### ⚙️ `mode : "out-in" | "in-out"`
206+
207+
- `out-in`
208+
- 현재 컴포넌트의 exit 애니메이션을 완전히 끝내고
209+
- 다 사라진 다음 새로운 자식이 enter 애니메이션으로 등장합니다.
210+
- `in-out`
211+
- 새로운 자식이 enter 애니메이션으로 먼저 들어오고
212+
- 다 자리잡은 다음에 기존 자식에게 exit 애니메이션을 실행합니다.
213+
214+
## 📲 (실험) 모바일 화면 전환 애니메이션을 만들어보자
215+
216+
이제까지 살펴본 내용을 바탕으로, 모바일 화면 전환 애니메이션을 구현해보겠습니다. <br/>
217+
218+
:::info 요구사항
219+
220+
- 새로운 화면은 오른쪽에서 왼쪽으로 슬라이드 인
221+
- 기존 화면은 왼쪽으로 슬라이드 아웃
222+
- 두 화면이 동시에 전환됨
223+
:::
224+
225+
여러 자식 컴포넌트가 동시에 들어오고 나가는 상황이므로 `<TransitionGroup/>` 컴포넌트를 사용하면 되겠네요
226+
227+
```tsx
228+
import { TransitionGroup, CSSTransition } from "react-transition-group";
229+
import { Routes, Route, useLocation } from "react-router-dom";
230+
231+
function App() {
232+
const location = useLocation();
233+
234+
return (
235+
<TransitionGroup className="page-wrapper">
236+
<CSSTransition
237+
key={location.key}
238+
classNames="slide"
239+
timeout={300}
240+
mountOnEnter
241+
unmountOnExit
242+
>
243+
<Routes location={location}>
244+
<Route path="/" element={<HomePage />} />
245+
<Route path="/about" element={<AboutPage />} />
246+
{/* ... */}
247+
</Routes>
248+
</CSSTransition>
249+
</TransitionGroup>
250+
);
251+
}
252+
```
253+
254+
### ⚠️ 여기서 중요한 포인트
255+
256+
- `key={location.key}` : 라우팅 할때 마다 key가 바뀌므로, `<TransitionGroup/>` 입장에서 "이전 화면 하나", "새 화면 하나" 라는 두 컴포넌트를 동시에 관리 할 수 있게 합니다
257+
- `mountOnEnter`, `unmountOnExit` : 현재 보여줄 화면만 DOM 에 남기고, 이전 화면은 애니메이션이 끝나는 순간 언마운트 되도록 합니다
258+
259+
### 🎨 CSS 를 작성해보자
260+
261+
이제 prefix 에 따른 CSS 만 작성해주면 됩니다
262+
263+
```css
264+
/* 부모: 겹쳐서 보여줄 수 있도록 relative */
265+
.page-wrapper {
266+
position: relative;
267+
overflow: hidden;
268+
}
14269

15-
디자인학과 졸업전시 웹사이트 프젝 진행 중
270+
/* 각 페이지는 겹칠 수 있도록 absolute */
271+
.page-wrapper > * {
272+
position: absolute;
273+
top: 0;
274+
left: 0;
275+
width: 100%;
276+
height: 100%;
277+
}
16278

17-
전체 페이지에 수평 아코디언 애니메이션을 넣으려고
18-
처음엔 모든 페이지를 한 번에 마운트해서 max-width로 접음
19-
근데 Lighthouse에서 DOM 요소 3,495개 경고 뜨고
20-
TBT 높고 메모리 사용량도 계속 늘어남
21-
언마운트 ?
279+
/* Enter (새 화면이 오른쪽에서 들어옴) */
280+
.slide-enter {
281+
transform: translateX(100%); /* 시작: 오른쪽 바깥 */
282+
}
283+
.slide-enter-active {
284+
transform: translateX(0%);
285+
transition: transform 300ms ease;
286+
}
287+
.slide-enter-done {
288+
transform: translateX(0%);
289+
}
22290

23-
## 문제 접근방법
291+
/* Exit (이전 화면이 왼쪽으로 밀려나감) */
292+
.slide-exit {
293+
transform: translateX(0%); /* 시작: 현재 위치 */
294+
}
295+
.slide-exit-active {
296+
transform: translateX(-100%); /* 왼쪽 바깥으로 나감 */
297+
transition: transform 300ms ease;
298+
}
299+
.slide-exit-done {
300+
transform: translateX(-100%);
301+
}
302+
```
24303

25-
React Transition Group으로 상태 전이에 따라 in=true/false로 애니메이션 시도
26-
컴포넌트마다 enter/exit 애니메이션 끝나면 자동으로 언마운트 되도록 처리
27-
switch transition도 고려했지만, 여러 페이지를 동시에 다뤄야 해서 transitionGroup 선택
28-
CSS로 transform 기반 애니메이션 구현해서 reflow/repaint 최소화
304+
### 🚀 어떻게 동작하나?
29305

30-
## 해결방법
306+
1. 라우트 변경 => `<CSSTransition key={...}>` 가 새로운 화면을 하나 더 렌더 (TransitionGroup이 이전 화면은 in={false}, 새 화면은 in={true} 상태로 관리)
307+
2. 이전 화면에는 `.slide-exit` / 새 화면에는 `.slide-enter` 클래스가 각각 들어감
308+
3. 다음 animation frame에서 `.slide-exit-active`, `.slide-enter-active` 클래스가 붙으면서
309+
- 이전 화면: translateX(-100%)로 왼쪽으로 밀려나가고
310+
- 새 화면: translateX(0)까지 오른쪽에서 슬라이드 인
311+
4. 약 300ms 뒤 transition이 끝나면
312+
- 이전 화면은 onExited => 언마운트
313+
- 새 화면은 .slide-enter-done 상태로 정상적으로 남음
31314

32-
`<TransitionGroup><CSSTransition>` 구조로 전환 흐름 제어
315+
## 📖 참고 자료
33316

34-
전환 시 DOM에 둘 다 남겨두고, 퇴장 페이지는 exit 애니메이션 후 DOM에서 제거
35-
새 페이지는 enter > entered 상태로 자연스럽게 등장
36-
css transform: translateX()로 GPU 가속 유도
37-
timeout 또는 transitionend 로 애니메이션 완료 감지 -> ,언마운트
317+
- [React Transition Group 공식문서](https://reactcommunity.org/react-transition-group/)
318+
- [React Transition Group 소스코드](https://github.com/reactjs/react-transition-group)

0 commit comments

Comments
 (0)