Prettier는 왜 내 Dart 코드만 그냥 두고 갈까 (부제: 그래서 만들어봤다)

이메일로 마크다운 문서를 하나 받았다. 안에 Dart 코드가 블록 단위로 흩어져 있었는데, 하나같이 줄이 안 맞아 있었다.

그래서 이 짓을 하고 있었다.

블록 하나를 복사한다. dartpad.dev에 붙여넣는다. 포맷을 돌린다. 결과를 복사한다. 문서로 돌아와 그 자리에 붙여넣는다. 다음 블록으로 간다.

세 번째 블록쯤에서 현타가 왔다. 나는 이 문서를 어차피 prettier --write로 한 번 훑을 참이었다. 얘가 다 해주면 안 되나?

얘는 왜 어떤 건 해주고 어떤 건 안 해줄까

Prettier로 마크다운을 포맷해본 사람은 알겠지만, 얘는 코드 블록 안쪽까지 건드린다. ```json에 중괄호랑 공백을 아무렇게나 쑤셔넣어 두면, 저장할 때 알아서 반듯하게 펴놓는다. yaml도 그렇고 css도 그렇다.

그런데 이건 몇 번을 저장해도 그대로다.

```dart
void main(){print('hello');}
```

띄어쓰기 하나 안 고치고 그냥 지나간다.

(이 문단 쓰다가 실제로 당했다. 못생긴 json 예제를 넣어놨더니 저장하는 순간 프리티어가 예쁘게 펴버려서, before랑 after가 똑같아졌다. 얘 일 잘한다.)

처음엔 마크다운 안쪽은 원래 안 건드리는 줄 알았다. 근데 json은 되잖아. yaml도 되고. css도 된다. 그럼 dart만 왜?

답은 시시하다. Prettier가 Dart를 모른다. JavaScript, CSS, 마크다운, YAML 같은 건 Prettier가 직접 포맷터를 들고 있는데, Dart는 그 목록에 없다. 모르는 언어라서 안 건드리는 거지, 안 건드리기로 정한 게 아니다.

그럼 알려주면 되는 거 아닌가? 그게 플러그인이다.

플러그인이라길래 큰 건 줄 알았다

솔직히 말하면 "포맷터 플러그인"이라는 말 때문에 겁을 먹었다. Dart 파서를 짜야 하나? 들여쓰기 규칙을 다 구현해야 하나? (전 Dart 문법 트리 같은 거 못 짭니다. 진짜로요.)

그럴 필요가 없었다. 내 컴퓨터엔 이미 dart format이 깔려 있다. Flutter 쓰면 딸려 온다. 포맷 잘하는 애가 이미 있는데 내가 왜 다시 만드나.

그래서 실제로 만든 건 통역사에 가깝다. Prettier한테 "얘가 Dart라는 언어인데, 얘 만나면 나한테 넘겨"라고 등록해두고, 넘어온 코드를 dart format한테 던지고, 돌아온 걸 그대로 돌려주는 것.

칼질 잘하는 사람 옆에 두고 나는 접시만 나르는 거다.

Prettier 플러그인은 세 덩어리다

플러그인이 내보내야 하는 건 딱 세 개다. 이름만 보면 무섭게 생겼는데 하는 일은 단순하다.

languages — "이런 언어가 있어요" 명단. fence에 dart라고 써 있으면 나한테 보내라는 신고서다.

parsers — 코드를 받아서 처리하는 곳. 보통은 여기서 코드를 문법 트리로 쪼개는데, 나는 그냥 dart format한테 던진다.

printers — 처리된 걸 다시 글자로 뽑는 곳. dart format이 이미 완성된 텍스트를 주니까, 여기서 할 일이 없다. 받은 걸 그대로 뱉는다.

export const languages = [{ name: "Dart", aliases: ["dart"], parsers: ["dart"] }];

정말 이게 다다. "파서"라고 부르지만 실제로는 문자열 하나 들고 있는 상자를 만들고, "프린터"는 그 상자를 열어서 문자열을 꺼낸다. 어렵게 생긴 이름에 비해 하는 일이 없다.

호출은 이렇게 한다.

spawn("dart", ["format", "--output=show", "--stdin-name", stdinName]);

--output=show는 파일을 고치지 말고 결과를 화면에 뱉으라는 뜻이다. 파일이 아니라 문자열을 다루고 있으니까.

여기서 제일 마음에 드는 트릭

문제가 하나 있다.

Dart 포맷 규칙은 프로젝트마다 다르다. analysis_options.yaml에 이런 걸 적어두면

formatter:
  page_width: 100
  trailing_commas: preserve

dart format이 그걸 읽고 그 규칙대로 포맷한다. (trailing_commas: preserve는 Dart 3.8 이상에서 동작한다.)

그럼 내 플러그인도 이 YAML을 읽어야 하나? 파일 찾아 올라가면서, 파싱하고, 옵션 해석하고...

안 했다.

dart format한테는 --stdin-name이라는 옵션이 있다. "지금 들어오는 이 코드, 원래는 여기 있던 파일이라고 치자"고 알려주는 옵션이다.

그래서 docs/guide.md를 포맷하는 중이면 docs/guide.dart라는 있지도 않은 경로를 넘긴다. 그러면 Dart가 알아서 그 위치부터 위로 올라가면서 analysis_options.yaml을 찾는다. 평소에 진짜 Dart 파일 포맷할 때 하던 그대로.

파일은 만들지 않는다. 이름만 빌려준다. 덕분에 이 플러그인에는 YAML 파서가 한 줄도 없다.

남이 이미 잘하는 일은 남한테 시키면 된다. (YAML 파서 안 짜도 된다는 걸 깨달았을 때가 이 프로젝트에서 제일 기분 좋았다.)

조용히 아무것도 안 하는 게 제일 무섭다

여기서 진짜 삽질 이야기.

이 플러그인은 dart format이 실패하면 그 블록을 그대로 둔다. 일부러 그렇게 만들었다. 글 쓰다 보면 문법이 덜 완성된 예제를 넣어둘 때가 있는데, 그것 때문에 문서 전체 포맷이 실패하면 곤란하니까.

그런데 이 착한 설계에 구멍이 있었다.

dart가 아예 안 깔려 있어도 똑같이 조용히 지나간다. prettier --check는 종료 코드 0을 뱉는다. "다 잘 포맷돼 있습니다!" 하면서.

아무것도 포맷 안 했는데.

CI에서 이게 제일 무섭다. Dart SDK 안 깔린 러너에서 초록불이 뜬다. 잘 돌아가는 것처럼 보인다. 아무 일도 안 하면서.

그래서 지금은 dart를 못 찾으면 경고를 한 번 띄운다.

[prettier-plugin-markdown-dart] `dart` was not found on PATH. Dart code blocks are left unchanged.

교훈: 조용한 성공과 조용한 실패가 구별이 안 되면, 그건 성공이 아니다. (이거 알아내는 데 한참 걸렸다. 테스트는 다 통과하고 있었거든. 내 컴퓨터엔 Dart가 깔려 있으니까.)

참고로 이것 말고도 하위 프로세스 타임아웃 관련해서 한참 헤맨 게 있는데, 그건 primer에서 다룰 깊이가 아니라 접어둔다. 궁금하면 저장소 CLAUDE.md에 이유까지 적어놨다.

그래서 뭐가 필요한가

정리하면 세 가지다.

Prettier 3. Node 18 이상. 그리고 dartPATH에 있을 것. (Flutter 깔았으면 이미 있다.)

설치하고

npm install --save-dev prettier prettier-plugin-markdown-dart

설정에 한 줄 넣으면 끝이다.

{
  "plugins": ["prettier-plugin-markdown-dart"]
}

이제 평소처럼 prettier --write README.md 하면 ```dart 블록이 알아서 정리된다. 2026년 8월 18일 기준 1.1.1이 올라가 있다.

만들어보라, 생각보다 작다

이 플러그인 전체가 100줄이 안 된다. (v1.1.1 기준 92줄.) Dart 문법은 한 글자도 몰라도 된다. 포맷은 dart format이 하고, 설정 탐색은 Dart가 하고, 마크다운은 Prettier가 한다. 나는 셋을 이어주기만 했다.

혹시 Prettier가 안 건드려주는 언어를 쓰고 있다면 — Swift든 Kotlin든 뭐든 — 그 언어에 CLI 포맷터가 있는지부터 보라. 있으면 절반은 끝난 거다.

그리고 부탁인데, 만들면 테스트는 꼭 넣으시라. 나도 "잘 되네" 하고 넘어갔다가, 코드를 다시 훑는 김에 위에 쓴 조용한 실패를 발견했다. (그것도 테스트가 아니라 손으로 PATH를 비워보고 나서야 알았다. 헤헤)

틀린 내용 있으면 알려주세요. 고치겠습니다.