수년간 깨달은 진실이 있습니다: 코드의 모양은 코드가 하는 일만큼이나 중요합니다. 잘 포맷된 코드는 리뷰가 빠르고, 버그가 적으며, 유지보수가 훨씬 쉽습니다. 반대로, 적절히 축소된 코드는 더 빨리 로드되고 사용자의 대역폭을 절약합니다.

이 동전의 양면에 대해 이야기해 봅시다.

포맷팅이 생각보다 중요한 이유

탭 vs 스페이스, 세미콜론 유무, 중괄호 위치에 대해 코드 리뷰에서 몇 시간씩 논쟁하는 팀을 봐왔습니다. 그건 기능을 배포하는 데 쓰이지 않는 시간입니다. 일관된 포맷팅은 이런 논쟁을 완전히 없앱니다.

하지만 팀 화합만의 문제가 아닙니다. 균일하게 포맷된 코드는 버그를 발견하기 더 쉽게 만듭니다. 이것을 생각해 보세요:

javascript

sendNotification() 호출은 관리자뿐 아니라 매번 실행됩니다 — 들여쓰기가 오해를 줍니다. 대부분의 포맷터가 강제하는 필수 중괄호를 사용하면 이 버그는 명백합니다:

javascript

포맷팅 모범 사례

스타일을 선택하고 자동화하세요. 사람이 일관되게 포맷할 것이라고 믿지 마세요. Prettier를 사용하세요 — 고집이 센 도구이지만, 그게 장점입니다. 저장 시 실행되도록 설정하면 포맷팅에 대해 다시는 생각할 필요가 없습니다.

들여쓰기: 2 스페이스가 JavaScript/TypeScript 규약입니다. Google 스타일 가이드, Airbnb 스타일 가이드, Standard 스타일 모두 이에 동의합니다.

세미콜론: 사용하세요. 네, JavaScript에는 ASI(Automatic Semicolon Insertion)가 있지만, 잘 알려진 함정들이 있습니다. 그냥 세미콜론을 넣으세요.

줄 길이: 80-100자 이내로 유지하세요. 긴 줄은 가로 스크롤을 유발하고 diff를 읽기 어렵게 만듭니다.

축소가 실제로 어떻게 작동하는가

축소는 포맷팅의 반대입니다 — 프로덕션용으로 코드를 최대한 작게 만듭니다. Terser 같은 축소 도구가 하는 일은 다음과 같습니다:

  • 공백과 주석 제거 — 모든 스페이스, 탭, 줄바꿈, // TODO: fix later 주석? 사라집니다.
  • 변수명 단축userAccountBalancea로 됩니다. calculateMonthlyPaymentb로 됩니다. 로컬 변수만 해당됩니다 — 외부 코드가 참조할 수 있는 것은 이름을 바꾸지 않습니다.
  • 죽은 코드 제거 — 한 번도 호출되지 않는 함수는 삭제됩니다.
  • 상수 사전 계산const TAX_RATE = 0.2; total * (1 + TAX_RATE)total*1.2로 됩니다.

결과는? 일반적으로 50-70% 파일 크기 감소입니다. 전후 비교를 봅시다:

전 (읽기 좋은, 147바이트):

javascript

후 (축소됨, 62바이트):

javascript

같은 기능, 58% 작아졌습니다.

소스맵을 잊지 마세요

축소된 코드는 읽을 수 없어 프로덕션 에러 디버깅이 고통스럽습니다. 소스맵은 축소된 코드를 원본 소스에 매핑하여 이 문제를 해결합니다. 모든 최신 빌드 도구가 소스맵을 생성합니다 — 에러 추적 서비스(Sentry, Bugsnag 등)에 업로드하고 있는지 확인하세요.

실용적인 워크플로우

실용적인 워크플로우

개발 시: 포맷된 읽기 좋은 코드를 작성하세요. 자동 포맷팅과 린팅을 위해 Prettier + ESLint를 사용하세요. 프로덕션에서는: 빌드 도구(webpack, Vite, esbuild)를 통해 모든 것을 축소하세요.

60초 만에 Prettier + ESLint 설정하기

아직 포맷팅을 자동화하지 않았다면, 가장 빠른 설정 방법입니다:

javascript

그런 다음 package.json에 포맷 스크립트를 추가하세요: "format": "prettier --write src/**/*.{js,ts,jsx,tsx}". 한 번 실행하면 전체 코드베이스가 포맷되고, 에디터를 저장 시 포맷하도록 설정하세요. 처음 diff는 엄청날 수 있지만, 그 후로는 포맷팅 논쟁이 영원히 끝납니다.

