Skip to content

Commit 2fa0d77

Browse files
committed
docs: 2026-04-29-fontend update
1 parent e2c3434 commit 2fa0d77

2 files changed

Lines changed: 34 additions & 34 deletions

File tree

_posts/frontend/2026-05-09-frontend.md

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ categories: [Blogging,frontend]
2727

2828
목표는 “가장 최신 도구 사용”이 아니었습니다. 360만 줄이 넘는 레거시 코드베이스를 최대한 안전하게, 그리고 단계별로 검증하면서 빠른 빌드 환경으로 옮기는 것이었습니다.
2929

30-
# 전환 기준
30+
## 전환 기준
3131

3232
처음부터 특정 도구를 정해두지는 않았습니다. 도구를 고르기 전에 빌드 시간 병목구간을 찾아야했습니다.
3333

34-
## 빌드 분석
34+
### 빌드 분석
3535

3636
Jenkins 빌드 로그에서 먼저 확인한건 전체 빌드 시간입니다. 개발 환경 빌드 기준 약 36분, 이 빌드에는 minification이 포함되지 않습니다. 운영 빌드가 약 50분이었으니, 그 차이인 약 12~14분은 minification 단계(ParallelUglifyPlugin)에서 나온다고 볼 수 있었습니다.
3737

@@ -49,7 +49,7 @@ minification이 빠진 개발기 빌드만으로도 36분이 걸린다는 사실
4949
| 코드 압축 (minification) | ParallelUglifyPlugin | ~14분 |
5050
| 기타 | 번들링, 에셋 처리 | ~2분 |
5151

52-
## 코드베이스 제약 조건
52+
### 코드베이스 제약 조건
5353

5454
| 항목 | 수치 | 의미 |
5555
| --- | --- | --- |
@@ -62,7 +62,7 @@ minification이 빠진 개발기 빌드만으로도 36분이 걸린다는 사실
6262

6363
병목 위치를 알았다고 바로 도구를 정할 수는 없었습니다. 어떤 도구를 쓸 수 있는지는 코드베이스 상태가 결정합니다. 이 숫자들이 도구 선택의 필터가 됐습니다. 가장 빠른 도구가 아니라, 이 조건에서 실제로 동작하는 도구를 찾아야 했습니다.
6464

65-
## 도구 선정
65+
### 도구 선정
6666

6767
빌드 파이프라인을 기준으로 보면 해결 경로는 둘로 나뉩니다.
6868

