
지금까지 prettier-plugin을 개발하기 위한 사전학습을 완료했습니다...만!
마지막으로 한가지만 더 살펴보려고 합니다.
지금 부터는 이론적인 부분보다는 실제 구현체를 만들기 위해서 필요한 요령이라고 생각하시면 좋을것 같습니다.
test는 개발 당시의 개발자 경험(DX) 상승에도 큰 영향을 미치지만, 처음 코드를 보는 다른 사람들에게도 큰 도움을 주곤합니다.

다음 사진은 prettier에서 제공하는 test의 일부를 확인한 모습입니다.
다음을 확인해보니 module.exports 내부에 languages와 parsrs, printers가 눈에 보이면서 각각의 요소에 연결되어 있는것을 확인할 수 있습니다.
하지만 앞선 글에서 설명한것과 같이 html , Js 등등은 이미 prettier에서 내장되어 제공되고 있으므로 해당 코드의 parser와 printer를 사용하면 훨씬 쉽게 작업이 가능할것 이라고 생각할 수 있습니다.
사실 아직까지도 감이 안올 수 있습니다. prettier의 모든 부분을 완벽하게 이해하는것이 아니라면 당연히 다른 프로젝트의 구현체를 참고하여 구현하면 좀 더 쉬운 구현이 가능할 것입니다.
다른 프로젝트의 prettier 내장 parser 사용법을 익혀봅시다!
다음은 trivago에서 개발한 prettier-plugin 입니다.


2700+ 의 스타수와 33명의 contibutors가 있는것으로 보아 충분히 시도할 만하다고 생각했습니다.

그래서 다음과 같이, parsers에 내가 원하는 요소들, 에대해서 parsers를 연결해서 exports를 진행하면 됩니다.
여기서 languages 와 printer 가 제거된 이유는 앞선 글에서 확인할 수 있습니다! babel , flow, typescript 등 이미 prettier가 제공하는 속성이기 때문입니다.
여기서 저희는 preprocess를 조작해 주기만 하면 됩니다. preprocess는 printer가 동작하기 직전에 동작하며, print 동작을 하기전 우리가 "원하는" 현태의 코드로 변호나하는 역할을 수행합니다. 저희는 여기서 정렬을 수행해주면 된다는 것을 알 수 있습니다.

https://github.dev/prettier/prettier/blob/main/src/main/plugins/load-plugin.js
해당 코드에서 확인해보면 parser 내부의 동작과정을 이해할 수 있습니다.
prettier -plugin 의 모든 부분을 우리가 개발 할 필요는 없습니다.
우리는 prettier에서 기본 제공하는 언어인 JS, HTML 에서 동작하는 plugin을 활용할 것이기 때문에 별도의 설정을 하지 않아도 좋습니다.
하지만, preprocess를 통해서 우리가 원하는 형태로 출력하는 것이 필요합니다.
저의 최종 목표를 먼저 설정하면 좀 더 명확하게 문제에 접근 할 수 있습니다.
// input
function add(a: number, b: number) {
const element = /*html */ `<div><div>hihi
</div><div>hihi
</div>
</div>`;
return a + b;
}
// output
function add(a: number, b: number) {
const element = /*html */ `<div>
<div>hihi</div>
<div>hihi</div>
</div>`;
return a + b;
}
전 prettier의 plugin 으로 JS(혹은 ts, tsx 등등) 파일 내부의 백틱(``)으로 감싸여진 html 코드가 다음과 같이 정렬되기를 원합니다.
그리고 문제를 해결하기 위해서, 큰 문제를 여러가지의 작은문제로 쪼개면 좀 더 빠르게 방향을 잡을 수 있습니다.
문제를 작게 나누어 보았지만, 아직 조금 명확하게 보이진 않네요. 그래도 조금씩 시도해봅시다.
우리는 현재 prettier-plugin을 개발하고 있습니다. 당연하게도 코드의 변경사항을 배포를 통해서 확인하는것은 말도 안됩니다. 생산성 자체가 굉장히 낮아지며, 무엇보다 그렇게 개발하면 안됩니다.
react 프로젝트를 수행할때, webpack을 사용한다면, webpack-dev-server를 사용하는것과 같이 적어도 우리가 개발한 코드에 대한 변경사항을 개발환경에서 바로바로 테스트를 할 수 있어야 합니다.
그래서 저는 Jest 기반의 테스트 환경, prettier config에 직접 plugins를 추가해 VS code 기반 환경에서 save on format 기능 또한 정상 동작하는지 확인하려 했습니다.
// Prettier의 기본 설정에 대한 타입을 정의합니다. 이는 Prettier 문서에서 제공하는 옵션들을 기반으로 합니다.
interface PrettierConfig {
semi?: boolean;
printWidth?: number;
tabWidth?: number;
trailingComma?: "none" | "es5" | "all";
singleQuote?: boolean;
jsxBracketSameLine?: boolean;
parser?: string;
plugins?: string[];
}
// 사용자 정의 설정을 포함하는 확장 인터페이스를 정의합니다.
interface ModifiedOptions extends PrettierConfig {
// 아직 추가되지 않았습니다.
// 해당 부분은 사용자 정의 옵션을 추가로 입력받을 때 사용합니다.
}
// Prettier 설정을 정의합니다.
const config: ModifiedOptions = {
parser: "typescript",
plugins: ["./src/index.ts"], // 해당 경로의 module을 export하여 사용합니다
printWidth: 80,
tabWidth: 2,
trailingComma: "all",
singleQuote: true,
jsxBracketSameLine: true,
semi: true,
};
export default config;
import prettier from "prettier";
import config from "./test-config"; // 설정한 config
const code = `
function add(a:number,b:number) {
const element = /*html */ \`<div><button /><button /><button /><button /></div>\`
return a + b;
}
`;
const formattedCode = `function add(a: number, b: number) {
const element = /*html */ \`<div>
<button></button>
<button></button>
<button></button>
<button></button>
</div>\`;
return a + b;
}
`;
test("one line html code test", () => {
const output = prettier.format(code, config); //prettier에 따른 format을 받습니다
expect(output).toEqual(formattedCode);
});
다음과 같이 설정하여 prettier의 formatter가 동작하여 해당 code의 결과물을 반환하여 비교할 수 있도록 합니다.
해당 기능을 통해서 설정한 module에서의 코드가 정상작동 하는지 쉽게 확인할 수 있습니다.
두번째로는 해당 프로젝트의 example 폴더 내부의 환경입니다.