Tree Shaking vs 축소

사람들이 이 둘을 자주 혼동하지만, 함께 작동하는 서로 다른 최적화 기법입니다:

  • 축소는 공백 제거, 이름 단축, 표현식 사전 계산을 통해 개별 파일을 작게 만듭니다. 사용하지 않는 export는 제거하지 않습니다.
  • Tree shaking은 모듈 간 사용하지 않는 코드를 제거합니다. lodash-es에서 { debounce }만 import하면, tree shaking이 번들에서 나머지를 모두 제거합니다.

둘 다 프로덕션 빌드에 필수적입니다. Vite와 esbuild 같은 최신 번들러는 둘 다 자동으로 수행합니다 — tree shaking이 의존성 그래프를 분석할 수 있도록 CommonJS(require) 대신 ES 모듈 import(import)를 사용하고 있는지 확인하세요.

일반적인 축소 함정

1. 프로덕션에서 function.name에 의존하기. 축소 도구는 함수 이름을 변경하므로, myFunction.name은 프로덕션에서 "a" 같은 쓸모없는 값을 반환합니다. 안정적인 함수 이름이 필요한 경우(로깅, 에러 추적, 리플렉션용), 명시적인 문자열 식별자를 대신 사용하세요.

2. eval()을 사용하는 코드 축소. eval()은 이름으로 어떤 변수든 참조할 수 있으므로, 축소 도구는 해당 스코프에서 아무것도 안전하게 이름을 바꿀 수 없습니다. 대부분의 축소 도구는 eval()을 포함하는 함수에서 이름 변경을 건너뛰지만, 이는 최적화를 크게 제한합니다. 가능하면 eval()을 완전히 피하세요.

3. 축소된 빌드를 테스트하지 않기. 일부 버그는 축소 후에만 나타납니다 — 특히 프로퍼티 이름 맹글링 관련. 개발 빌드뿐 아니라 항상 프로덕션 빌드에 대해 테스트 스위트를 실행하세요.

번들 크기 측정하기

축소는 시작점을 알아야 의미가 있습니다. 번들 크기를 확인하는 빠른 방법들입니다:

javascript

이 도구들은 어떤 의존성이 번들을 차지하고 있는지 정확히 보여주는 시각적 트리맵을 생성합니다. 놀랄 수도 있습니다 — 한번은 moment.js 로케일이 최종 번들의 500KB를 차지하는 프로젝트를 발견한 적이 있습니다. dayjs로 전환하니 즉시 490KB가 절약됐습니다.

Prettier vs ESLint: 같은 것이 아닙니다

자, 이건 꼭 해야 할 말인데요. 이 혼란을 *여기저기서* 보거든요: Prettier와 ESLint는 같은 도구가 아닙니다. 같은 일을 하지도 않습니다. 그런데도 개발자들이 이 둘을 교환 가능한 것으로 취급하는 걸 계속 봅니다. 설명해 드리겠습니다.

Prettier는 코드 포맷터입니다. 코드가 어떻게 *보이는지*를 신경 씁니다 — 공백, 줄바꿈, 세미콜론, 후행 쉼표, 따옴표 스타일, 들여쓰기. 그게 전부입니다. 코드가 실제로 작동하는지는 알지도 못하고 관심도 없습니다. 전체 데이터베이스를 삭제하는 함수가 있어도 Prettier는 그냥 깔끔하게 들여쓰기되어 있는지만 확인합니다.

ESLint는 반면 린터입니다. 코드 *품질*을 신경 씁니다 — 사용하지 않는 변수, 도달할 수 없는 코드, 잠재적 버그, 누락된 에러 핸들링, 접근성 문제. 토요일 새벽 2시에 호출당할 때 여러분을 괴롭힐 것들을 잡아냅니다.

하지만 여기서 문제가 됩니다 — ESLint에도 일부 포맷팅 규칙이 *내장*되어 있고, 그게 혼란의 시작입니다. 두 도구를 적절히 설정하지 않고 실행하면 서로 싸우게 됩니다. Prettier가 한 방식으로 코드를 포맷하면 ESLint가 다른 방식이어야 한다고 불평하고, ESLint를 위해 고치면 Prettier가 다시 포맷하고... 끝없는 좌절의 루프입니다.