@@ -71,7 +71,7 @@ minification이 빠진 개발기 빌드만으로도 36분이 걸린다는 사실
7171
번들러 교체 → Webpack 자체를 교체, 트랜스파일러도 함께 교체
7272
```
7373

74-
번들러를 교체하면 트랜스파일러 문제도 같이 풀릴 수 있습니다. 두 경로를 모두 열어두고 후보를 봤습니다.
74+
번들러를 교체하면 트랜스파일러 문제도 같이 풀릴 수 있습니다. 두 케이스를 모두 열어두고 후보를 봤습니다.
7575
후보를 추릴 때는 [State of JavaScript 2025](https://2025.stateofjs.com/ko-KR/libraries/build-tools/) 설문 결과와 npm 주간 다운로드 수치를 같이 봤습니다. 개인적인 선호보다 실제 사용 흐름을 먼저 보려는 의도였습니다.
7676

7777
| **경로** | **후보 도구** | **State of JS 2025 사용률** | **사용 경험 응답 수** | **npm 주간 다운로드** | **공식 문서** |
@@ -90,21 +90,21 @@ webpack은 이 설문에서 사용률 87%(9,797명)로 여전히 가장 많이
9090

9191
남은 후보는 **Vite**, **Rspack**, **SWC**였습니다. 여기서부터는 코드베이스 제약 조건으로 다시 걸렀습니다.
9292

93-
### **Vite** ([공식 문서](https://vite.dev/guide/build) / [plugin-legacy](https://github.com/vitejs/vite/tree/main/packages/plugin-legacy))
93+
#### Vite ([공식 문서](https://vite.dev/guide/build) / [plugin-legacy](https://github.com/vitejs/vite/tree/main/packages/plugin-legacy))
9494

9595
Vite는 개발 서버와 운영 빌드의 번들러가 다릅니다. 개발 서버는 esbuild, 운영 빌드는 Rollup을 사용합니다. 이 프로젝트처럼 "빌드 성공 여부"보다 "런타임 동작 일관성"이 중요한 레거시 프로젝트에서 두 환경의 동작 차이는 관리하기 어려운 리스크였습니다.
9696

9797
Vite의 기본 빌드 타겟은 Chrome 111+, Safari 16.4+ 기준의 모던 브라우저입니다. 레거시 브라우저 지원을 위한 `@vitejs/plugin-legacy`가 있지만, 이 플러그인은 브라우저 기능 수준(ES Modules, dynamic import)을 기준으로 동작합니다. 공식 문서에도 IE11은 기본 제외 대상으로 명시되어 있고, React 15 같은 특정 프레임워크 버전에 대한 검증 경로를 제공하지는 않습니다.
9898

9999
`System.import()` 1,361건과 CommonJS 혼재 환경은 Vite의 ESM-first 구조와 맞지 않았고, Webpack 플러그인 설정도 전면 재작성해야 했습니다.
100100

101-
### **Rspack** ([공식 문서](https://rspack.rs/guide/start/introduction) / [플러그인 호환성](https://rspack.rs/guide/compatibility/plugin))
101+
#### Rspack ([공식 문서](https://rspack.rs/guide/start/introduction) / [플러그인 호환성](https://rspack.rs/guide/compatibility/plugin))
102102

103103
Rspack은 Webpack API 호환을 목표로 합니다. 공식 문서에는 "상위 50개 webpack 플러그인 중 85% 이상 사용 가능 또는 대안 제공"이라고 적혀 있습니다. 기존 설정을 거의 그대로 가져갈 수 있다는 점이 매력적이었습니다.
104104

105105
다만 공식 호환성 문서에는 "100% webpack API 완전 호환은 설계 목표가 아님"도 함께 명시되어 있었습니다. 코드베이스에서 확인한 MobX 레거시 데코레이터 289개 파일, 프로덕션 수준으로 검증된 사례는 공식 문서와 커뮤니티에서 찾기 어려웠습니다. 이 조합을 직접 POC로 검증하는 비용이 Webpack 5 전환 비용보다 클 것으로 판단했습니다.
106106

107-
### **SWC** ([공식 문서](https://swc.rs/))
107+
#### SWC ([공식 문서](https://swc.rs/))
108108

109109
SWC는 Webpack을 유지하고 트랜스파일러만 교체하는 경로였습니다. 코드베이스에서 확인한 제약 조건도 `.swcrc` 설정으로 대부분 대응했습니다.
110110

@@ -117,27 +117,27 @@ Webpack 플러그인 생태계를 그대로 가져갈 수 있었습니다. 변
117117

118118
이렇게 해서 Webpack 5 + SWC 조합으로 좁혀졌습니다. Webpack 5는 가장 빠른 선택이 아니라 실제로 도달 가능한 선택이었고, SWC는 그 위에서 트랜스파일러 병목만 바꾸는 가장 작은 변경이었습니다.
119119

120-
# 별도 레포에서 시작한 이유
120+
## 별도 레포에서 시작한 이유
121121

122122
이 작업은 기존 운영 레포(`legacy-ui`)를 직접 건드리지 않고 시작했습니다.
123123

124124
운영 중인 레포에서 Webpack 버전을 올리다 빌드가 깨지면 다른 개발자에게 바로 영향이 갑니다. git 히스토리도 지저분해질 수밖에 없습니다. `legacy-ui`를 복사해 `turbo-ui`라는 별도 레포를 만들고, Jenkins에도 전용 빌드 Job을 새로 만들었습니다. 기존 CI/CD 파이프라인과 완전히 분리된 곳에서 Phase별로 검증하려는 선택이었습니다.
125125

126126
변경사항은 `turbo-ui`에서 검증을 끝낸 뒤 레포 옮기는 전략으로 잡았습니다.
127127

128-
# 단계적 전환
128+
## 단계적 전환
129129

130130
개선은 세 단계로 나누었습니다. 처음부터 근본 전환으로 뛰어들지 않았습니다. 기존 구조 안에서 줄일 수 있는 것부터 줄이고, 마지막에 Webpack 5와 SWC로 넘어갔습니다.
131131

132-
## 1단계: Webpack 2 환경에서 바로 줄이기
132+
### 1단계: Webpack 2 환경에서 바로 줄이기
133133

134134
Webpack을 건드리지 않고도 줄일 수 있는 것들이 있었습니다.
135135

136-
### **빌드 스크립트 통합**
136+
#### 빌드 스크립트 통합
137137

138138
기존에는 Jenkins 빌드가 시작될 때 내부 git 패키지를 git install 스크립트 순차 실행하면서, 매 빌드마다 `npm update``npm uninstall`을 반복했습니다. 이것을 `npm install --no-audit --no-fund` 단일 명령으로 통합했습니다.
139139

140-
### **캐시 활성화**
140+
#### 캐시 활성화
141141

142142
Babel이 변환한 결과를 로컬 캐시에 저장하도록 설정했습니다. 첫 빌드에는 효과가 거의 없지만, 같은 파일을 다시 빌드할 때 이전 변환 결과를 재사용할 수 있습니다.
143143

@@ -149,7 +149,7 @@ loaders: ['react-hot-loader', 'babel-loader']
149149
loaders: ['react-hot-loader', 'babel-loader?cacheDirectory']
150150
```
151151