해당 코드를 확인해보면, dist로 생성된(tsc 명렁어로 빌드된) 모듈을 export 받아서 실행시킵니다.
따라서 example/ 휘하의 파일들은 모두 prettier 플러그인이 동작하기 때문에 실제 코드에서도 바로바로 확인할 수 있는 환경을 셋팅할 수 있습니다.
사실 이 과정의 대부분은 이미 해결되어있습니다.
우리는 prettier에서 기본 제공하는 languages 기반에서 동작할것이기 때문에, 별도의 languages에 대한 연결은 하지 않아도 괜찮습니다.
하지만 plugin이 동작하기 위해선 백틱내부에 html 코드가 있는지 확인하는 과정이 꼭 필요했는데요!
이 과정을 해소 하기 위해서 정규식으로 간단하게 해결할 수 있었습니다.

다음 정규식을 통해서 해당 plugin이 동작가능한 환경인지에 대한 확인을 진행합니다.
해당 글에서는 정규식에 대한 자세한 설명은 생략하도록 하겠습니다.
왜 백틱 바로 앞에/*html */ 이 붙어야만 하나요?
prettier plugin인을 만들때, 그저 백틱 내부의 html 코드가 작성되어 있다면 바로 formatting을 수행하려 했습니다.
하지만 일부 특수한 경우에는 정렬을 원하지 않을 수 있어 이 부분은 option으로 따로 제공을 하는것이 옳다는 판단을 내리게 되었습니다. 해당 방식은 해당 익스텐션 에 크게 영향을 받았습니다.
백틱 내부의 요소에 대해서는 reset을 시켜주어야만 정상적으로 Tokenizer -> Lexer -> Parser 를 수행시킬 수 있으리라 생각했습니다.
그에 따라서 다음과 같은 코드를 통해서 해당 백틱 내부의 html 코드를 수정해주었습니다.

본 코드의 entify는 다른 단락에서 추가 설명하겠습니다. 우선 지금은 현재 작성된 정규표현식을 통한 reset과정이 진행됨을 확인하시면 됩니다!
해당 코드는 태그의 양쪽 부분들에 대해서 공백을 제거하는 로직입니다. 이 방식을 사용하면, html 코드가 말끔히 연결되어 정렬시킬 수 있습니다.'
prettier plugin 개발의 마지막에 거의 도착했습니다!
자세한 코드는 해당 프로젝트의 레포에서 확인하실 수 있으며,
해당 글에서는 해당 로직의 핵심적인 부분만을 포함합니다.