해결책은 아주 간단합니다: eslint-config-prettier를 사용하세요. Prettier와 충돌하는 모든 ESLint 규칙을 끄므로, ESLint는 코드 품질에 집중하고 Prettier가 포맷팅을 담당합니다. 더 이상 싸움은 없습니다.

javascript

"prettier"가 extends 배열의 마지막 항목인 것을 주목하세요. 이것이 중요합니다 — 위에 나열된 플러그인의 포맷팅 규칙을 오버라이드해야 하기 때문입니다. ESLint/Prettier 충돌을 디버깅하는 데 몇 시간을 보내다가 순서가 잘못되어 있었다는 걸 발견한 팀을 본 적이 있습니다. 이건 저를 믿으세요.

제가 추천하는 설정: Prettier가 모든 외관을 담당하게 하고, 실제로 버그를 잡는 ESLint 규칙을 설정하세요. Prettier가 이미 처리하는 세미콜론에 대해 ESLint가 불평하게 하는 건 낭비입니다.

탭 vs 스페이스 대논쟁 (해결됨)

좋아요, 프로그래밍의 성전에 대해 이야기합시다. 탭인가 스페이스인가? 이 논쟁 때문에 우정이 끝나는 걸 봤습니다. *며칠*이나 계속되는 Slack 스레드를 봤습니다. 시니어 개발자가 탭을 옹호하는 2,000단어짜리 Confluence 페이지를 쓰는 것을 직접 목격했습니다. 장대했고 완전히 불필요했습니다.

실제 데이터를 봅시다. Stack Overflow 개발자 설문조사는 일관되게 스페이스가 더 인기 있다는 것을 보여줍니다 — 개발자의 약 60-65%가 스페이스를 선호합니다. GitHub의 공개 저장소 분석도 비슷한 이야기를 합니다.

하지만 여기서 흥미로운 점은: 언어마다 크게 다르다는 것입니다. Go는 탭을 사용합니다 — 이건 논쟁의 여지도 없이, gofmt가 탭을 강제하고 아무도 gofmt와 논쟁하지 않습니다. Python은 4 스페이스를 사용합니다 — PEP 8이 그렇게 말하고, PEP 8과도 논쟁하지 않습니다. JavaScript와 TypeScript? 커뮤니티는 대체로 2 스페이스로 결정했고, 거의 모든 주요 스타일 가이드가 동의합니다.

탭의 접근성 논거는 실제로 설득력이 있습니다 — 탭은 각 개발자가 선호하는 시각적 너비를 설정할 수 있게 해주며, 이는 시각 장애가 있는 개발자에게 중요합니다. 이것은 탭을 선호할 현실적이고 정당한 이유입니다.

하지만 아시나요? 여기에 *진짜* 정답이 있습니다. 진심으로 말하는 겁니다: 포맷터가 강제하는 것을 사용하고 논쟁을 멈추세요. 프로젝트가 tabWidth: 2useTabs: false로 Prettier를 사용한다면, 2 스페이스를 사용합니다. 마침표. 포맷터가 결정을 내리고, 여러분은 그것을 받아들이고, 정말 중요한 것에 에너지를 쏟으세요 — 누군가 검색 상자에 이모지를 입력했을 때 앱이 크래시하는지 같은 것에요.

포맷팅 논쟁에 인생을 낭비하기엔 너무 짧습니다. 로봇이 결정하게 하세요.

현대적 축소: esbuild, SWC, 그리고 속도 혁명

수년간 Terser는 JavaScript 축소의 왕이었습니다. UglifyJS를 대체했고, 실전에서 검증되었으며, 훌륭하게 작동했습니다. 유일한 문제? 느리다는 겁니다. 대규모 코드베이스에서는 *정말로* 느립니다. "커피 한 잔 마시러 가기" 정도의 느림이 아니라 — "CI 파이프라인을 바라보며 경력 선택을 재고하기" 수준의 느림입니다.

그때 esbuild가 나타나 모든 것을 바꿨습니다. Go로 작성된 esbuild는 Terser보다 10-100배 빠릅니다. 과장이 아닙니다 — 벤치마크가 거의 코미디 수준입니다. Terser가 축소하는 데 30초 걸리는 프로젝트? esbuild는 300밀리초에 합니다. 처음 봤을 때 너무 빨리 끝나서 뭔가 고장났다고 진심으로 생각했습니다.

