대규모 시스템 설계 스터디 01 — 1명에서 수백만 명까지
이 글은 시스템 설계 면접에서 자주 나오는 서버 1대 구조가 어떻게 수백만 사용자 구조로 진화하는지를 1장 기준으로 정리한 스터디 기록이다. Claude와 질문하고 역질문 받으면서 체화한 내용을 그대로 옮겼고, 책 내용만이 아니라 공부하면서 튀어나온 질문도 함께 담았다.
시작: 서버 1대
처음 구조는 이렇다.
유저 → 서버 1대 → DB 1개
서버가 웹 요청도 받고, DB도 들고 있다. 유저가 1명이면 아무 문제 없다. 유저가 늘기 시작하면 하나씩 무너진다. 1장은 이 구조가 어떻게 진화하는지 따라간다.
첫 번째 변화는 단순하다. DB를 서버에서 분리한다.
유저 → 웹 서버 (요청 처리)
→ DB 서버 (데이터 저장)
웹 서버는 CPU를 많이 쓰고, DB는 디스크 I/O를 많이 쓴다. 한 서버에 두면 서로 방해한다.
수직 확장 vs 수평 확장
서버가 버티질 못하기 시작하면 두 가지 선택지가 생긴다.
Scale Up — 서버 스펙을 올린다. 코드 변경 없이 더 빵빵한 서버로 교체한다. 단순하지만 한계가 있다. 아무리 비싼 서버도 끝이 있고, 1대니까 죽으면 전체가 죽는다.
Scale Out — 서버를 여러 대 늘린다. 이론적으로 무한 확장 가능. 대규모 시스템의 표준 방향이다. 앞단에 로드밸런서를 둬서 요청을 분산한다.
유저 → 로드밸런서 → 서버 1
→ 서버 2
→ 서버 3
로드밸런서는 서버 1이 죽으면 감지하고 서버 2, 3으로만 보낸다. 유저는 모른다. 로드밸런서 자체도 Active-Standby로 2대 운영한다. SPOF(단일 장애점) 제거가 핵심 원칙이다.
캐시 — Memcached API로 파고들기
책에 캐시 계층이 나왔을 때 Memcached API를 직접 물어봤다. 개념보다 실제 명령어를 먼저 보면 이해가 빠르다.
Memcached 핵심 명령어:
| 명령어 | 동작 |
|---|---|
set | 있으면 덮어씀 |
add | 없을 때만 저장 |
get | 조회 |
delete | 삭제 |
incr/decr | 숫자 증감 |
여기서 의문이 생겼다. set이랑 add를 왜 구분해서 쓸까?
답은 캐시 전략에 있다.
Read-Through에서는 add를 써야 한다
캐시 미스가 났을 때 두 요청이 동시에 DB로 달려가는 상황을 생각해보자.
요청 A → 캐시 미스 → DB 조회 중...
요청 B → 캐시 미스 → DB 조회 중...
요청 A → 캐시에 set
요청 B → 캐시에 set → A가 저장한 값을 덮어씀 ❌
add를 쓰면 두 번째 저장은 조용히 실패한다. 이미 있으니까. 불필요한 덮어쓰기를 막는다. 레이스 컨디션 방지가 목적이다.
Write-Through에서는 반드시 set을 써야 한다
Write-Through는 쓸 때 캐시와 DB를 동시에 갱신하는 전략이다. 이미 있는 값을 최신으로 업데이트하는 게 목적이니까 set으로 무조건 덮어써야 한다. add를 쓰면 이미 키가 있을 때 저장이 안 되고, 캐시가 구버전으로 남는다.
결론: Read-Through는 읽기 요청에서 캐시를 채우는 전략, Write-Through는 쓰기 요청 시 캐시와 DB를 동시에 갱신하는 전략. 둘은 서로 독립적이라 함께 쓸 수 있다.
Write-Back — 빠르지만 위험하다
여기서 세 번째 전략이 나온다. 캐시에만 먼저 쓰고, DB는 나중에 비동기로 저장한다.
언제 쓰냐가 중요하다. 쓰기가 폭발적으로 많은데 모든 쓰기를 DB에 즉시 반영할 필요가 없을 때다.
게임 경험치가 좋은 예다. 몬스터 잡을 때마다 경험치 +10. 1분에 수백 번 업데이트가 발생한다. 매번 DB에 쓰면 DB가 버티질 못한다. 캐시에서 누적하다가 주기적으로 DB에 반영하면 된다.
좋아요 카운터, 실시간 위치 추적도 같다. 반면 결제, 주문, 재고는 절대 Write-Back을 쓰면 안 된다. 캐시 장애 시 데이터가 날아간다.
판단 기준은 하나다.
"이 데이터가 유실되면 얼마나 치명적인가?"
캐시 계층 — 브라우저, CDN, Redis는 뭐가 다른가
책에 "캐시를 어떤 계층에 두느냐"는 표현이 나온다. 처음엔 뭔 소린지 몰랐다. 직접 물어보니 계층이 세 가지였다.
브라우저 캐시 (내 컴퓨터)
↓
CDN (네트워크 엣지, 전 세계 공유)
↓
인메모리 캐시 Redis/Memcached (서버 제어)
↓
DB
결정적 차이는 제어권이다.
브라우저 캐시는 클라이언트 것이다. 서버가 Cache-Control 헤더로 힌트를 줄 수는 있지만, 만료 전엔 서버에 요청 자체를 안 보낸다. 서버가 데이터를 바꿔도 클라이언트는 모른다. 배포 후 구버전 JS/CSS 문제가 여기서 생긴다. 쓰기 전략 적용이 사실상 불가능하다.
CDN은 "정적 콘텐츠 캐싱이니까 브라우저 캐시랑 같은 거 아니야?"라는 의문이 들었다. 다르다. 결정적 차이는 공유 여부다.
브라우저 캐시:
유저 A가 이미지 다운로드 → A 브라우저에만 저장
유저 B는 여전히 오리진에서 다운로드
CDN:
유저 A가 이미지 요청 → CDN에 없으면 오리진에서 가져와 CDN에 저장
유저 B가 같은 이미지 요청 → CDN에서 바로 응답 (오리진 안 감)
CDN은 전 세계 유저가 공유하는 캐시다. 브라우저 캐시는 나만 쓴다.
실제로 한 페이지를 열 때 이미지는 CDN에서, API 데이터는 Redis에서 동시에 온다. 브라우저가 두 요청을 병렬로 날린다. 각자 역할 분담이다.
세션은 어디에 저장되나
캐시 얘기를 하다가 세션 쿠키 얘기가 나왔다.
"세션 쿠키가 브라우저 캐시인가?"
아니다. 목적이 완전히 다르다.
| 브라우저 캐시 | 쿠키 | |
|---|---|---|
| 목적 | 성능 (재요청 줄이기) | 상태 유지 (서버가 클라이언트 식별) |
| 매 요청 전송 | ❌ 자동 전송 안 함 | ✅ 자동 전송 |
쿠키는 세션 ID(열쇠)만 가지고 있다. 실제 세션 데이터는 서버의 Redis나 NoSQL에 있다. 브라우저는 열쇠만 들고 다니고, 서버가 그 열쇠로 금고를 여는 구조다.
무상태 아키텍처 — 왜 NoSQL에 세션을 저장하나
책 그림 1-14에서 NoSQL이 두 데이터센터 밖에 독립적으로 있는 게 보인다. 처음엔 이게 뭔지 헷갈렸다.
서버가 여러 대로 늘어나면 문제가 생긴다.
유저 → 서버 1 (세션 저장)
유저 재요청 → 서버 2로 가면 → 세션 없음 → 로그인 풀림 ❌
세션을 서버 메모리에 두면 특정 서버에 종속된다. 로드밸런서가 자유롭게 분배를 못 한다. 서버를 늘리고 줄이는 자동 확장이 불가능해진다.
해결책: 세션을 서버 메모리에서 꺼내 공유 저장소로 옮긴다.
서버 1 \
서버 2 → Redis / NoSQL (세션 공유 저장소)
서버 3 /
어느 서버로 가든 같은 세션을 조회한다. 이게 Stateless 아키텍처의 본질이다.
그림에서 NoSQL이 두 DC 밖에 있는 이유도 여기 있다. DC2가 장애 나도 NoSQL은 살아있고, 세션이 유지된다. DC1 복구 후 유저는 로그인 풀리지 않은 상태로 돌아온다.
SAML — 사내 시스템의 세션 관리
세션 얘기가 SAML까지 이어졌다.
SAML은 SSO(Single Sign-On)를 구현하는 인증 프로토콜이다. 인증을 IdP에 위임한다.
IdP (Identity Provider) — 인증 담당. 회사 계정 서버.
SP (Service Provider) — 서비스. 사내 전자결재, 그룹웨어 등.
흐름은 이렇다.
1. 유저 → SP 접근
2. SP → IdP로 리다이렉트 (SAML Request)
3. 유저 → IdP에서 로그인
4. IdP → SAML Assertion(XML 토큰) 발급
5. SP → Assertion 검증 → 자체 세션 생성
여기서 세션이 두 개 생긴다.
| 세션 | 위치 | 역할 |
|---|---|---|
| IdP 세션 | IdP 서버 | "이 유저는 이미 인증됨" |
| SP 세션 | SP 서버 (쿠키) | "이 유저는 이 서비스 이용 중" |
IdP 세션이 살아있으면 → 다른 SP 접근 시 로그인 없이 바로 Assertion 발급. SSO가 되는 이유다.
일반 세션 vs SAML:
| 일반 세션 | SAML | |
|---|---|---|
| 인증 주체 | 서비스 직접 | IdP가 대신 |
| 세션 수 | 1개 | 2개 (IdP + SP) |
| SSO | ❌ | ✅ |
| IdP 장애 시 | 서비스 정상 | 모든 서비스 로그인 불가 |
대기업 사내 시스템이 SAML을 많이 쓰는 이유가 있다. 직원 퇴사 시 IdP 계정 하나만 삭제하면 연결된 모든 사내 서비스 접근이 동시에 차단된다. 인사팀 입장에서 강력한 도구다.
SAML 장애 분석 — 전자결재 XML 오류
공부하다 실제 업무 케이스가 나왔다. 전자결재 시스템에서 특정 문서 생성 시 오류가 반복 발생하는 상황이었다.
처음 가설은 "IdP/SP 세션 동기화 문제"였다. 흐름을 단계별로 짚어보면 이렇다.
1. 사내시스템 데이터 작성 ✅
2. 전자결재 버튼 클릭 ✅
3. SAML 세션 인증 ✅ 통과
4. 전자결재 API 호출 ✅
5. API에서 XML 문서 생성 ❌ 오류
3단계 SAML 인증이 통과했다는 게 핵심이다. 세션 동기화 문제였다면 3단계에서 실패했을 것이다.
실제 원인은 사내시스템 → 전자결재 API로 넘어가는 데이터 형식 불일치일 가능성이 높다. 특히 결재선이 1명일 때 단일 값으로 직렬화되고, 2명 이상일 때 배열로 직렬화되는 구조 차이가 XML 생성 단계에서 터진다.
이런 케이스가 흥미로운 이유는 아키텍처 지식이 실제 장애 분석에 그대로 쓰인다는 점이다. 흐름을 계층별로 나눠서 보면 어디서 터졌는지 좁히는 게 빠르다.
DB 다중화 — 다중화와 분산은 다르다
DB 다중화 설명에서 중요한 개념 구분이 있었다.
"주/부 구조면 전체 데이터가 두 DB 모두에 있는 거 맞지? 분산 DB 전략이 아니고?"
맞다. 둘은 완전히 다른 전략이다.
다중화 (Replication):
주 DB: [A, B, C, D, E]
부 DB: [A, B, C, D, E] ← 전체 복사본
→ 목적: 가용성 + 읽기 성능 분산
샤딩 (Sharding):
DB 1: [A, B]
DB 2: [C, D]
DB 3: [E] ← 데이터를 나눠서 저장
→ 목적: 쓰기 성능 + 용량 확장
다중화는 같은 데이터를 여러 곳에, 샤딩은 다른 데이터를 여러 곳에.
동기화는 어떻게 하나
변경이 어떻게 부 DB에 전달되는지도 궁금했다.
**바이너리 로그(binlog)**를 쓴다. 주 DB의 모든 변경 쿼리가 시간순으로 로그에 기록되고, 부 DB가 그걸 구독해서 동일하게 재실행한다.
주 DB → 쿼리 실행 → binlog 기록
↓
부 DB IO Thread → binlog 구독 → Relay Log에 복사
↓
부 DB SQL Thread → 쿼리 재실행
↓
부 DB 데이터 반영
부 DB는 주 DB의 쿼리를 그대로 다시 실행하는 방식이다.
동기화 방식에 따라 트레이드오프가 갈린다.
| 비동기 | Semi-Sync | 완전 동기 | |
|---|---|---|---|
| 속도 | 빠름 | 중간 | 느림 |
| 유실 위험 | 있음 | 거의 없음 | 없음 |
| 실무 사용 | 일반적 | 중요 데이터 | 특수 상황 |
대부분은 비동기를 쓴다. 빠르고 충분하다. Semi-Sync는 주 DB가 죽어도 최소 1개 부 DB에는 데이터가 남아있도록 보장한다.
Replication Lag — 방금 내가 쓴 데이터가 안 보인다
비동기 복제라 시간차가 생긴다. 내가 방금 바꾼 데이터를 부 DB에서 조회하면 아직 구버전일 수 있다.
유저 닉네임 변경: "민성" → "MinSung"
0.05초 후 프로필 조회 → 부 DB에서 읽음 → 아직 "민성" ❌
이게 Read-Your-Writes 문제다. 해결 방법:
// 같은 트랜잭션 안에서 쓰기 직후 읽기 → 주 DB 사용
@Transactional
public User updateAndReturn(Long id, String nickname) {
userRepository.save(user); // 주 DB 쓰기
return userRepository.findById(id); // 같은 트랜잭션 → 주 DB 읽기
}
SNS 프로필 수정, 주문 완료 후 내역 확인처럼 "내가 방금 쓴 걸 즉시 봐야 하는" 케이스가 여기 해당한다. 다른 유저 게시물 조회나 통계 대시보드는 부 DB에서 읽어도 무방하다.
쿼리 라우팅 — ProxySQL
읽기는 부 DB로, 쓰기는 주 DB로 자동 분기하는 방법이 두 가지다.
애플리케이션 코드에서 직접 DataSource를 나눠 처리하거나, ProxySQL 같은 미들웨어를 DB 앞에 두는 방법이다.
애플리케이션 → ProxySQL → 주 DB (INSERT/UPDATE/DELETE 자동)
→ 부 DB (SELECT 자동)
ProxySQL을 쓰면 애플리케이션 입장에서 DB가 하나인 것처럼 연결할 수 있다. 코드 변경 없이 라우팅이 된다. 부가적으로 커넥션 풀링도 처리해준다. 서버 100대가 DB에 직접 붙으면 커넥션이 100개 필요하지만, ProxySQL을 거치면 10개로도 충분하다.
샤딩 — 레코드 단위 분리
샤딩에서도 개념 정리가 있었다.
"샤딩은 테이블 단위가 아니고 레코드 단위인가?"
둘 다 가능하다.
수평 샤딩(Horizontal) — 레코드 단위. 같은 테이블을 여러 DB에 나눠서 저장한다. 보통 "샤딩"이라고 하면 이걸 말한다.
user_id 1~1000만 → 샤드 1
user_id 1000만~2000만 → 샤드 2
user_id 2000만~ → 샤드 3
수직 샤딩(Vertical) — 테이블/도메인 단위. 도메인 자체를 다른 DB로 분리한다.
DB1: 유저 서비스 (users, user_profiles)
DB2: 주문 서비스 (orders, payments)
DB3: 상품 서비스 (products, reviews)
마이크로서비스 아키텍처랑 자연스럽게 연결된다. 팀이 나뉘어 있으면 각 팀이 자기 DB만 접근하면 된다.
샤딩 방법 세 가지
데이터를 어느 샤드로 보낼지 결정하는 방법:
- 범위 기반 —
user_id 1~1000만 → 샤드 1. 단순하지만 특정 범위에 몰리면 핫스팟 발생 - 해시 기반 —
hash(user_id) % 3. 균등하게 분산. 샤드 추가하면 재배치 필요 (5장 안정 해시로 해결) - 위치 기반 — 한국 유저 → Korea 리전. 글로벌 서비스에 적합. 지역별 불균등 가능
수직 샤딩 후 조인 문제
버티컬 샤딩의 단점은 명확하다. DB가 나뉘면 SQL JOIN이 안 된다.
users → DB1
orders → DB2
SELECT u.name, o.order_date FROM users u JOIN orders o ...
→ 불가능
해결책은 세 가지다. 애플리케이션 레벨에서 각 DB를 따로 조회해서 합치거나, 자주 쓰는 컬럼을 중복 저장하거나, 통계/분석용 데이터 웨어하우스에서 배치로 조인한다.
자주 조인하는 테이블을 억지로 나누면 성능이 오히려 나빠진다. 도메인 간 결합도가 낮을 때만 수직 샤딩이 효과적이다.
대규모 시스템은 보통 이 순서로 간다.
수직 샤딩 (도메인 분리) → 각 도메인 내에서 수평 샤딩
메시지 큐 — 소비자가 뭔데
메시지 큐를 공부할 때 "소비자"라는 단어가 처음 나왔다.
"큐에서 메시지를 꺼내 처리하는 서버 또는 프로세스."
생산자 (앞단, 유저 요청 받음) → [메시지 큐] → 소비자 (뒤단, 실제 처리)
생산자가 앞단이다. 소비자가 뒤단이다. 유튜브라면 영상 업로드를 받는 웹 서버가 생산자, 인코딩 서버가 소비자다.
생산자는 큐에 넣고 즉시 응답한다. 소비자가 알아서 꺼내 처리한다. 유저는 인코딩이 끝날 때까지 기다리지 않아도 된다.
소비자를 여러 개 띄우면 병렬 처리가 된다.
[메시지 큐]
작업1 → 소비자 1 (처리 중)
작업2 → 소비자 2 (처리 중)
작업3 → 소비자 3 (처리 중)
작업4 → 대기
트래픽 폭발 시 소비자만 늘리면 된다. 생산자는 신경 안 써도 된다.
ack/nack — 메시지 유실을 막는 방법
// 처리 성공
channel.basicAck(deliveryTag, false);
// 큐 → 해당 메시지 삭제
// 처리 실패 (재시도)
channel.basicNack(deliveryTag, false, true); // requeue=true → 큐에 다시 넣음
// 처리 실패 (버림)
channel.basicNack(deliveryTag, false, false); // requeue=false → DLQ로
ack를 받기 전까지 큐는 메시지를 "처리 중"으로 간주한다. 소비자가 메시지를 받고 처리 중에 갑자기 죽으면?
소비자 → 메시지 받음 → 처리 중 → 죽음 (ack 못 보냄)
큐 → ack 안 옴 → 다른 소비자에게 자동 재전달
메시지가 유실되지 않는다.
처리를 반복해서 실패한 메시지는 **Dead Letter Queue(DLQ)**로 보낸다. 개발자가 나중에 원인을 분석하고 수동으로 재처리할 수 있다. 전자결재처럼 유실이 절대 안 되는 시스템에서 중요하다.
TCP의 ack와 철학이 같다. TCP는 패킷 수신을 확인하고, 메시지 큐 ack는 메시지 처리 완료를 확인한다. 계층만 다를 뿐 "확인 응답으로 유실 방지"라는 개념은 동일하다.
다중화 — 모든 계층에 SPOF를 없애라
계층별로 다중화를 정리하면 이렇다.
| 계층 | 다중화 방법 | 목적 |
|---|---|---|
| DNS | 클라우드 자동 처리 | 도메인 장애 방지 |
| 로드밸런서 | Active-Standby 2대 | 트래픽 입구 보호 |
| 웹 서버 | 서버 N대 | 트래픽 분산 + 장애 격리 |
| 캐시 | Redis Cluster | DB 부하 차단 유지 |
| DB | 주/부 복제 | 데이터 유실 방지 |
| 데이터센터 | DC 2개 이상 | 지역 단위 재해 대비 |
로드밸런서는 요청을 분배만 하고 처리는 안 해서 병목이 거의 안 된다. 장애 시 가장 먼저 터지는 건 실제 처리를 담당하는 웹 서버다.
독립적 분리 — 장애 격리와 확장성
모놀리식 구조에서는 이미지 처리가 과부하여도 결제가 같이 느려진다. 각 기능이 독립된 서비스로 분리되면:
- 장애 격리 — 이미지 서비스가 죽어도 주문/결제는 정상
- 독립적 스케일 아웃 — 이벤트 때 주문 서비스만 10대로 늘림
- 독립적 배포 — 결제 로직 수정 시 결제 서비스만 배포
마무리 역질문 세 개
챕터 끝에 역질문을 받았다.
Q1. 유저가 갑자기 10배로 늘면 병목이 어디서 먼저 생기나?
처음엔 로드밸런서가 먼저 터질 것 같았다. 아니었다.
웹 서버 → DB → 캐시 → NoSQL → 로드밸런서
요청을 직접 처리하는 웹 서버가 가장 먼저 과부하가 온다. 웹 서버를 스케일 아웃하면 DB로 부하가 옮겨간다. 로드밸런서는 분배만 하기 때문에 처리량이 매우 높고 마지막에 고려하게 된다.
Q2. 전자결재 시스템을 1장 아키텍처로 설계한다면?
두 가지가 바로 떠올랐다.
첫째, 전자결재 처리는 메시지 큐로 분리한다. 사내시스템(생산자)이 큐에 넣고 즉시 "접수됐습니다" 응답. 전자결재 서버(소비자)가 뒤에서 알아서 처리. XML 생성이나 PDF 변환처럼 오래 걸리는 작업이 유저를 기다리게 하지 않는다.
둘째, 웹 서버가 여러 대면 SAML 세션 문제가 생긴다. 세션을 NoSQL에 저장해서 해결한다.
유저
↓
사내시스템 웹 서버 (Stateless)
↓ ↓
NoSQL(세션) 메시지 큐
↓
전자결재 서버 (소비자)
↓
DB (결재 문서)
Q3. 캐시, 메시지 큐, NoSQL 중 하나만 쓴다면?
캐시를 골랐다.
"DB 읽기가 레이턴시에 가장 큰 영향을 준다. 캐시가 그 부하를 차단하면 전체 성능 향상이 가장 크다."
캐시 히트율 80% 가정:
DB 요청 10,000건 → 캐시가 8,000건 차단 → DB는 2,000건만 처리
DB 100ms → 캐시 1ms
메시지 큐는 특정 작업을 빠르게, NoSQL은 특정 데이터를 빠르게. 캐시는 전체 시스템을빠르게 만든다.
1장 핵심 두 가지
모든 진화 과정을 관통하는 원칙이 두 가지다.
다중화 — SPOF를 없앤다. 어느 계층이든 한 군데가 죽어도 전체가 안 죽는 구조.
독립적 분리 — 컴포넌트를 나눈다. 장애가 격리되고, 필요한 부분만 스케일 아웃할 수 있다.
이 두 가지가 합쳐져야 수백만 사용자를 감당하는 시스템이 된다.
다음은 2장 — 개략적인 규모 추정. "이 시스템 QPS가 얼마야?" 같은 면접 질문에 빠르게 답하는 감각을 훈련한다.
이어 읽기
시리즈는 순서대로, 편집 추천은 맥락대로, 비슷한 주제는 태그 기준으로 정리합니다.
시리즈 전체
대규모 시스템 설계 스터디1/1편- 1.대규모 시스템 설계 스터디 01 — 1명에서 수백만 명까지
함께 읽으면 좋은 글
편집 추천비슷한 주제의 글
태그가 겹치는 글입니다. 시리즈와 편집 추천에 이미 나온 글은 제외합니다.
LLM 공부 06 | MoE와 GPU 클러스터: 거대 모델은 어떻게 나뉘어 도는가
MoE expert가 MLP/FFN과 어떻게 연결되는지 설명하고, 거대 LLM serving이 GPU memory, sharding, replication, interconnect, KV cache, scheduler를 다루는 분산컴퓨팅 문제가 되는 이유를 정리한다.
AI 웹개발 기초: 프론트엔드 1-3 | jQuery에서 React로 넘어간 진짜 이유
jQuery의 DOM 직접 조작과 AJAX가 해결한 문제를 인정하고, UI가 커지면서 React, Vue, Angular 같은 state/data 기반 component UI가 왜 필요해졌는지 설명하는 AI 웹개발 기초 시리즈 프론트엔드 1-3.
AI 웹개발 기초: 프론트엔드 1-4 | React를 쓰는데 왜 Node.js와 npm이 필요할까
React와 Vue 같은 현대 프론트엔드 프로젝트에서 Node.js와 npm이 왜 필요한지, 브라우저 실행 코드와 Node.js 기반 개발 도구를 나누어 설명하고 package.json, lockfile, Vite build, bundler/compiler, tree shaking, code splitting, environment variable, source map, CI cache까지 연결하는 AI 웹개발 기초 시리즈 프론트엔드 1-4.