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 이상.
그리고 dart가 PATH에 있을 것. (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를 비워보고 나서야 알았다. 헤헤)
틀린 내용 있으면 알려주세요. 고치겠습니다.
다음 글도 받아보세요.
글을 끝까지 읽으셨다면, 다음 글은 받은편지함이나 RSS 리더에서 만나보세요.