SWC는 Rust로 작성된 또 다른 경쟁자입니다. 순수 축소에서는 esbuild만큼 빠르지 않지만, 더 완전한 도구체인입니다 — 트랜스파일, 번들링, 축소를 모두 하나로 처리합니다. Next.js를 사용한다면, 이미 내부적으로 SWC를 사용하고 있습니다.

중간 규모 프로젝트(~500개 JS 파일)에서의 대략적인 비교입니다:

ToolLanguageMinification TimeNotes
TerserJavaScript~25sBattle-tested, most compatible
esbuildGo~0.3sBlazing fast, some edge cases
SWCRust~0.8sFull toolchain, great ecosystem

속도 차이가 정말 중요할까요? 솔직히, 5초 빌드의 작은 프로젝트에서는 아마 아닙니다. 하지만 대규모 모노레포에서 CI/CD를 실행하고 빌드 파이프라인이 하루에 50번 돌아간다면? 그 분들은 빠르게 쌓입니다. Terser에서 esbuild로 전환하는 것만으로 CI 시간을 10분 이상 줄인 팀을 본 적이 있습니다.

좋은 소식은 Vite를 사용한다면, 개발 빌드에는 이미 esbuild를, 프로덕션에는 Rollup(Terser 또는 esbuild와 함께)을 사용하고 있다는 것입니다. Next.js는 SWC를 사용합니다. Angular도 esbuild를 실험하고 있습니다. 이 생태계는 빠르게 움직이고 있습니다.

한 가지 주의할 점: esbuild와 SWC가 항상 Terser와 바이트 단위로 동일한 출력을 생성하는 것은 아닙니다. 드문 경우에 Terser의 더 공격적인 최적화가 약간 더 작은 번들을 생성합니다. 하지만 1-2% 정도의 차이입니다 — 대부분의 경우 100배의 속도 향상은 충분히 가치가 있습니다.

CSS와 HTML 축소도

JavaScript에 대해 이야기해 왔지만, CSS와 HTML 축소도 놓치지 마세요. 진짜로, CSS 번들이 JavaScript보다 *큰* 프로젝트를 본 적이 있습니다. 특히 Tailwind 같은 유틸리티 우선 프레임워크를 사용할 때(PurgeCSS가 작업하기 전).