152-
## 2단계: minification과 HMR 개선
152+
### 2단계: minification과 HMR 개선
153153

154154
1단계로 개발기 빌드에서 minification을 제거하고 로더 캐시를 켰지만, 운영 빌드의 병목 두 개는 그대로 남아 있었습니다.
155155

@@ -158,7 +158,7 @@ loaders: ['react-hot-loader', 'babel-loader?cacheDirectory']
158158

159159
이 단계에서는 Webpack 버전을 올리지 않고도 줄일 수 있는 부분을 먼저 정리했습니다.
160160

161-
### **minification 교체**
161+
#### minification 교체
162162

163163
`ParallelUglifyPlugin`을 Go 네이티브 바이너리로 동작하는 esbuild로 교체했습니다. 네이티브 코드 단일 패스 처리라 JS 기반 멀티 프로세스와 압축 속도 차이가 큽니다. 바로 사용 가능할줄 알았지만 `esbuild-loader`의 `ESBuildMinifyPlugin`을 붙였더니 실행하자마자 에러가 났습니다.
164164

@@ -212,7 +212,7 @@ new ESBuildMinifyWebpack2({
212212
});
213213
```
214214

215-
### **react-hot-loader 조건부 처리**
215+
#### react-hot-loader 조건부 처리
216216

217217
`react-hot-loader`는 개발 서버가 실행 중일 때 소스를 변경하면 페이지를 새로 고치지 않고 화면을 갱신해주는 HMR(Hot Module Replacement) 로더입니다. CI 빌드는 개발 서버를 띄우지 않아 이 기능이 동작할 수 없는데도, 모든 JS 파일이 이 로더 체인을 통과하고 있었습니다. 처리 대상이 14,520개 모듈인 환경에서 의미 없는 로더가 매 파일마다 끼어있는 셈이었습니다.
218218

@@ -226,7 +226,7 @@ loaders: process.env.CHECK_TYPE === 'local'
226226
: ['babel-loader?cacheDirectory']
227227
```
228228

229-
### **DllPlugin 도입**
229+
#### DllPlugin 도입
230230

