
2024년에 한 핀테크 회사의 프론트엔드 플랫폼 팀 채용 공고를 보고 공부했던 노트가 있어요. 서비스를 만드는 개발자가 아니라, 그 개발자들이 쓰는 개발환경과 프레임워크를 만드는 자리였죠. 2년 만에 다시 열어보니 채용은 당연히 끝났고, 거기 적힌 기술도 꽤 많이 바뀌어 있었어요.
그런데 공고에 적혀 있던 사전 과제 출제 범위 여섯 줄은 지금 봐도 좋은 체크리스트였어요. Git, 모듈 시스템, package.json 의존성, 번들러, CI/CD, 오류 감지. 프론트엔드 개발환경이 뒤에서 어떻게 돌아가는지를 이 여섯 가지가 거의 다 덮거든요. 그래서 이 여섯 줄을 뼈대로 삼고, 내용은 2026년 기준으로 다시 채워봤어요.
- Git을 구성하는 요소와 자주 쓰는 명령어의 동작
- Node.js에서 CommonJS와 ECMAScript Modules의 사용과 상호 호환성
- package.json에서 dependencies, devDependencies, peerDependencies의 동작
- 트랜스파일러와 번들러의 동작과 활용
- 프론트엔드 개발환경에서 CI/CD 구성
- 프론트엔드 서비스와 SSR 서버를 안전하게 운영하기 위한 오류 감지와 디버깅
면접용 프론트엔드 개념 전반은 프론트엔드 지원 전에 정리해두면 좋은 개념들에 따로 있어요. 이 글은 그보다 한 층 아래, 개발환경 쪽이에요.
1. Git: 큰 저장소를 통째로 받지 않기#
Git은 파일 내용(blob), 디렉토리 구조(tree), 스냅샷(commit)이라는 객체를 쌓아두는 저장소예요. 저장소가 작을 때는 이 구조를 몰라도 되지만, 수백 개 서비스를 한 모노레포에 넣으면 이야기가 달라져요. 제가 본 강연에서는 저장소가 40GB까지 커져서 그냥 클론하는 것 자체가 불가능해졌다고 했어요.
처음 떠올리는 건 shallow clone(--depth)이에요. 히스토리를 잘라서 받으니 빠르지만, 변경 이력을 추적하기 어려워서 바뀐 서비스만 골라 배포하는 데는 맞지 않아요. 그래서 쓰는 게 Git 2.22부터 들어온 partial clone이에요. 히스토리는 다 받되 파일 내용(blob)은 필요할 때 가져와요.
# 커밋·트리는 다 받고, 파일 내용(blob)은 필요할 때 가져와요
git clone --filter=blob:none --sparse <repo>
# 작업 트리에 올릴 디렉토리만 고르면 그 안의 blob만 내려받아요
git sparse-checkout set apps/web packages/ui2026년 기준으로는 partial clone과 sparse-checkout을 같이 쓰는 게 큰 모노레포의 표준 조합이에요. sparse-checkout은 작업 트리에 어떤 경로만 올릴지 정하는 기능이고, 디렉토리 단위로만 지정하는 cone 모드가 Git 2.37부터 set의 기본값이에요. 둘의 역할이 달라요. partial clone은 다운로드를 미루고, sparse-checkout은 디스크에 올리는 범위를 줄여요.
CI 러너에서는 한 발 더 나가요. 러너가 매번 체크아웃하느라 타임아웃이 나면, 저장소를 미리 복제해둔 컨테이너 이미지를 매일 구워두고 그 위에서 변경분만 받는 방법이 있어요. 제가 본 사례에서는 이 방법으로 36분 걸리던 체크아웃이 22초로 줄었어요. GitHub Actions를 쓴다면 actions/checkout의 filter 옵션으로 partial clone을 바로 쓸 수도 있고요.
브랜치마다 디렉토리를 따로 두고 싶다면
git worktree도 같이 알아두세요. 한 저장소에서 여러 브랜치를 동시에 체크아웃해두는 기능이라, 에이전트 여러 개가 같은 저장소에서 동시에 작업할 때 특히 유용해요.
2. CommonJS와 ESM: 이제 require(esm)이 돼요#
2024년 노트에서 유일하게 비어 있던 항목이에요. 그 사이에 가장 많이 바뀐 항목이기도 하고요.
CommonJS(CJS)는 require()와 module.exports를 쓰는 Node.js의 원래 모듈 시스템이에요. 동기적으로 파일을 읽어서 실행해요. ESM(ECMAScript Modules)은 import와 export를 쓰는 표준 모듈 시스템이고, 의존 관계를 정적으로 분석할 수 있어서 Tree shaking 같은 최적화가 가능해요. Node.js는 package.json의 "type": "module" 설정이나 .mjs, .cjs 확장자로 어느 쪽인지 판단해요.
문제는 둘을 섞을 때였어요. ESM에서 CJS를 import하는 건 오래전부터 됐지만, CJS에서 ESM을 require()하는 건 안 됐거든요. 그래서 라이브러리는 CJS와 ESM 두 벌을 빌드해서 exports 필드로 나눠 내보내는 dual package 방식이 흔했어요. 빌드도 두 번, 타입도 두 벌, 버그도 두 배였죠.
{
"name": "my-lib",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}이게 Node 22.12(2024년 12월)부터 달라졌어요. require()로 ESM 파일을 플래그 없이 불러올 수 있게 됐고, Node 20.19에도 백포트됐어요. 이제 ESM 한 벌만 배포해도 CJS 사용자가 require()로 쓸 수 있어요. 단 하나 제한은 top-level await를 쓰는 모듈이에요. 동기적인 require()로는 기다릴 수 없어서 ERR_REQUIRE_ASYNC_MODULE 에러가 나요.
실무에서 챙길 건 두 가지예요. 새 라이브러리는 ESM 단일 빌드를 기본으로 두고, 꼭 지원해야 할 옛 Node 버전이 있을 때만 dual package를 유지하는 거예요. 그리고 exports 필드의 조건 순서(types, import, require, default)를 정리해두면 번들러마다 다른 파일을 집어가는 사고를 막을 수 있어요.
3. package.json: 의존성 세 가지와 패키지 매니저#
세 필드의 차이부터 정리할게요.
| 필드 | 설치 시점 | 쓰는 경우 |
|---|---|---|
dependencies |
항상 설치돼요 | 런타임에 필요한 패키지 |
devDependencies |
개발 환경에서만 설치돼요. production 설치나 라이브러리 사용자 쪽에서는 빠져요 | 빌드 도구, 테스트, 린터 |
peerDependencies |
직접 설치하지 않고, 사용하는 쪽이 설치한 걸 같이 써요 | React처럼 하나만 존재해야 하는 패키지 |
peerDependencies가 가장 헷갈려요. 핵심은 싱글톤이에요. UI 라이브러리가 React를 dependencies에 넣으면 앱의 React와 라이브러리의 React가 두 개 설치될 수 있고, 그러면 Hook이 깨져요. 그래서 "너희가 설치한 React를 내가 같이 쓸게"라고 선언하는 게 peer예요. npm 7부터는 peer도 자동으로 설치해주지만, 버전이 안 맞으면 경고가 아니라 잠재적 런타임 에러라고 생각해야 해요.
패키지 매니저가 이 필드를 읽고 하는 일은 세 단계예요. "react": "^18.2.0" 같은 범위를 정확한 버전으로 고정하고 그 의존성의 의존성까지 고정하는 Resolution(결과가 lock 파일), 고정된 버전을 레지스트리에서 받는 Fetch, 받은 파일을 소스 코드에서 쓸 수 있게 연결하는 Link예요. 이 중 Link 방식이 패키지 매니저마다 달라요.
| 방식 | 동작 | 2026년 현황 |
|---|---|---|
| npm | 의존성을 node_modules에 평평하게 끌어올려(hoisting) 설치해요 |
가장 기본이지만 선언하지 않은 패키지를 쓸 수 있는 유령 의존성 문제가 있어요 |
| pnpm | 전역 저장소에 한 번만 받고 hard link와 symlink로 연결해요. 선언한 패키지만 보여요 | 모노레포 기본값처럼 자리 잡았어요. 9.5부터 catalog:로 워크스페이스 전체 버전을 한 곳에서 관리해요 |
| Yarn PnP | node_modules 없이 의존성 맵을 JS 파일로 두고 require가 그걸 참조해요 |
Yarn 4(Yarn Modern)로 이어지고 있어요. 엄격하지만 도구 호환성 비용이 있어요 |
| Bun | 자체 런타임과 통합된 설치기예요 | 설치 속도가 빠르고 쓰는 곳이 늘었지만, 아직 Node 기반 CI와 섞어 쓰는 경우가 많아요 |
유령 의존성(phantom dependency)은 꼭 알아둘 개념이에요. npm이 중복 설치를 줄이려고 하위 의존성을 최상위 node_modules로 끌어올리면, 내가 package.json에 적지 않은 패키지도 import가 돼버려요. 지금은 잘 되다가 의존성 트리가 바뀌는 날 깨지죠. pnpm과 PnP가 엄격한 건 이걸 막으려는 거예요.
한 가지 더, 팀 전체가 같은 패키지 매니저 버전을 쓰게 해주던 Corepack이 Node 25부터 기본 배포에서 빠졌어요. package.json의 packageManager 필드로 버전을 고정하는 관행은 그대로 두되, 설치는 pnpm이나 Yarn이 안내하는 독립 설치 방법을 따르는 게 맞아요.
4. 트랜스파일러와 번들러: Rust로 넘어간 체인#
브라우저는 파일을 하나씩 받아요. 그런데 우리는 파일을 수백 개로 나눠서 개발하죠. 그 사이를 메우는 게 번들러고, 하는 일은 세 단계예요.
- Resolution —
import './utils'같은 모호한 경로를 정확한 파일 경로로 풀어요.exports필드와 조건부 export를 해석하는 것도 여기예요. - Load(Transform) — TypeScript, JSX, 최신 문법을 브라우저가 이해하는 JS로 바꿔요. 트랜스파일러가 하는 일이에요.
- Optimization — 안 쓰는 코드를 제거하고(Tree shaking,
sideEffects필드로 제외 지정), 변수명을 줄이고 패턴을 압축해요(Minification).
이 구조는 2024년이나 지금이나 같아요. 바뀐 건 누가 하느냐예요. Babel과 Terser가 하던 일을 Rust나 Go로 만든 도구가 가져갔어요.
- Vite 8(2026년 3월)은 esbuild와 Rollup 조합을 버리고 Rust 번들러 Rolldown을 기본으로 써요. Rolldown은 2026년 5월에 1.0이 나왔고, 트랜스파일은 같은 팀의 Oxc가 맡아요. 복잡한 프로젝트는 Vite 7에서
rolldown-vite패키지로 먼저 바꿔보고 8로 올리는 걸 권장해요. - Next.js 16은 Turbopack이 개발 서버와 프로덕션 빌드 모두의 기본이에요. 커스텀 webpack 설정이 있으면
--webpack플래그로 돌아갈 수 있어요. - webpack 설정을 그대로 두고 속도만 올리고 싶다면 webpack 호환 API를 가진 Rspack이 있어요.
React Native 쪽은 사정이 달라요. 기본 번들러 Metro는 여전히 Tree shaking이 제한적이에요. Expo가 EXPO_UNSTABLE_TREE_SHAKING 같은 실험 플래그로 프로덕션 빌드에 한해 지원하고, 그 외에는 Rspack 기반의 Re.Pack이 대안으로 쓰여요. 2024년 강연에서 한 팀이 Metro의 긴 빌드와 --reset-cache 문제 때문에 esbuild로 직접 갈아탔다는 이야기를 들었는데, 그때 겪은 문제가 아직 다 풀린 건 아닌 셈이에요.
도구 이름은 계속 바뀌지만 질문은 같아요. 우리 빌드는 세 단계 중 어디서 시간을 쓰고 있나? 이걸 답할 수 있으면 다음 도구가 나와도 갈아탈지 말지 판단할 수 있어요.
5. CI/CD: 바뀐 것만 빌드하고 캐시는 공유하기#
모노레포에서 빌드 수는 (변경된 서비스 수) × (환경 수)로 늘어나요. 개발, 스테이징, 라이브를 따로 빌드하면 서비스 하나 고쳐도 세 번이죠. 제가 본 사례에서는 모든 빌드를 독립된 러너에서 병렬로 돌리도록 파이프라인을 고쳐서 이상적일 때 6분 안에 끝냈어요. 지금도 원칙은 같아요. 파이프라인을 독립된 컴퓨팅 환경으로 쪼개는 것이 먼저예요.
2026년에는 여기에 두 가지가 기본으로 붙어요.
- 영향받는 것만 빌드 — Turborepo는
turbo build --affected, Nx는nx affected로 main 대비 바뀐 패키지와 그 의존 패키지만 골라 실행해요. 한때 모노레포 도구의 대명사였던 Lerna는 지금 Nx 팀이 유지보수하고 있어요. - 원격 캐시 — 같은 입력이면 다른 러너에서 빌드한 결과를 그대로 가져와요. Turborepo는 Vercel 원격 캐시나 자체 호스팅, Nx는 Nx Cloud를 써요. 캐시가 맞으면 빌드 자체를 건너뛰니 체감이 커요.
CI 플랫폼은 GitHub Actions가 사실상 표준이 됐어요. 앞에서 말한 actions/checkout의 partial clone 옵션, 러너 이미지 미리 굽기, 그리고 Docker 레이어 캐싱(공통 레이어는 앞에, 자주 바뀌는 JS는 뒤에)을 조합하면 큰 저장소도 감당할 수 있어요.
배포 산출물 크기도 챙겨야 해요. Next.js의 standalone 출력처럼 런타임에 실제로 필요한 파일만 추적해서 담는 기능을 쓰면, 제가 본 사례에서는 40GB짜리 모노레포에서 200MB짜리 배포본이 나왔어요. pnpm이나 PnP처럼 node_modules 구조가 다른 환경에서는 이 추적이 바로 안 될 수 있으니 미리 확인해보세요.
6. 오류 감지와 성능: 배포하기 전에 알기#
마지막 항목은 "안전하게 운영하기"예요. 성능과 오류, 두 축으로 나눌게요.
성능은 Web Vitals로#
Google이 정한 Core Web Vitals 세 가지가 기준이에요.
- LCP(Largest Contentful Paint) — 가장 큰 콘텐츠가 그려지기까지의 시간. 로딩이 끝난 시점으로 봐요.
- INP(Interaction to Next Paint) — 사용자 입력 후 다음 화면이 그려지기까지의 지연. 2024년 3월 12일에 FID를 대체했어요. 첫 입력만 보던 FID와 달리 페이지에 머무는 동안의 모든 상호작용을 보고, 200ms 이하면 좋음이에요.
- CLS(Cumulative Layout Shift) — 누적 레이아웃 이동.
2024년 노트에는 FID가 적혀 있었는데, 지금 측정 코드에 FID가 남아 있다면 INP로 바꿔야 해요.
LCP를 줄이는 큰 그림은 여전히 SSR이에요. 클라이언트 렌더링은 HTML을 받고, JS 번들을 받고, 실행하고, 그제야 API를 불러서 콘텐츠가 보여요. SSR은 서버가 요청 헤더로 사용자를 식별해서 가까운 API 서버에 바로 요청하고, 콘텐츠가 든 HTML을 한 번의 요청으로 돌려줘요. 제가 본 사례에서는 이걸로 LCP 평균이 0.9초까지 내려갔어요. 지금은 여기에 React Server Components와 스트리밍이 React 19와 주요 프레임워크의 기본으로 들어와서, 어떤 컴포넌트를 서버에서 끝내고 어떤 걸 클라이언트로 보낼지가 설계의 중심이 됐어요.
대신 SSR은 빌드·배포 시간, 모니터링, 서버 비용이 늘어요. 5번의 CI/CD 최적화가 괜히 나온 게 아니에요.
오류는 1분 안에#
프론트엔드는 Sentry 같은 SaaS 말고는 서비스 상태를 진단하고 오류를 즉시 알아채는 체계가 없는 팀이 많아요. 제가 본 팀의 목표는 분명했어요. 새로 발생한 오류는 1분 안에 감지된다. 이걸 위해 필요한 건 도구 하나가 아니라 조합이에요.
- 에러 수집기 + 소스맵 — 압축된 번들에서 난 에러를 원래 코드 위치로 되돌려야 해요. 소스맵 업로드를 배포 파이프라인에 넣어두세요.
- 분산 추적 — 프론트엔드 요청과 백엔드 로그를 같은 trace ID로 묶는 OpenTelemetry 계열 도구가 표준이 됐어요. "어느 API가 느려서 LCP가 밀렸는지"를 한 화면에서 보려면 필요해요.
- 카나리 배포 — 100대 중 1대에만 먼저 반영해서 오류율을 보고 전체로 넓혀요. 오류를 빨리 잡는 것보다 오류가 적게 퍼지게 하는 장치예요.
- 배포 전 성능 비교 — 느려졌다는 걸 사용자가 알려주기 전에 알아야 해요. PR마다 번들 크기와 Web Vitals를 이전 빌드와 비교하는 단계를 두면 돼요.
테스트는 두 가지 규칙만 지켜도 달라져요. 버그를 고칠 때 테스트를 쓰고, 최적화할 때 성능을 측정해요. 웹 E2E는 Playwright가 표준이 됐고, 실제 사용자 동작과 테스트 동작이 다른 부분(모바일에서 입력할 때 같이 나는 클릭 이벤트 같은 것)이 있으면 그걸 테스트 코드가 아니라 라이브러리 레벨에서 맞추는 게 좋아요.
마무리: 여섯 개 질문으로 점검하기#
2년 전 공고는 끝났지만, 그 여섯 줄은 지금 내가 쓰는 개발환경에 던지는 질문으로 바꿀 수 있어요.
- 우리 저장소는 어떻게 클론되고, CI 체크아웃에 몇 초가 걸리나?
- 우리가 배포하는 패키지는 ESM 한 벌인가, 아직 dual package인가?
peerDependencies에 적힌 패키지는 정말 싱글톤이어야 하는 것들인가? 유령 의존성은 없나?- 빌드는 Resolution, Load, Optimization 중 어디서 시간을 쓰나?
- 서비스 하나를 고치면 빌드가 몇 개 돌고, 그중 캐시로 건너뛰는 건 몇 개인가?
- 새 에러가 나면 몇 분 만에 알게 되고, INP는 측정하고 있나?
하나씩 답하다 보면 다음에 고칠 게 저절로 보여요. 도구 이름은 2년 뒤에 또 바뀌어 있겠지만, 질문은 그대로일 거예요.
Until you make peace with who you are, you will never be content with what you have.
— Doris Mortman