CSS에는 cssnano가 대표 도구입니다. 공백 제거 이상의 작업을 합니다 — 중복 규칙 병합, 색상을 더 짧은 형식으로 변환(#ff0000#f00 또는 red로), 불필요한 프로퍼티 제거, calc() 표현식 최적화도 합니다. 일반적인 스타일시트에서 30-50%의 절약을 기대할 수 있습니다.

css

HTML의 경우, html-minifier-terser가 태그 사이의 공백을 제거하고, 선택적 닫는 태그를 제거하고, 인라인 CSS와 JS를 축소하고, HTML 주석을 제거하고, 불리언 속성을 축약합니다. 놀라울 정도로 효과적입니다 — HTML이 많은 페이지에서 20-30% 감소를 본 적이 있습니다.

그리고 여기서 좋은 소식입니다: Angular, React, Vue 또는 빌드 단계가 있는 거의 모든 최신 프레임워크를 사용한다면, 이것은 이미 자동으로 일어나고 있습니다. 빌드 도구가 프로덕션 빌드의 일부로 CSS와 HTML 축소를 처리합니다. 하지만 내부에서 무슨 일이 일어나는지 아는 것은 정말 유용합니다 — 특히 프로덕션 HTML이 소스와 일치하지 않는 이유를 디버깅해야 할 때, 또는 마지막 중요 렌더 경로를 최적화하려 할 때.

CI/CD 파이프라인에서의 코드 포맷팅

여기서 "저장 시 포맷"의 문제점을 말씀드리겠습니다 — *팀의 모든 사람*이 올바르게 설정해야만 작동합니다. 그리고 저도 이것 때문에 당한 적이 있습니다. 이런 시나리오를 아시죠: 누군가 팀에 합류하고, 레포를 클론하고, 변경을 시작하고, 5줄의 실제 코드에 400개의 포맷팅 변경이 섞인 PR을 제출합니다. 그 PR을 리뷰하는 건 악몽입니다.

해결책은? CI에서 포맷팅을 강제하세요. 포맷되지 않은 코드를 병합하는 것을 불가능하게 만드세요. 방법은 이렇습니다:

1단계: CI 체크 추가. CI 파이프라인에 prettier --check .를 추가하세요. 어떤 파일이든 Prettier의 포맷팅과 일치하지 않으면 코드 1로 종료됩니다. 논쟁도 토론도 없습니다 — CI가 법입니다.

javascript

2단계: pre-commit 훅 추가. CI에서 포맷팅 문제를 잡는 것은 좋지만, 코드가 *커밋되기 전에* 잡는 것이 더 좋습니다. 여기서 Husky와 lint-staged가 등장합니다.

javascript
javascript
javascript

lint-staged의 아름다운 점은 전체 코드베이스가 아니라 실제로 변경한 파일에서만 실행된다는 것입니다. 따라서 pre-commit 훅이 30초가 아니라 1-2초 걸립니다. 1초짜리 훅을 비활성화할 사람은 없습니다.

"포맷팅이 PR 마찰의 끊임없는 원인"에서 "포맷팅에 대해 말 그대로 한 번도 생각하지 않게 됐다"로 일주일 만에 바뀐 팀을 봤습니다. 15분 설정이 천 배의 가치를 돌려주는 그런 종류의 것입니다.

실제 번들 크기 사례 연구

실제 숫자로 이야기합시다. "번들을 작게 유지하라"는 추상적인 조언은 맥락 없이는 별로 도움이 되지 않습니다. 이런 정확한 시나리오를 프로덕션 프로젝트에서 봤고, 차이는 정말 충격적입니다.

사례 1: lodash vs lodash-es. 이건 고전입니다. import _ from 'lodash'를 하고 _.debounce만 사용하면, 전체 라이브러리를 임포트하고 있는 겁니다 — 축소 + gzip 71.5 KB. import { debounce } from 'lodash-es'로 전환하고 tree shaking을 하면, 그 함수만으로 약 1.5 KB입니다. 98% 감소입니다. Bundlephobia에서 직접 확인해 보세요.

사례 2: moment.js vs dayjs. Moment.js는 수년간 날짜 처리의 금본위였지만, 축소 + gzip 72.1 KB입니다 — 그리고 기본적으로 모든 로케일 데이터를 포함합니다. Day.js는 거의 동일한 API를 가지면서 2.9 KB입니다. 오타가 아닙니다. 기본적으로 같은 기능에 72 KB vs 3 KB입니다. Moment.js 팀 자체가 이제 대안을 추천합니다.

사례 3: 아이콘 라이브러리. 이건 항상 사람들을 당하게 합니다. import { FaHome } from 'react-icons/fa'를 하면 괜찮습니다 — tree-shakeable이니까요. 하지만 일부 아이콘 라이브러리는 tree shaking을 위해 설정되어 있지 않아서, 5개만 필요한데 수백 개의 SVG 아이콘을 임포트하게 됩니다. 아이콘 임포트가 번들에 200+ KB를 추가한 걸 본 적이 있습니다. 아이콘 라이브러리가 tree shaking을 지원하는지 항상 확인하거나, 아이콘을 개별적으로 임포트하세요.

사례 4: 날짜 포맷팅 라이브러리. 특정 타임존에서 날짜 하나를 포맷해야 했나요? 한번은 moment-timezone(모든 타임존 데이터를 포함하는 전체 버전)을 임포트한 프로젝트를 봤습니다 — 축소 + gzip 97 KB — 날짜 하나를 포맷하기 위해서만요. 네이티브 Intl.DateTimeFormat API는 번들 비용 없이 대부분의 타임존 포맷팅을 네이티브로 처리합니다.

여기서의 교훈은 "의존성을 절대 사용하지 마라"가 아닙니다 — 그건 어리석을 겁니다. 교훈은: 무엇을 임포트하고 있고 얼마나 비용이 드는지 알아야 합니다. 그 반짝이는 새 라이브러리를 추가하기 전에 npx source-map-explorer build/static/js/*.js를 실행하거나 Bundlephobia를 확인하세요. 느린 모바일 연결의 사용자들이 감사할 것입니다.

직접 해보세요

지저분한 코드를 빨리 정리해야 하나요? JavaScript Formatter에 붙여넣으면 즉시 읽기 좋은 출력을 얻을 수 있습니다. 프로덕션용으로 줄일 준비가 됐나요? JavaScript Minifier가 최소한으로 줄여줍니다. 그리고 코드가 유효한지 확신이 없다면, 먼저 JavaScript Validator로 확인해 보세요.