코드 분할
고친 사람 github-actions[bot]
코드 분할은 웹 앱의 자바스크립트를 한 덩어리로 보내지 않고 여러 조각으로 나눠 보냅니다. 브라우저는 첫 화면에 필요한 조각만 먼저 받습니다. 나머지 조각은 사용자가 그 기능을 쓰려 할 때 받습니다. 그래서 앱이 커져도 첫 화면이 뜨는 시간이 덜 늘어납니다.
쉽고 빠른 이해
코드 분할은 「앱 코드를 화면별로 나눠 보내기」입니다. 쇼핑몰이라면 상품 목록 화면의 코드와 결제 화면의 코드를 다른 파일에 담습니다.
이게 없으면 첫 화면만 보려는 사용자도 앱 코드 전부를 받아야 합니다. 앱이 클수록 첫 화면이 늦게 뜹니다.
도는 순서는 이렇습니다.
- 코드에 「이 부분은 나중에 받아도 된다」는 표시를 해 둡니다
- 빌드할 때 그 표시를 기준으로 파일을 여러 개로 나눕니다
- 브라우저는 첫 화면 파일만 먼저 받습니다. 나머지는 필요해질 때 받습니다
대가가 있습니다. 나중 파일을 받는 동안 사용자가 잠깐 기다립니다. 너무 잘게 나누면 받을 파일이 많아져 오히려 느려집니다. 그래서 코드 분할은 앱이 커서 첫 화면이 늦을 때 씁니다. 작은 앱에는 쓰지 않습니다.
상세
두꺼운 백과사전을 한 권으로 제본했다고 해 보겠습니다. 항목 하나를 찾으려 해도 그 책 전체를 들고 와야 합니다. 여러 권으로 나눠 두면 오늘 찾을 항목이 든 권만 꺼내 옵니다. 코드 분할은 웹 앱의 코드를 이렇게 여러 권으로 나눠 둡니다.
번들 하나가 커지는 문제
웹 앱의 자바스크립트 코드는 보통 파일 수백 개로 나뉘어 쓰입니다. 이 파일 하나하나를 모듈이라고 부릅니다. 브라우저가 모듈 수백 개를 하나씩 따로 받으면 요청이 그만큼 쌓여 느려집니다.
그래서 번들러가 배포 전에 모듈들을 한 파일로 묶습니다. 이렇게 묶은 파일을 번들이라고 부릅니다. 번들러는 어느 모듈이 어느 모듈을 불러 쓰는지 따라가며 필요한 모듈을 모읍니다.
묶기만 하면 새 문제가 생깁니다. 첫 화면에 필요 없는 코드까지 번들 하나에 다 들어갑니다. 쇼핑몰이라면 상품 목록만 보러 온 사용자도 결제 화면과 관리자 화면의 코드를 함께 받습니다.
받는 것으로 끝나지도 않습니다. 브라우저는 받은 자바스크립트를 읽어 들여 실행한 뒤에야 화면이 반응합니다. 번들이 클수록 받는 시간과 실행하는 시간이 둘 다 늘어납니다.
서버 애플리케이션에서는 잘 안 겪는 문제입니다. 서버는 프로그램을 한 번 띄우면 코드를 메모리에 올려 두고 계속 씁니다. 웹 앱의 코드는 사용자가 페이지를 열 때마다 네트워크를 건너 브라우저로 갑니다. 처음 오는 사용자와 새로 배포한 뒤에 온 사용자는 그 코드를 전부 새로 받습니다.
청크와 나누는 지점
코드 분할은 번들을 다시 여러 조각으로 나눕니다. 나뉜 조각 하나를 청크라고 부릅니다. 청크 하나가 브라우저가 받는 파일 하나입니다.
페이지를 열 때 바로 받는 첫 청크가 하나 있습니다. 나중에 받을 청크는 여럿 생깁니다.
어디서 나눌지는 개발자가 코드에 적습니다. 모듈을 불러오는 방법이 둘이라 그 차이로 표시합니다. 앱의 첫 화면 코드 파일에 아래 두 줄이 있다고 해 봅니다. 오른쪽 주석에 a.js·b.js 가 각각 들어가는 청크를 적었습니다.
import a from "./a.js"; // 첫 청크
const p = import("./b.js"); // 별도 청크
첫 줄은 정적 임포트입니다. 이 첫 화면 파일이 실행되기 전에 a.js 가 반드시 준비돼 있어야 한다는 뜻입니다. 그래서 번들러는 a.js 를 첫 청크에 같이 넣습니다.
둘째 줄은 동적 임포트입니다. 함수처럼 괄호를 붙여 부릅니다. 이 줄이 실행되는 순간에야 b.js 를 가져옵니다. 번들러는 이것을 「나중에 받아도 된다」는 표시로 읽습니다. 그래서 b.js 를 별도 청크로 뺍니다.
동적 임포트는 모듈을 바로 돌려주지 못합니다. 받는 데 시간이 걸리기 때문입니다. 대신 모듈이 도착하면 그것을 넘겨주겠다는 프로미스(Promise)를 돌려줍니다. 자바의 CompletableFuture 가 나중에 올 결과를 대신 들고 있는 것과 같은 생각입니다.
브라우저가 청크를 받는 순서
나뉜 청크가 어떻게 오가는지 버튼 하나로 따라가 보겠습니다. 쇼핑몰 관리 화면에 매출 차트 버튼이 있습니다. 차트를 그리는 코드는 동적 임포트로 빼 두었습니다.
sequenceDiagram
participant 사용자
participant 브라우저
participant 서버
사용자->>브라우저: 페이지 열기
브라우저->>서버: 첫 청크 요청
서버-->>브라우저: 첫 청크
Note over 브라우저: 첫 화면이 뜬다
사용자->>브라우저: 차트 버튼 누르기
브라우저->>서버: 차트 청크 요청
서버-->>브라우저: 차트 청크
Note over 브라우저: 차트를 그린다
차트 버튼을 한 번도 안 누른 사용자는 차트 청크를 끝내 받지 않습니다. 첫 청크가 가벼워진 만큼 첫 화면이 빨리 뜹니다. 버튼을 누른 사용자만 그 순간에 청크 하나를 더 받습니다.
나누는 세 갈래
나눌 지점을 고르는 기준은 흔히 셋입니다. 무엇을 한 청크로 묶느냐가 다릅니다.
| 갈래 | 한 청크에 묶는 코드 | 받는 때 |
|---|---|---|
| 화면 단위 | 주소 하나에 해당하는 화면 | 사용자가 그 주소로 옮겨 갈 때 |
| 기능 단위 | 코드가 큰 기능 하나. 차트·편집기·지도 | 그 기능을 처음 쓸 때 |
| 라이브러리 분리 | 바깥에서 가져온 라이브러리 | 첫 화면과 함께. 대신 앱 코드와 따로 저장된다 |
화면 단위는 웹 앱이 화면을 바꾸는 방식과 맞물립니다. 싱글 페이지 애플리케이션은 HTML(HyperText Markup Language) 페이지 하나를 받은 뒤 화면 전환을 전부 자바스크립트로 하는 웹 앱입니다. 주소가 바뀌어도 페이지를 새로 받지 않습니다. 브라우저 안의 라우터가 주소에 맞는 화면을 골라 그립니다.
가장 흔한 출발점은 화면 단위입니다. 라우터가 화면을 고르는 순간에 그 화면의 청크를 받으면 되기 때문입니다.
기능 단위는 한 화면 안에서 더 덜어 냅니다. 차트나 편집기처럼 코드가 크면서 누르기 전에는 안 쓰는 기능이 대상입니다. 앞 소절의 차트 버튼이 이 갈래입니다.
라이브러리 분리를 이해하려면 파일 이름부터 봐야 합니다. 브라우저는 한 번 받은 파일을 브라우저 캐시에 저장해 둡니다. 다음에 이름이 같은 파일이 필요하면 다시 받지 않고 저장해 둔 것을 씁니다.
번들러는 흔히 파일 내용으로 계산한 해시 값을 파일 이름에 붙입니다. 내용이 한 글자라도 바뀌면 이름도 바뀝니다. 이름이 바뀐 파일은 브라우저가 새로 받습니다.
라이브러리 코드는 앱 코드보다 훨씬 드물게 바뀝니다. 둘을 한 청크에 두면 앱 코드 한 줄을 고칠 때마다 라이브러리까지 새 파일이 됩니다. 따로 두면 배포한 뒤에도 라이브러리 청크는 브라우저 캐시에 저장해 둔 것을 계속 씁니다.
지연 로딩과 나눠 맡는 일
코드 분할과 함께 자주 나오는 말이 지연 로딩입니다. 지연 로딩은 무언가를 쓰는 순간이 올 때까지 받기를 미루는 방식입니다. 두 말은 같은 일의 앞뒤를 나눠 맡습니다.
코드 분할은 빌드할 때 코드를 조각으로 나누는 일입니다. 지연 로딩은 앱이 돌 때 그 조각을 언제 받을지 정하는 일입니다. 나눠 두지 않으면 미룰 조각이 없습니다. 나눠만 두고 첫 화면에서 다 받으면 나눈 보람이 없습니다.
코어 웹 바이탈과 이어지는 지점
코어 웹 바이탈은 사용자가 느끼는 페이지 속도를 재는 지표 묶음입니다. 그 가운데 두 지표가 첫 화면 자바스크립트의 크기에 흔들립니다.
최대 콘텐츠풀 페인트(Largest Contentful Paint, LCP)는 첫 화면에서 차지하는 넓이가 가장 큰 그림이나 글 덩어리가 그려지기까지의 시간입니다. 화면을 자바스크립트로 그리는 앱이면 스크립트를 다 받아 실행한 뒤에야 그 덩어리가 생깁니다. 첫 청크가 작아지면 그 덩어리가 그만큼 일찍 그려질 수 있습니다.
다음 페인트까지의 상호작용(Interaction to Next Paint, INP)은 클릭한 뒤 화면이 다음으로 그려지기까지의 시간입니다. 이 시간이 길어지는 까닭은 브라우저가 일을 맡기는 방식에 있습니다.
브라우저는 자바스크립트 실행과 화면 그리기를 메인 스레드 하나에 맡깁니다. 첫 청크가 크면 그것을 실행하는 동안에는 클릭이 들어와도 차례를 기다립니다. 처음에 실행할 코드가 줄면 이 기다림을 줄이는 데 도움이 됩니다.
나눠서 생기는 대가
나누면 첫 화면은 가벼워집니다. 대신 기다림이 없어지는 것이 아니라 뒤로 옮겨 갑니다. 그 기다림이 어디서 드러나는지 셋을 봅니다.
첫째는 늦게 받는 청크의 기다림입니다. 버튼을 누른 순간 청크를 받기 시작하면 받는 동안 화면이 멈춘 듯 보입니다. 그래서 흔히 받는 동안 로딩 표시를 띄웁니다. 곧 쓸 것 같은 청크를 한가할 때 미리 받아 두기도 합니다. 이것을 프리페치라고 부릅니다.
둘째는 너무 잘게 나눈 경우입니다. 청크가 많으면 요청 수가 다시 늘어납니다. 요청마다 서버까지 오가는 왕복이 붙습니다. 잘게 나눌수록 모듈을 하나씩 따로 받던 처음 문제 쪽으로 다가갑니다.
셋째는 청크가 다른 청크를 부르는 경우입니다. 첫 청크를 받아 실행해야 둘째 청크가 필요하다는 것을 압니다. 둘째를 받아 실행해야 셋째를 요청합니다. 이렇게 앞 요청이 끝나야 다음 요청이 출발하는 모양을 요청 워터폴이라고 부릅니다.
나눌 때와 나누지 않을 때
고르는 기준은 둘입니다. 하나는 그 코드를 첫 화면에서 쓰는가입니다. 다른 하나는 그 코드를 쓰는 사용자가 얼마나 되는가입니다.
| 상황 | 고를 방식 | 까닭 |
|---|---|---|
| 첫 화면이 바로 쓰는 코드 | 첫 청크에 둔다 | 나누면 첫 화면이 청크 하나를 더 기다린다 |
| 일부 사용자만 가는 화면 | 화면 단위로 나눈다 | 안 가는 사용자는 끝내 안 받는다 |
| 크고 가끔 쓰는 기능 | 기능 단위로 나눈다 | 첫 청크에서 가장 크게 덜어 낸다 |
| 자주 안 바뀌는 라이브러리 | 따로 뺀다 | 배포해도 저장된 파일을 다시 쓴다 |
| 화면이 몇 개 없는 작은 앱 | 나누지 않는다 | 덜어 낼 코드가 적고 요청만 는다 |
마지막 줄처럼 번들이 작으면 나눌 이유가 약합니다. 코드 분할은 앱이 커져서 첫 화면이 눈에 띄게 늦어질 때 꺼내는 도구입니다.
같은 이름의 다른 쓰임
통신에서 쓰는 코드 분할 다중화는 이 말과 관계가 없습니다. 여러 사용자의 신호를 서로 다른 부호로 구별해 한 주파수에 같이 싣는 방식입니다. 영어 이름이 code division 이라서 한국어로 옮기면 「코드 분할」이 겹칩니다. 웹 앱의 코드 분할은 영어로 code splitting 입니다.
관련 항목
코드 분할을 해 주는 번들러
번들러 · webpack · Rollup · Vite · esbuild · Parcel
코드 분할을 이루는 구성 요소
청크 · 번들 · 모듈 · ES 모듈 · 정적 임포트 · 동적 임포트 · 프로미스
코드 분할과 함께 번들을 줄이는 기법
트리 셰이킹 · 미니파이 · 데드 코드 제거 · 스코프 호이스팅 · 트랜스파일
나뉜 청크를 받는 때를 정하는 기법
지연 로딩 · 프리페치 · 프리로드 · React · Suspense
청크를 오래 다시 쓰게 하는 캐시 장치
브라우저 캐시 · 콘텐츠 해시 · 해시 · 캐시 무효화 · CDN
코드 분할이 줄이려는 지연을 재는 지표
코어 웹 바이탈 · 최대 콘텐츠풀 페인트 · 다음 페인트까지의 상호작용 · 첫 콘텐츠풀 페인트 · 상호작용 가능 시간 · 총 차단 시간
코드 분할에서 자주 나는 문제
요청 워터폴 · 청크 로드 오류 · 번들 크기 · 레이아웃 이동
코드 분할이 쓰이는 웹 앱 구조
싱글 페이지 애플리케이션 · 클라이언트 사이드 렌더링 · 서버 사이드 렌더링 · 라우팅 · 하이드레이션
코드 분할이 속하는 상위 분류
프론트엔드 · 웹 성능 · 성능 · JavaScript
다른 이름: Code Splitting · 코드 스플리팅 · 번들 분할