231231
콜드 빌드에서 트랜스파일 병목을 줄이기 위해 Webpack의 DllPlugin도 이 단계에서 도입했습니다. DllPlugin은 `react``lodash` 같이 자주 바뀌지 않는 외부 라이브러리(vendor)를 한 번만 별도로 빌드해두는 방식입니다. 이후 빌드에서는 이미 처리된 결과를 참조하기 때문에, 소스 코드가 바뀌더라도 전체 11,383개 파일을 처음부터 다시 처리하지 않고 애플리케이션 코드만 재빌드할 수 있었습니다. `webpack.dll.config.js`를 작성하고 `build:dll` 스크립트를 추가해 vendor를 미리 빌드해두는 구조를 만들었습니다.
232232

@@ -236,9 +236,9 @@ loaders: process.env.CHECK_TYPE === 'local'
236236

237237
2단계까지 적용하였을때 운영 빌드 실측 기준으로 **전체 빌드 시간이 40~50분 → 31분**으로 줄었습니다. 하지만 Webpack은 여전히 11,383개 파일을 매번 처리했고, Babel 6도 그대로 였습니다. CI 환경의 콜드 빌드를 본격적으로 줄일려면 결국 Webpack5와 SWC로 넘어가야했습니다.
238238

239-
## 3단계: 근본 전환 — Webpack 5 + SWC
239+
### 3단계: 근본 전환 — Webpack 5 + SWC
240240

241-
### 예상 밖의 병목
241+
#### 예상 밖의 병목
242242

243243
3단계로 가려면 Node 10 / npm 6에서 Node 24로 먼저 올라가야 했습니다.
244244

@@ -275,7 +275,7 @@ loaders: process.env.CHECK_TYPE === 'local'
275275

276276
그 결과 `npm install`은 2시간 이상에서 1분으로 줄여 해결할 수 있었습니다.
277277

278-
### Webpack 5 전환
278+
#### Webpack 5 전환
279279

280280
Webpack 5에서는 Webpack 2 설정들을 상당수가 제거됐거나 다른 방식으로 바뀌었습니다.
281281

@@ -292,7 +292,7 @@ Webpack 5에서는 Webpack 2 설정들을 상당수가 제거됐거나 다른
292292

293293
전환하면서 `webpack.parts.js`도 약 200줄에서 96줄로 줄었습니다. `file-loader`, `url-loader`, `react-hot-loader`, `BabiliPlugin`, `CompressionPlugin`처럼 Webpack 5에서 내장됐거나 더 이상 필요하지 않은 로더와 플러그인을 걷어냈습니다. 설정은 짧아졌고 역할도 더 분명해졌습니다.
294294

295-
### Babel에서 SWC로
295+
#### Babel에서 SWC로
296296

297297
Webpack 5만으로는 충분하지 않았습니다.
298298

@@ -311,7 +311,7 @@ Webpack 5 브랜치를 두 갈래로 분리했습니다. 한쪽은 Babel 6을
311311
| 구현 언어 | JavaScript | Rust (네이티브 바이너리) |
312312
| CI 캐시 없을 때 | 매번 느림 | 캐시 없이도 빠름 |
313313

314-
### 전환 중 마주친 이슈들
314+
#### 전환 중 마주친 이슈들
315315

316316
Webpack 5와 SWC로 전환하면서 기존 빌드 도구가 묵인해주던 문제들이 한꺼번에 드러났습니다.
317317

@@ -322,7 +322,7 @@ Webpack 5와 SWC로 전환하면서 기존 빌드 도구가 묵인해주던 문
322322
| Node 코어 폴리필 런타임 에러 | Webpack 5가 `process`, `Buffer` 자동 주입 제거 | `resolve.fallback` + `ProvidePlugin`으로 수동 선언 |
323323
| `babel-polyfill` 중복 로딩 | entry와 소스 파일 양쪽에서 import | entry 제거 후 `NormalModuleReplacementPlugin`으로 빈 모듈 치환 |
324324

325-
## 결과
325+
### 결과
326326

327327
전환 이후 빌드 시간은 크게 줄었습니다.
328328

