포매터가 건드리면 안 되는 것들
공백을 정리하는 일이 왜 단순한 치환이 아닌지, CSS·JS·HTML·SQL에서 각각 어디가 걸리는지 정리했습니다.
들여쓰기를 맞추는 일은 공백을 다시 배치하는 것뿐입니다. 정규식 몇 줄로 될 것 같습니다.
src.replace(/\s+/g, ' ')
이 한 줄이 무엇을 부수는지 언어별로 봅니다.
문자열 안은 코드가 아니다
원본 const q = "SELECT * FROM t"; // 공백 두 칸
공백을 줄이면 const q = "SELECT * FROM t"; // 공백 두 칸
주석은 그대로인데 문자열 리터럴이 바뀌었습니다. 이 코드가 쿼리를 로그에 찍거나 해시하고 있었다면 결과가 달라집니다.
포매터가 토크나이저를 거쳐야 하는 첫 번째 이유입니다. 문자열·템플릿 리터럴·정규식 리터럴·주석은 서식 대상이 아니라 내용입니다.
CSS — 콜론이 두 가지 뜻이다
선언에서 콜론은 속성과 값을 가릅니다. color: red에서 콜론 뒤 공백은 넣어도 되고 빼도 됩니다.
선택자에서 콜론은 의사 클래스를 엽니다. a:hover에 같은 규칙을 적용하면 a: hover가 되고, 이것은 선택자가 아니라 잘못된 선언이 됩니다. 규칙 전체가 무시됩니다.
공백 자체가 결합자인 자리도 있습니다.
div p 자손 결합자. 공백을 지우면 divp 라는 없는 태그
a + b 형제 결합자. 여기서는 공백을 지워도 a+b 로 유효
calc(100% - 2px) 빼기 앞뒤 공백은 필수. calc(100%-2px) 는 무효
calc의 마이너스는 부호로도 읽힐 수 있어서 규격이 공백을 요구합니다. 최소화 도구가 calc 안의 공백을 지우면 그 값은 계산되지 않습니다.
JavaScript — 줄바꿈이 세미콜론이 된다
function f() {
return
42;
}
f() // undefined
return 다음의 줄바꿈에서 세미콜론이 자동으로 들어갑니다(ASI). return;이 되고 42;는 도달하지 않는 문이 됩니다.
역방향도 있습니다. 세미콜론 없이 쓴 코드에서 줄을 합치면 의미가 바뀝니다.
const a = b
(c || d).e() // b(c || d).e() 로 읽힌다
줄바꿈을 자유롭게 옮길 수 있다는 전제가 JavaScript에서는 성립하지 않습니다. return, throw, break, continue, 후위 ++/--, 화살표 함수의 => 뒤가 그런 자리입니다.
HTML — 내용을 보존해야 하는 요소가 있다
원본 <pre>줄1\n 줄2</pre><p>문단 공백</p><textarea> 그대로 </textarea>
공백을 줄이면 <pre>줄1 줄2</pre><p>문단 공백</p><textarea> 그대로 </textarea>
<p> 안의 공백 축약은 브라우저가 어차피 하는 일이라 결과가 같습니다. <pre>와 <textarea>는 다릅니다 — 줄바꿈과 공백이 그대로 보이는 요소입니다. <script>와 <style> 안은 아예 다른 언어입니다.
들여쓰기 쪽에도 걸리는 게 있습니다.
<br> <img> <input> <meta> <link> <hr>
닫는 태그가 없는 void 요소입니다. 여는 태그를 만날 때마다 들여쓰기 단계를 올리는 구현은 여기서 계속 밀립니다. 그리고 <p>나 <li>처럼 닫는 태그를 생략할 수 있는 요소도 있어서, 태그만 세면 중첩이 맞지 않습니다.
SQL — 식별자와 키워드를 구분해야 한다
SQL 포매터가 보통 하는 일은 절 단위로 줄을 나누고 키워드를 대문자로 올리는 것입니다.
select id, name from users where status = 'active' order by created_at desc
SELECT id, name
FROM users
WHERE status = 'active'
ORDER BY created_at DESC
대문자로 올릴 대상은 키워드뿐입니다. 컬럼명 select_count나 문자열 'active'는 그대로 둬야 합니다. 따옴표로 감싼 식별자("order")는 대소문자가 의미를 가지므로 더더욱 건드리면 안 됩니다.
최소화가 실제로 얼마나 줄이나
빌드된 파일을 재 봤습니다. 이미 최소화된 상태에서 압축만 걸었을 때입니다.
index.js 919,282B → gzip 300,853B (33%) brotli 245,963B (27%)
index.css 117,222B → gzip 51,780B (44%) brotli 49,050B (42%)
작은 CSS 조각을 직접 최소화해 보면 123바이트가 88바이트로, 28% 줄었습니다. 그런데 두 결과를 gzip으로 압축하면 차이가 크게 줄어듭니다 — 반복되는 공백과 들여쓰기는 압축기가 가장 잘 처리하는 패턴이기 때문입니다.
최소화의 이득은 압축 이후에 남는 부분입니다. 공백 제거보다는 식별자 단축과 죽은 코드 제거 쪽이 실제로 남습니다.
확인 순서
- 정규식으로 코드를 정리하고 있다면 문자열·주석·정규식 리터럴이 대상에서 빠지는지 봅니다
- CSS 최소화 후
a:hover같은 의사 클래스와calc()가 살아 있는지 확인합니다 - JavaScript는 세미콜론을 생략한 코드에서 줄 병합이 위험합니다. 포매터를 바꿀 때 테스트를 돌립니다
- HTML 최소화 설정에서
pre·textarea·script·style보존이 켜져 있는지 봅니다 - 전송 압축이 이미 켜져 있다면 최소화의 추가 이득을 재 보고 판단합니다
tools.onuel.dev에 언어별로 나뉘어 있습니다. SQL 포매터는 절 단위로 줄을 나누고 키워드만 대문자로 올리며 식별자와 문자열은 두고, CSS 포매터는 선언을 한 줄씩 펴거나 한 줄로 접습니다. JavaScript 포매터는 문자열·템플릿 리터럴·주석을 쓴 그대로 두고 블록 단위로 들여쓰며, HTML 포매터는 void 요소를 처리하고 pre·textarea·script 내용을 건드리지 않습니다. 같은 성격으로 XML 포매터와 JSON 포매터가 있습니다.
- #포매터
- #CSS
- #JavaScript