|
| 1 | +# CHAPTER 12 리액트 디자인 패턴 |
| 2 | + |
| 3 | +## 리액트에서 널리 사용되는 디자인 패턴 |
| 4 | + |
| 5 | +- [고차 컴포넌트 패턴](#고차-컴포넌트-패턴) |
| 6 | +- [렌더링 Props 패턴](#렌더링-props-패턴) |
| 7 | +- [Hooks 패턴](#hooks-패턴) |
| 8 | +- [정적 가져오기](#정적-가져오기) |
| 9 | +- [동적 가져오기](#동적-가져오기) |
| 10 | +- [코드 스플리팅](#코드-스플리팅) |
| 11 | +- [PRPL 패턴](#PRPL-패턴) |
| 12 | +- [로딩 우선순위](#로딩-우선순위) |
| 13 | + |
| 14 | +## 고차 컴포넌트 패턴 |
| 15 | + |
| 16 | +고차 컴포넌트는 **컴포넌트를 인자로 받아 새로운 컴포넌트를 반환**하는 컴포넌트 |
| 17 | +즉, 인자로 받은 컴포넌트에 추가 기능을 적용한 컴포넌트를 반환 |
| 18 | + |
| 19 | +```javascript |
| 20 | +// 컴포넌트를 인자로 받음 |
| 21 | +function withStyles(Component) { |
| 22 | + return (props) => { |
| 23 | + const style = { padding: '0.2rem', margin: '1rem' }; |
| 24 | + return <Component style={style} {...props} />; // 새로운 컴포넌트 반환 |
| 25 | + }; |
| 26 | +} |
| 27 | + |
| 28 | +const Button = () => <button>Click me!</button>; |
| 29 | +const Text = () => <p>Hello World!</p>; |
| 30 | + |
| 31 | +const StyledButton = withStyles(Button); |
| 32 | +const StyledText = withStyles(Text); |
| 33 | +``` |
| 34 | + |
| 35 | +### 목적 |
| 36 | + |
| 37 | +여러 컴포넌트에 동일한 로직을 사용하고 싶을 때 사용 |
| 38 | + |
| 39 | +### 활용 예시 |
| 40 | + |
| 41 | +특정 스타일 적용, 인증 요구, 전역 상태 추가 |
| 42 | + |
| 43 | +### 장점 |
| 44 | + |
| 45 | +- 로직을 한 곳에 집중시켜 중복 제거 |
| 46 | +- 효과적으로 관심사 분리 |
| 47 | + |
| 48 | +### 단점 |
| 49 | + |
| 50 | +고차 컴포넌트가 대상 컴포넌트에 전달하는 prop의 이름이 일으키는 충돌 → 디버깅과 애플리케이션 확장에 어려움 발생 |
| 51 | + |
| 52 | +## 렌더링 Props 패턴 |
| 53 | + |
| 54 | +자신의 렌더링 로직을 구현하는 대신, 렌더링 prop을 호출 |
| 55 | +렌더링 prop의 이름이 `render`일 필요는 없고, JSX를 렌더링하면 렌더링 prop으로 간주 |
| 56 | + |
| 57 | +```javascript |
| 58 | +<Component render={() => <h1>I am a render prop!</h1>} /> |
| 59 | + |
| 60 | +<Component render={data => <ChildComponent data={data} />} /> |
| 61 | +``` |
| 62 | + |
| 63 | +### 장점 |
| 64 | + |
| 65 | +고차 컴포넌트 패턴에서 발생할 수 있는 **이름 충돌 문제를 해결** → 어떤 props가 어디에서 오는지 정확하게 파악 가능 |
| 66 | + |
| 67 | +### 단점 |
| 68 | + |
| 69 | +- 대부분의 경우 렌더링 Props 패턴을 Hooks 패턴으로 대체 가능 |
| 70 | +- 라이프사이클 관련 메서드를 추가할 수 없음 → 데이터를 변경하지 않는 렌더링에 치중한 컴포넌트에만 사용 가능 |
| 71 | + |
| 72 | +### 상태 끌어올리기 |
| 73 | + |
| 74 | +A 컴포넌트와 B 컴포넌트가 상태를 공유하기 위해 **가장 가까운 공통 조상 컴포넌트**로 상태를 끌어올리는 것 |
| 75 | +작은 규모의 애플리케이션에서는 전역 상태 관리 라이브러리 대신 이 패턴 사용하는 것만으로도 충분 |
| 76 | +하지만 큰 규모라면 상태 끌어올리기가 복잡해지며, 리렌더링으로 인해 성능에 악영향 줄 수 있음 |
| 77 | +이때 렌더링 Props 패턴을 사용하면 좋음 |
| 78 | + |
| 79 | +```javascript |
| 80 | +function Input(props) { |
| 81 | + const [value, setValue] = useState(''); |
| 82 | + return ( |
| 83 | + <> |
| 84 | + <input type="text" value={value} onChange={(e) => setValue(e.target.value)} /> |
| 85 | + // 여기 |
| 86 | + {props.render(value)} |
| 87 | + </> |
| 88 | + ); |
| 89 | +} |
| 90 | + |
| 91 | +export default function App() { |
| 92 | + return ( |
| 93 | + <div className="App"> |
| 94 | + <h1>Temperature Converter</h1> |
| 95 | + <Input |
| 96 | + // 여기 |
| 97 | + render={(value) => ( |
| 98 | + <> |
| 99 | + <Kelvin value={value} /> |
| 100 | + <Fahrenheit value={value} /> |
| 101 | + </> |
| 102 | + )} |
| 103 | + /> |
| 104 | + </div> |
| 105 | + ); |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +### 컴포넌트의 자식으로 함수 전달하기 |
| 110 | + |
| 111 | +렌더링 Prop으로 전달하는 대신 **컴포넌트의 자식**으로 함수 전달 가능 |
| 112 | + |
| 113 | +```javascript |
| 114 | +function Input(props) { |
| 115 | + const [value, setValue] = useState(''); |
| 116 | + return ( |
| 117 | + <> |
| 118 | + <input type="text" value={value} onChange={(e) => setValue(e.target.value)} /> |
| 119 | + // 여기 |
| 120 | + {props.children(value)} |
| 121 | + </> |
| 122 | + ); |
| 123 | +} |
| 124 | + |
| 125 | +export default function App() { |
| 126 | + return ( |
| 127 | + <div className="App"> |
| 128 | + <h1>Temperature Converter</h1> |
| 129 | + <Input> |
| 130 | + // 여기 |
| 131 | + {(value) => ( |
| 132 | + <> |
| 133 | + <Kelvin value={value} /> |
| 134 | + <Fahrenheit value={value} /> |
| 135 | + </> |
| 136 | + )} |
| 137 | + </Input> |
| 138 | + </div> |
| 139 | + ); |
| 140 | +} |
| 141 | +``` |
| 142 | + |
| 143 | +## Hooks 패턴 |
| 144 | + |
| 145 | +Hooks를 사용하면 **클래스 컴포넌트를 사용하지 않고도 상태와 라이프사이클 메서드 활용 가능** |
| 146 | + |
| 147 | +### 장점 |
| 148 | + |
| 149 | +- 간결한 코드 |
| 150 | +- 복잡한 컴포넌트의 단순화 |
| 151 | +- 상태 관련 로직 재사용 |
| 152 | +- UI에서 분리된 로직 공유 |
| 153 | + |
| 154 | +## 정적 가져오기 |
| 155 | + |
| 156 | +`import 모듈 from '모듈'`을 사용해 가져오는 모듈은 모두 정적으로 가져온 것 |
| 157 | + |
| 158 | +컴포넌트를 정적으로 가져오면 웹팩은 모듈을 초기 번들에 포함시킴 |
| 159 | +애플리케이션을 빌드한 후에는 웹팩이 생성한 번들 확인 가능 |
| 160 | + |
| 161 | +## 동적 가져오기 |
| 162 | + |
| 163 | +모듈이 초기 번들에 불필요하게 포함되어 로딩 시간이 증가하는 문제를 해결하기 위해 컴포넌트를 동적으로 가져올 수 있음 |
| 164 | +컴포넌트를 정적으로 가져오는 대신 실제로 필요한 시점에 맞춰 불러오는 것 |
| 165 | +리액트의 `Suspense` 컴포넌트로 동적으로 로드할 컴포넌트를 감싸면 된다. |
| 166 | +리액트 18부터 SSR 환경에서도 `Suspense` 사용 가능 |
| 167 | + |
| 168 | +### 상호작용 시 가져오기 |
| 169 | + |
| 170 | +ex. 채팅 애플리케이션에서 사용자가 이모지를 클릭하면 EmojiPicker 컴포넌트를 동적으로 가져오는 상황 |
| 171 | + |
| 172 | +### 화면에 보이는 순간 가져오기 |
| 173 | + |
| 174 | +IntersectionObserver API를 사용해 컴포넌트가 화면에 보이는지 확인 |
| 175 | +ex. 사용자가 스크롤해야 화면에 나타나는 컴포넌트 |
| 176 | + |
| 177 | +## 코드 스플리팅 |
| 178 | + |
| 179 | +복잡한 애플리케이션에서는 적절한 시기에 정적/동적 임포트가 가능하도록 코드를 최적으로 스플리팅하고 번들링해야 함 |
| 180 | + |
| 181 | +### 경로 기반 분할 |
| 182 | + |
| 183 | +때로는 특정 페이지(경로)에서만 필요한 리소스가 있음 |
| 184 | +이런 경우 경로별로 컴포넌트를 지연 로딩하면, 현재 경로에 필요한 코드가 포함된 번들만 요청 가능 |
| 185 | + |
| 186 | +### 번들 분할 |
| 187 | + |
| 188 | +거대한 번들 하나를 요청하는 대신, 여러 개의 작은 번들로 분할하는 방법 |
| 189 | + |
| 190 | +#### 장점 |
| 191 | + |
| 192 | +- 첫 번째 콘텐츠가 사용자 화면에 표시되는 시간(`FCP`) 단축 |
| 193 | +- 가장 큰 콘텐츠가 화면에 렌더링되는 시간(`LCP`) 개선 |
| 194 | +- 모든 콘텐츠가 화면에 표시되고 인터랙티브해지는 데 걸리는 시간(`TTI`) 개선 |
| 195 | + |
| 196 | +## PRPL 패턴 |
| 197 | + |
| 198 | +저사양 기기나 인터넷 연결이 불안정한 지역에서도 애플리케이션이 원활하게 작동해야 함 |
| 199 | +어떤 환경에서도 애플리케이션이 최대한 효율적으로 로드될 수 있도록 PRPL 패턴 사용 |
| 200 | + |
| 201 | +4가지 핵심 성능 고려사항에 중점 |
| 202 | + |
| 203 | +- `Push` : 중요한 리소스를 효율적으로 푸시해 서버 왕복 횟수를 최소화하고 로딩 시간 단축 |
| 204 | +- `Render` : 초기 경로를 최대한 빠르게 렌더링해 UX 개선 |
| 205 | +- `Pre-cache` : 자주 방문하는 경로의 에셋을 백그라운드에서 미리 캐싱해 서버 요청 횟수 줄이고 더 나은 오프라인 경험 제공 |
| 206 | +- `Lazy-load` : 자주 요청되지 않는 경로나 에셋은 지연 로딩 |
| 207 | + |
| 208 | +## 로딩 우선순위 |
| 209 | + |
| 210 | +`preload`는 브라우저의 최적화 기능으로, **중요한 리소스를 더 일찍 요청** 가능 |
| 211 | +로딩 순서를 지정하면 Core Web Vitals의 로딩 성능 및 지표에 긍정적인 영향 미침 |
| 212 | + |
| 213 | +`TTI` 또는 `FID`를 최적화할 때 유용 |
| 214 | +상호작용에 필요한 리소스를 먼저 로딩하다가 |
| 215 | +`FCP`, `LCP`에 필요한 리소스(ex. 폰트, 히어로 이미지)의 로딩이 지연되지 않도록 주의해야 함 |
| 216 | + |
| 217 | +자바스크립트 자체의 로딩을 최적화하려면, `<body>` 태그보다는 `<head>` 태그 안에서 `<script defer>`를 사용하는 게 좋음 |
| 218 | + |
| 219 | +### SPA의 Preload |
| 220 | + |
| 221 | +#### `prefetching` |
| 222 | + |
| 223 | +- 앞으로 사용될 가능성이 높은 리소스를 미리 가져와 캐시에 저장하는 방식 |
| 224 | +- 브라우저가 인터넷 연결 상태와 대역폭을 고려해 어떤 리소스 미리 가져올지 결정 |
| 225 | + |
| 226 | +#### `preload` |
| 227 | + |
| 228 | +- **즉시** 사용해야 하는 리소스 (ex. 초기 렌더링에 사용되는 특정 폰트나 접속 시 바로 보이는 히어로 이미지 등) |
| 229 | +- **어떤 상황에서든 무조건 미리 로드** |
| 230 | +- 초기 렌더링 후 약 1초 이내에 표시되어야 하는 리소스만 선별해 미리 로드하는 것이 좋음 |
| 231 | + |
| 232 | +### Preload + async 기법 |
| 233 | + |
| 234 | +브라우저가 스크립트를 높은 우선순위로 다운로드하면서도, 스크립트를 기다리는 동안 파싱이 멈추지 않도록 하기 위한 기법 |
| 235 | + |
| 236 | +```html |
| 237 | +<link rel="preload" href="emoji-picker.js" as="script" /> |
| 238 | +<script src="emoji-picker.js" async></script> |
| 239 | +``` |
| 240 | + |
| 241 | +### 크롬 95+ 버전에서의 Preload |
| 242 | + |
| 243 | +크롬 95+ 버전에서는 preload의 queue-jumping 동작이 개선되어 preload 기능이 더 안전해짐 |
| 244 | + |
| 245 | +> queue-jumping이란? |
| 246 | +> 브라우저는 리소스를 요청할 때 queue에 쌓아서 순차적으로 처리합니다. |
| 247 | +> 기본적으로 브라우저는 HTML 파일을 읽고, 필요한 스크립트나 스타일시트를 순서대로 로드합니다. |
| 248 | +> 하지만 preload를 사용하면, 중요한 리소스를 우선적으로 로드하도록 **큐에서 뛰어넘을 수 있습니다**. |
| 249 | +
|
| 250 | +## 리스트 가상화 |
| 251 | + |
| 252 | +대규모 데이터 리스트의 렌더링 성능을 향상시키는 기술 |
| 253 | +현재 화면에 보이는 행만 동적으로 렌더링 |
| 254 | +`react-virtualized` 같은 라이브러리를 사용하여 구현 |
| 255 | +스크롤되는 뷰포트 내에서 현재 보이는 항목만 렌더링하므로, 한 번에 수천 개의 행 데이터를 렌더링하는 데 드는 리소스를 절약 |
| 256 | + |
| 257 | +### 윈도잉/가상화의 작동 방식 |
| 258 | + |
| 259 | +`react-virtualized`에서의 작동 방식 |
| 260 | + |
| 261 | +https://github.com/user-attachments/assets/c4afb058-8c99-4adb-8a04-1a99cc89949f |
| 262 | + |
| 263 | +윈도잉이 동작하기 위해 필요한 요소 |
| 264 | + |
| 265 | +- 스크롤을 위한 큰 DOM 요소 |
| 266 | +- `position: relative`를 가지는 작은 컨테이너 DQM 요소 (영상에서 ul에 해당) |
| 267 | +- 컨테이너 내부에 위치하고 `position: absolute`이며 `top`, `left`, `width`, `height` 등을 설정한 자식 요소들 |
| 268 | + |
| 269 | +[react-window](https://github.com/bvaughn/react-window)는 [react-virtualized](https://github.com/bvaughn/react-virtualized) 개발자 Brian Vaughn이 다시 만든 라이브러리 |
| 270 | +smaller, faster, more tree-shakeable, beginner-friendly |
| 271 | + |
| 272 | +> 두 라이브러리가 어떻게 다른지 개발자가 직접 설명한 [글](https://github.com/bvaughn/react-window#how-is-react-window-different-from-react-virtualized)이 있네요 (둘 중에는 react-window를 사용하라고 권장하고 있습니다) |
| 273 | +> |
| 274 | +> 토스증권에서는 `react-virtuoso`, `@tanstack/react-virtual`을 상황에 맞게 사용 중이라고 해요 |
| 275 | +> `react-virtuoso`는 선언적이고, `@tanstack/react-virtual`은 headless라서 커스텀 하기 좋다고 합니다 |
| 276 | +> |
| 277 | +> https://npmtrends.com/@tanstack/react-virtual-vs-react-virtualized-vs-react-virtuoso-vs-react-window |
| 278 | +> 4가지를 같이 비교해 보니, 확실히 `react-virtuoso`, `@tanstack/react-virtual`이 가볍긴 하네요 |
| 279 | +>  |
| 280 | +
|
| 281 | +### 웹 플랫폼의 발전 |
| 282 | + |
| 283 | +`content-visibility: auto`를 설정하면 화면 밖 콘텐츠의 렌더링과 페인팅을 필요한 시점까지 지연할 수 있음 |
| 284 | +렌더링 비용이 큰 HTML 문서에 적용하면 좋음 |
| 285 | +동적인 콘텐츠 목록을 렌더링하는 경우에는 `react-window` 같은 라이브러리 사용하는 게 좋음 |
| 286 | + |
| 287 | +> https://developer.mozilla.org/en-US/docs/Web/CSS/content-visibility |
| 288 | +> [(번역) CSS content-visibility를 이용해 렌더링 성능 향상 시키기](https://velog.io/@superlipbalm/improving-rendering-performance-with-css-content-visibility) |
0 commit comments