_posts/frontend/2026-05-24-frontend.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ categories: [Blogging,frontend]
3333

3434
어떻게 검증하고 유지할건지, 기존 레거시 코드를 어떻게 동기화 할지, 롤백 시나리오 등 운영 반영 가능한 상태로 만드는거였습니다.
3535

36-
# **레거시 UI와 터보 UI를 나란히 올리기**
36+
## 레거시 UI와 터보 UI를 나란히 올리기
3737

3838
레거시와 터보의 어떻게 동등성을 검증할 수 있을까?
3939

@@ -57,7 +57,7 @@ QA팀에서 두 화면을 브라우저 탭에 나란히 열고 같은 동작을
5757

5858
"레거시 UI와 같은 조건에서 같은 방식으로 동작하는가"였습니다.
5959

60-
# **QA 검수 방식**
60+
## QA 검수 방식
6161

6262
이 프로젝트에는 이미 갖춰진 E2E 테스트 인프라가 없었습니다. React 버전도 오래됐고, 서비스 수가 많았으며, 복잡한 실제 업무 데이터 흐름이 얽혀 있었습니다. 이 상태에서 Playwright나 Cypress 같은 도구를 새로 도입해 전수 자동화를 만드는 것은 별도 프로젝트에 가까웠습니다.
6363

@@ -95,15 +95,15 @@ Network:
9595

9696
이 방식은 수치로 표현되지 않습니다. 하지만 당시 조건에서는 가장 현실적인 방법이었습니다. 자동화 테스트를 도입하지 않았다는 사실을 감추기보다, QA팀이 할수 있는 영역과 모니터링이 보완하는 영역을 나누는 편이 더 안전했습니다.
9797

98-
# **모니터링 수치로 판단하기**
98+
## 모니터링 수치로 판단하기
9999

100100
QA팀은 정해진 시나리오를 확인합니다. 하지만 사용자는 정해진 시나리오대로만 움직이지 않습니다.
101101

102102
특정 데이터 상태에서만 깨지는 화면이 있습니다. 특정 브라우저에서만 발생하는 오류가 있습니다. 오래 머물렀을 때만 발생하는 비동기 오류도 있습니다. 레거시 UI에서는 이런 문제가 늦게 발견되기 쉽습니다.
103103

104104
그래서 신규 빌드 UI에는 빌드 단계부터 런타임까지 세 영역에 모니터링을 붙였습니다. 빌드 시간 추이, 에러 발생, 성능 지표를 각각 수집했습니다.
105105

106-
## **빌드 모니터링**
106+
### 빌드 모니터링
107107

108108
빌드 단계에서는 다음 값을 모니터링 시스템으로 보냈습니다.
109109

@@ -122,7 +122,7 @@ QA팀은 정해진 시나리오를 확인합니다. 하지만 사용자는 정
122122

123123
![image2.png](/assets/img/frontend/2026-05-24-frontend-04.png)
124124

125-
## **에러 수집**
125+
### 에러 수집
126126

127127
JS 런타임 오류(`script_error`)와 Promise 미처리 거부(`unhandled_rejection`) 두 종류로 나뉘며, 각 이벤트에 `buildVariant` 필드를 함께 기록해 turbo와 legacy를 구분할 수 있게 했습니다.
128128

