개발 이야기4분 읽기

포매터가 건드리면 안 되는 것들

공백을 정리하는 일이 왜 단순한 치환이 아닌지, 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으로 압축하면 차이가 크게 줄어듭니다 — 반복되는 공백과 들여쓰기는 압축기가 가장 잘 처리하는 패턴이기 때문입니다.

최소화의 이득은 압축 이후에 남는 부분입니다. 공백 제거보다는 식별자 단축과 죽은 코드 제거 쪽이 실제로 남습니다.

확인 순서

  1. 정규식으로 코드를 정리하고 있다면 문자열·주석·정규식 리터럴이 대상에서 빠지는지 봅니다
  2. CSS 최소화 후 a:hover 같은 의사 클래스와 calc()가 살아 있는지 확인합니다
  3. JavaScript는 세미콜론을 생략한 코드에서 줄 병합이 위험합니다. 포매터를 바꿀 때 테스트를 돌립니다
  4. HTML 최소화 설정에서 pre·textarea·script·style 보존이 켜져 있는지 봅니다
  5. 전송 압축이 이미 켜져 있다면 최소화의 추가 이득을 재 보고 판단합니다

tools.onuel.dev에 언어별로 나뉘어 있습니다. SQL 포매터는 절 단위로 줄을 나누고 키워드만 대문자로 올리며 식별자와 문자열은 두고, CSS 포매터는 선언을 한 줄씩 펴거나 한 줄로 접습니다. JavaScript 포매터는 문자열·템플릿 리터럴·주석을 쓴 그대로 두고 블록 단위로 들여쓰며, HTML 포매터는 void 요소를 처리하고 pre·textarea·script 내용을 건드리지 않습니다. 같은 성격으로 XML 포매터JSON 포매터가 있습니다.

  • #포매터
  • #CSS
  • #JavaScript