enqueue 함수는 HTML 태그를 파싱하여 배열에 저장합니다. 이 배열은 후에 들여쓰기와 정렬을 위해 사용됩니다. 각 태그는 고유한 형식([#-# : index : tag : #-#])으로 변환되어 코드 내에 임시로 저장됩니다.

init 함수는 코드를 초기화하고 자체 닫히는 태그를 처리한 후, enqueue 함수를 호출하여 HTML 태그들을 준비합니다
const process = (code: string, step: number, distance): string => {
let indents = "";
let distanceIndents = " ";
// 초기 distance 설정
for (let i = 0; i < distance / 2; i++) {
distanceIndents += " ";
}
line.forEach((source, index) => {
code = code
.replace(/\n+/g, "\n") // 연속된 개행 문자를 하나로 줄입니다.
.replace(`[#-# : ${index} : ${source} : #-#]`, (match) => {
let subtrahend = 0; // 들여쓰기를 조정할 변수
const prevLine = `[#-# : ${index - 1} : ${line[index - 1]} : #-#]`;
indents += "0"; // 현재 들여쓰기 수준을 나타냄
// 특정 조건에 따라 들여쓰기 수준을 조정합니다.
if (index === 0) subtrahend++;
if (match.indexOf(`#-# : ${index} : </`) > -1) subtrahend++;
if (prevLine.indexOf("/> : #-#") > -1) subtrahend++;
if (prevLine.indexOf(`#-# : ${index - 1} : </`) > -1) subtrahend++;
const offset = indents.length - subtrahend; // 최종 들여쓰기 계산
indents = indents.substring(0, offset); // 들여쓰기 업데이트
const result = match
.replace(`[#-# : ${index} : `, "")
.replace(" : #-#]", ""); // 태그를 원래 형태로 복원
// 들여쓰기 적용
return (
(index !== 0 ? distanceIndents : "") +
result.padStart(result.length + step * offset)
);
});
});
code = code.replace(
/>[^<]*?[^><\/\s][^<]*?<\/|>\s+[^><\s]|<script[^>]*>\s+<\/script>|<(\w+)>\s+<\/(\w+)|<([\w\-]+)[^>]*[^\/]>\s+<\/([\w\-]+)>/g,
(match) => match.replace(/\n|\t|\s{2,}/g, "")
);
return code.substring(1, code.length - 1);
};
process 함수에서는 enqueue에서 생성된 형식을 사용하여 실제 코드의 포매팅을 진행합니다. 여기서 태그의 위치와 들여쓰기 수준에 따라 코드를 재정렬합니다.
- 첫 번째 태그의 경우: 문서의 시작 부분에서는 추가 들여쓰기 없이 시작합니다. 따라서 첫 번째 태그에서는 subtrahend 값을 증가시켜 들여쓰기를 조정하지 않습니다.
- 종료 태그의 경우: 종료 태그(</...>)를 만나면 들여쓰기 수준을 감소시켜야 합니다. 이를 위해 해당 태그가 종료 태그인지 확인하고, 맞다면 subtrahend 값을 증가시켜 들여쓰기를 줄입니다.
- 자체 닫히는 태그의 경우: <.../> 형식의 자체 닫히는 태그는 추가 들여쓰기 없이 처리합니다. 이런 태그를 만나면 subtrahend 값을 증가시켜 들여쓰기를 감소시킵니다.
- 이전 태그가 종료 태그인 경우: 이전 태그가 종료 태그일 경우, 현재 태그의 들여쓰기를 감소시켜야 합니다. 이전 태그가 종료 태그인지 확인하고, 맞다면 subtrahend를 증가시켜 들여쓰기를 조정합니다.
예를 들어, HTML 코드가 <div><span>text</span></div>로 주어졌을 때, 이 코드는 먼저 enqueue를 통해 <div>, <span>, </span>, </div> 태그를 파싱하고, process에서 이 태그들에 대한 들여쓰기와 정렬을 진행하여 최종적으로 포맷된 코드를 반환합니다.
지금까지 prettier plugin을 만들기 위한 과정을 살펴보았습니다.
생소한 문제라서 조금 어렵게 생각했지만, 공식문서와 오픈소스를 기반으로 조금씩 접근하다보니 해결할 수 있었다고 생각합니다.
해당 글이 앞으로 prettier-plugin을 개발하려고 하시는 분들께 소소하게 도움이 되길 바랍니다.