@@ -136,7 +136,7 @@ JS 런타임 오류(`script_error`)와 Promise 미처리 거부(`unhandled_rejec
136136

137137
![image3.png](/assets/img/frontend/2026-05-24-frontend-05.png)
138138

139-
## **성능 지표**
139+
### 성능 지표
140140

141141
이번 프로젝트의 목표는 빌드 시간 단축이었습니다. 성능 지표는 신규 빌드 UI가 기존 UI 대비 크게 나빠지지 않았는지 이상치를 확인하는 참고용으로만 수집했습니다. 레거시 서비스 특성상 두 빌드 모두 절대 수치가 빠르지 않아, 수치 자체의 개선은 이번 범위 밖이었습니다.
142142

@@ -149,7 +149,7 @@ JS 런타임 오류(`script_error`)와 Promise 미처리 거부(`unhandled_rejec
149149

150150
![스크린샷 2026-05-21 오후 4.27.17.png](/assets/img/frontend/2026-05-24-frontend-06.png)
151151

152-
# **기존 레거시 동기화**
152+
## 기존 레거시 동기화
153153

154154
신규 빌드 UI는 별도 레포에서 시작했습니다.
155155

@@ -176,7 +176,7 @@ JS 런타임 오류(`script_error`)와 Promise 미처리 거부(`unhandled_rejec
176176
| SWC 설정 | 제외 | 신규 빌드 레포 전용 컴파일러 설정 |
177177
| 빌드 스크립트 | 제외 | 레포별 운영 방식이 다름 |
178178

179-
## **커밋 체크포인트로 동기화 위치 추적하기**
179+
### 커밋 체크포인트로 동기화 위치 추적하기
180180

181181
처음에는 단순히 특정 디렉토리를 checkout하는 방식도 생각했습니다. 하지만 이 방식은 커밋 히스토리를 잃습니다. 누가 어떤 변경을 했는지, 원본 커밋이 무엇인지 추적하기 어렵습니다.
182182

@@ -200,7 +200,7 @@ Legacy-Repo: legacy-ui
200200

201201
작성자, 작성 날짜, 커밋 메시지가 그대로 보존되고 원본 레거시 SHA가 메시지로 기록됩니다. 장애가 생겼을 때 "어떤 기존 레거시 커밋이 신규 빌드 UI에 들어온 뒤 문제가 생겼는가"를 `git log --grep="Legacy-Commit:"` 한 줄로 추적할 수 있습니다.
202202

203-
## **동기화 전에 먼저 해결해야 했던 것**
203+
### 동기화 전에 먼저 해결해야 했던 것
204204

205205
신규 빌드 레포의 소스는 기존 레거시 소스를 그대로 둔 상태가 아니었습니다. SWC 전환 과정에서 기존 Babel 6가 통과시키던 문법 중 일부를 직접 수정했습니다. `System.import()`는 표준 동적 `import()`로 바꿨습니다. 일부 JSX 문법 오류와 `.js` 파일 안의 TypeScript 타입 표기도 정리했습니다. 중복 폴리필 import도 제거했습니다.
206206

@@ -218,15 +218,15 @@ Legacy-Repo: legacy-ui
218218

219219
분리된 신규 빌드 레포가 운영 이관 전까지 기존 레거시 개발 흐름을 놓치지 않게 하는 것. 그리고 문제가 들어왔을 때 어느 커밋에서 들어왔는지 추적할 수 있게 하는 것이었습니다.
220220

221-
# **롤백 시나리오**
221+
## 롤백 시나리오
222222

223223
마지막으로 롤백 시나리오를 세웠습니다.
224224

225225
롤백은 장애가 난 뒤에 생각하면 늦습니다. 특히 프론트엔드 번들은 정적 파일처럼 보이지만, 실제로는 API, 인증, 브라우저 캐시, chunk 로딩과 연결되어 있습니다. 배포 직후 화이트스크린이 뜨거나 chunk 파일이 404를 내면, 원인을 분석하는 동안 사용자는 계속 막혀 있습니다.
226226

227227
핵심 전제는 하나였습니다. 롤백은 "다시 빌드하는 것"이 아니어야 한다는 것입니다.
228228

229-
## **병렬 빌드 기반 롤백 구조**
229+
### 병렬 빌드 기반 롤백 구조
230230

231231
레거시 JOB과 터보 JOB은 항상 같은 소스를 기준으로 나란히 빌드됩니다. 동기화 파이프라인이 레거시 레포의 최신 변경을 터보 레포로 반영하고, Jenkins는 두 JOB을 동시에 실행합니다. 두 JOB이 모두 성공하면 각각의 번들 파일이 Jenkins 서버에 저장됩니다.
232232

0 commit comments

Comments
 (0)