<?xml version="1.0" encoding="utf-8"?><rss version="2.0"><channel><title>&lt;![CDATA[지나가던 개발자]]&gt;</title><link>https://znagadeon.dev</link><description>&lt;![CDATA[카카오 프론트엔드 개발자 since 2018. 01.]]&gt;</description><item><title>&lt;![CDATA[husky를 이용한 정적 분석 자동화]]&gt;</title><link>https://znagadeon.dev/post/automate-lint/</link><description>&lt;![CDATA[&lt;p&gt;&lt;a href="https://znagadeon.dev/post/format-my-code/index.html" title="null" rel="noopener" target="_blank"&gt;지난 글&lt;/a&gt;에서 editorconfig, prettier, eslint를 이용해 코드를 정적 분석하고 일관된 스타일을 적용하는 법에 대해 알아보았다. 분명 이 작업을 통해서 코드의 품질이 좋아지고, 놓치는 버그의 수도 확연히 줄어들었을 것이다. 하지만 과연 실제로도 그럴까? 충분히 꼼꼼한 사람이 아니라면 이런 일이 쉽게 발생할 수 있다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;PR 올렸어요. 아차! lint 돌리는 걸 깜빡했네요. 잠시만요...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;문제는 &lt;strong&gt;정적 분석을 손으로 돌려야 한다&lt;/strong&gt;는 점이다. 코드의 문제점을 자동으로 검출해주지만 그 과정을 사람이 명령해야 그제서야 느릿느릿 시작한다면 컴퓨터가 너무 나태하지 않은가? 이래서야 아주 일을 잘 하는 월급루팡이나 다를 게 없다. 이번 글에서는 이 월급루팡 컴퓨터가 스스로 알아서 일하게 만들어 보자.&lt;/p&gt;
&lt;h2 id="커밋할-때-자동으로-정적-분석-돌리기"&gt;커밋할 때 자동으로 정적 분석 돌리기&lt;/h2&gt;
&lt;p&gt;애초에 커밋하는 시점에 정적 분석이 끝나 있었다면 이런 문제는 발생하지 않았을 것이다. 보통 이런 문제를 해결하기 위해서 git hook이라는 기능을 사용한다. hook은 한국어로 갈고리라는 뜻인데, git hook은 말하자면 git이 실행되는 중간중간 갈고리를 걸어서 미리 등록해둔 내 스크립트를 실행시켜주는 것이다.&lt;/p&gt;
&lt;p&gt;다만 조금 아쉬운 점은 git hook 스크립트는 &lt;code class="short-code"&gt;.git&lt;/code&gt; 디렉토리 밑에 있다는 것이다. 즉 통상적인 방법으로는 버전 관리 시스템에 올릴 수 없다는 말이고, &lt;em&gt;우리 팀은 이런 git hook을 사용하고 있으니 이렇게 사용하라&lt;/em&gt;고 위키나 README에 구전처럼 전해야 한다.&lt;/p&gt;
&lt;p&gt;이를 아쉽게 생각한 어느 개발자가 &lt;code class="short-code"&gt;husky&lt;/code&gt;라고 하는 툴을 개발했다. &lt;code class="short-code"&gt;husky&lt;/code&gt;는 git hook을 현재 코드베이스에서 관리할 수 있게 해주며, 따라서 설치만 잘 되어 있다면 어떤 환경에서든 동일한 hook을 사용함을 보장하게 된다. 백문이 불여일견, 일단 설치해서 따라해 보자.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code class="short-code"&gt;husky&lt;/code&gt;는 현재 v5까지 출시되어 있다. 오픈 소스 프로젝트나 후원자 프로젝트에 한해 v5를 사용할 수 있고, 완전 무료로 사용하고 싶은 경우 v4를 사용해야 한다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You&amp;#39;re viewing documentation for husky v5, which is free to use in Open Source projects ❤️ and in early access for Sponsors 🎁. To use this new version at work, you can become a sponsor on GitHub Sponsors or Open Collective.&lt;/p&gt;
&lt;p&gt;If you can&amp;#39;t sponsor Husky, that&amp;#39;s okay, husky v4 is free to use in any project.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;npm i -D husky&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;설치한 뒤 &lt;code class="short-code"&gt;package.json&lt;/code&gt;에 hook을 추가할 수 있다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;&amp;quot;husky&amp;quot;&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;&amp;quot;hooks&amp;quot;&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-attr"&gt;&amp;quot;pre-commit&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;echo \&amp;quot;Hello, world!\&amp;quot;&amp;quot;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;        }&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;6&lt;/code&gt;&lt;code class="code"&gt;    }&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;7&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/hello-husky.png" alt="성공적으로 husky가 실행된 모습"&gt;&lt;figcaption aria-hidden="true"&gt;성공적으로 husky가 실행된 모습&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;이제 &lt;code class="short-code"&gt;pre-commit&lt;/code&gt; 훅에 정적 분석 스크립트를 추가하면, 커밋을 할 때마다 코드 포매팅을 강제할 수 있다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;&amp;quot;husky&amp;quot;&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;&amp;quot;hooks&amp;quot;&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-attr"&gt;&amp;quot;pre-commit&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;prettier --write . &amp;&amp; eslint .&amp;quot;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;        }&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;6&lt;/code&gt;&lt;code class="code"&gt;    }&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;7&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h2 id="수정된-파일만-분석할-수는-없을까"&gt;수정된 파일만 분석할 수는 없을까?&lt;/h2&gt;
&lt;p&gt;여기까지 따라해 보았다면 뭔가 답답함을 느낀 독자가 있을 것이다. 커밋 속도가 엄청나게 느려지기 때문이다. 이전에는 설명하지 않았지만 이전에 내가 소개한 정적 분석 시스템에는 실서비스에 적용하기엔 좀 뭣한 치명적인 단점이 있었는데, 바로 &lt;strong&gt;한 번 정적 분석 툴을 돌리면 모든 파일이 전부 분석 범위 안에 들어간다&lt;/strong&gt;는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/too-long.png" alt="0.5초의 압박"&gt;&lt;figcaption aria-hidden="true"&gt;0.5초의 압박&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;커밋만 느려지는 게 아니다. 여러분이 작업 중인 프로젝트가 적게는 수만 줄, 많게는 수십만 줄의 코드가 돌아가고 있는 거대한 시스템이라면 어떨까? &lt;code class="short-code"&gt;git blame&lt;/code&gt;을 사용했을 때 모든 소스코드에 내 이름이 나오게 하고 싶은 게 아니라면 내가 수정한 파일만 골라서 정적 분석 툴을 돌릴 수 있는 기능이 절실해진다. 이럴 때 사용할 수 있는 툴이 &lt;code class="short-code"&gt;lint-staged&lt;/code&gt;이다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;npm i -D lint-staged&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;이번에도 마찬가지로 &lt;code class="short-code"&gt;package.json&lt;/code&gt;에 설정을 작성한다. rc 파일을 생성하는 것도 가능한데, json, yaml 포맷을 지원한다. 이 때, &lt;strong&gt;스크립트에 있는 경로 지정자를 다 빼 버려야 알아서 변경된 파일만 추적하게 만들 수 있다.&lt;/strong&gt; 기존 prettier와 eslint는 경로를 지정해 주었는데 아래 스크립트에서는 경로가 빠진 것에 주목하자.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;&amp;quot;husky&amp;quot;&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;&amp;quot;hooks&amp;quot;&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-attr"&gt;&amp;quot;pre-commit&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;lint-staged&amp;quot;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;        }&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;    },&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;&amp;quot;lint-staged&amp;quot;&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;&amp;quot;**/*.{js,jsx,ts,tsx}&amp;quot;&lt;/span&gt;: [&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-string"&gt;&amp;quot;prettier --write&amp;quot;&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-string"&gt;&amp;quot;eslint&amp;quot;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;        ],&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;&amp;quot;**/*.json&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;prettier --write&amp;quot;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;13&lt;/code&gt;&lt;code class="code"&gt;    }&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;14&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/block-commit.png" alt="eslint 오류로 커밋이 멈추었다"&gt;&lt;figcaption aria-hidden="true"&gt;eslint 오류로 커밋이 멈추었다&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;이제 모든 코드는 커밋 시 자동으로 포매팅되며, 잘못된 코드는 커밋조차 불가능하다.&lt;/p&gt;
&lt;h2 id="정리"&gt;정리&lt;/h2&gt;
&lt;p&gt;이전 글에서도 말했지만 사람은 누구나 실수를 한다. 나는 그 실수를 어떻게 대하는지가 성장으로 가는 발판이라고 본다. 시작은 실수투성이 코드였지만 정적 분석과 자동화라는 두 가지 무기를 가지고 우리는 일관적이고 잘 정리된 코드를 얻을 수 있게 되었다. 혹자는 간단하고 귀찮은 일은 모조리 자동화시키고 사람은 생산적이고 창의적인 일에 시간을 쏟는 것이 생산성 향상의 지름길이라고 한다. 정적 분석 같은 귀찮은 일은 컴퓨터에 맡겨버리고 우리는 좀 더 아름다운 아키텍처에 관심을 쏟아보도록 하자.&lt;/p&gt;
]]&gt;</description><author>&lt;![CDATA[Kim Songhyun]]&gt;</author><pubDate>2025-02-05</pubDate></item><item><title>&lt;![CDATA[블로그는 사드세요... 제발]]&gt;</title><link>https://znagadeon.dev/post/build-static-blog/</link><description>&lt;![CDATA[&lt;p&gt;내가 지금 글을 쓰고 있는 이 블로그는 (몇 번인가 이야기한 적이 있는 것 같긴 한데) Vue를 이용해 바닥부터 올린 사이트이다. 요샌 별로 특이한 방식은 아니지만, 정적으로 페이지를 빌드해서 Netlify에 올리는 방식으로 배포하고 있다. &lt;code class="short-code"&gt;webpack-dev-server&lt;/code&gt;를 이용해 로컬에 미리보기 페이지를 만들고, 각종 컴포넌트들을 Vue로 만들려다보니 필연적으로 웹팩을 사용하게 됐는데, 웹팩은 정적 웹페이지를 빌드하는 용도가 맞긴 하지만 그렇다고 블로그처럼 수십 수백 개의 페이지를 정적으로 만드는 데 특화된 툴은 아니기 때문에 이런저런 꼼수가 많이 필요해졌다. 각 꼼수마다 장단점이 있는데, 혹시 나처럼 바닥부터 블로그를 올려보려는 사람이 있을까 싶어 어떤 시행착오를 거쳐 성능을 개선하고 개발적인 유연성을 얻었는지 공유해보려고 한다.&lt;/p&gt;
&lt;h2 id="초기-html-webpack-plugin"&gt;초기: &lt;code class="short-code"&gt;html-webpack-plugin&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;웹팩으로 정적 사이트를 빌드한다면 대부분은 &lt;code class="short-code"&gt;html-webpack-plugin&lt;/code&gt;을 사용한다. 적절히 옵션을 전달해주면 웹페이지에 필요한 거의 모든 것들을 내 입맛대로 조절할 수 있다. 처음에는 페이지별로 &lt;code class="short-code"&gt;html-webpack-plugin&lt;/code&gt;을 전부 따로 선언하면 깔끔하게 페이지가 완성될 거라고 생각했다(지금도 몇 가지 제약조건 하에서는 가장 좋은 방법이라고 생각한다).&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; renderPost = &lt;span class="hljs-function"&gt;(&lt;span class="hljs-params"&gt;filename&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; HtmlWebpackPlugin({&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;        filename,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;template&lt;/span&gt;: &lt;span class="hljs-string"&gt;`./layouts/post.pug`&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;favicon&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;#x27;./favicon.ico&amp;#x27;&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;chunks&lt;/span&gt;: [&lt;span class="hljs-string"&gt;&amp;#x27;post&amp;#x27;&lt;/span&gt;],&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;    });&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;13&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;14&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;plugins&lt;/span&gt;: [&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;15&lt;/code&gt;&lt;code class="code"&gt;        ...posts.map(&lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;post&lt;/span&gt; =&amp;gt;&lt;/span&gt; renderPost(post.filename)),&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;16&lt;/code&gt;&lt;code class="code"&gt;    ],&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;17&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;페이지가 늘어날수록 플러그인에는 똑같은 수의 플러그인이 추가된다. 뭐 내가 게시글을 몇천 몇만 개씩 쓸 것도 아니고 플러그인 갯수는 별로 상관없긴 하다. 다만 이 방법에는 사소한 문제점이 있는데, 한 번 &lt;code class="short-code"&gt;webpack-dev-server&lt;/code&gt;가 실행된 뒤 새로 작성한 게시글은 watch가 되지 않는다는 것이다. 사실 그냥 껐다 켜면 될 일이긴 한데 당시에는 &lt;code class="short-code"&gt;vuepress&lt;/code&gt;같은 오픈소스 페이지 빌더들을 사용할까도 고민했었기 때문에 이 단점이 굉장히 크게 다가왔던 것 같다.&lt;/p&gt;
&lt;h2 id="중기-vue-router--prerender-spa-plugin"&gt;중기: &lt;code class="short-code"&gt;vue-router&lt;/code&gt; + &lt;code class="short-code"&gt;prerender-spa-plugin&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;모든 게시글을 빠짐없이 watch하기 위해서는 결국 &lt;code class="short-code"&gt;html-webpack-plugin&lt;/code&gt;을 여러 개 사용한다는 아이디어는 폐기해야 했다. 그럼 무조건 하나의 html 파일만 사용해아 한다는 건데, 한 html 안에서 수많은 페이지들을 어떻게 구분할 것인가를 많이 고민했던 것 같다. 며칠을 고민한 뒤 고른 방법은 &lt;code class="short-code"&gt;vue-router&lt;/code&gt;로 페이지를 분리한 뒤 &lt;code class="short-code"&gt;prerender-spa-plugin&lt;/code&gt;으로 모든 페이지를 캡처하는 방식이었다. 실무에서도 단순 html만 서빙하는 페이지에서 많이 사용하는 방식으로 알고 있는데 그래서인지 이전 방식보다 확실히 신뢰도가 오른 느낌이다.&lt;/p&gt;
&lt;h2 id="후기-express--webpack-dev-middleware"&gt;후기: &lt;code class="short-code"&gt;express&lt;/code&gt; + &lt;code class="short-code"&gt;webpack-dev-middleware&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;del&gt;갑자기 분위기 서버&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;몇 주 전엔가 글또에 글을 올렸는데 한 페이지에 댓글이 두 개 뜬다는 제보가 들어왔다(댓글은 &lt;a href="https://utteranc.es/" title="null" rel="noopener" target="_blank"&gt;Utterances&lt;/a&gt;라고 하는 깃헙 기반 앱을 사용하고 있다).&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/comment-error.png" alt=""&gt;&lt;figcaption aria-hidden="true"&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;문제를 수정한 지금도 여전히 명확한 원인은 못 찾았는데, 아마 &lt;code class="short-code"&gt;prerender-spa-plugin&lt;/code&gt;은 새 페이지를 캡처할 때 새 창을 띄우는 게 아니라 그냥 URL을 교체하는 듯하다. &lt;code class="short-code"&gt;vue-router&lt;/code&gt;는 URL이 교체될 때 페이지를 완전히 새로 렌더링하는 게 아니라 필요한 부분만 교체하므로, 아마 댓글 컴포넌트가 사라지지 않은 채 새로 init되면서 두 개가 나타난 것이 아닌가 하는 게 내 생각이다.&lt;/p&gt;
&lt;p&gt;내가 뭘 잘못한 게 아니고 플러그인의 근본 구현 자체가 내 블로그와 전면 충돌하고 있었으므로 완전히 새로운 해결책이 필요했다. 이쯤에서 플러그인으로 페이지를 캡처하는 것을 포기하고 아예 캡처 함수를 내가 구현하기로 했다. 자연히 웹팩만으로는 구현이 어려워졌고, 서버를 띄운 뒤 puppeteer 혹은 AJAX 요청을 통해 컨텐츠를 캡처하는 쪽으로 가닥을 잡았다.&lt;/p&gt;
&lt;p&gt;기존에 만들어뒀던 컴포넌트들을 최대한 활용하기 위해 프론트엔드 코드를 유지할 방법이 필요했고 마침 그 즈음에 &lt;a href="https://jeonghwan-kim.github.io/dev/2020/07/18/webpack-dev-middleware.html" title="null" rel="noopener" target="_blank"&gt;&lt;code class="short-code"&gt;webpack-dev-middleware&lt;/code&gt;&lt;/a&gt;라는 라이브러리를 알게 되었다. 정적 에셋은 웹팩으로 빌드하고 라우팅 로직과 게시글 API만 서버에서 담당하게 되면서 전체적인 구조는 오히려 더 깔끔해졌고 사이트맵이나 RSS도 외부 서비스 없이 자체적으로 만들어낼 수 있었다.&lt;/p&gt;
&lt;p&gt;서버를 완성하고 나서는 빌드 스크립트를 작성했다. 서버를 띄운 뒤 모든 페이지를 캡처하고, 다시 서버를 닫는 단순한 구조다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;(&lt;span class="hljs-keyword"&gt;async&lt;/span&gt; () =&amp;gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;    spawnSync(&lt;span class="hljs-string"&gt;&amp;#x27;npm&amp;#x27;&lt;/span&gt;, [&lt;span class="hljs-string"&gt;&amp;#x27;run&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;#x27;build:static&amp;#x27;&lt;/span&gt;]);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; server = spawn(&lt;span class="hljs-string"&gt;&amp;#x27;npm&amp;#x27;&lt;/span&gt;, [&lt;span class="hljs-string"&gt;&amp;#x27;run&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;#x27;serve&amp;#x27;&lt;/span&gt;]);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; (&lt;span class="hljs-keyword"&gt;let&lt;/span&gt; post &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; postNames) {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; capture(&lt;span class="hljs-string"&gt;`&lt;span class="hljs-subst"&gt;${host}&lt;/span&gt;/post/&lt;span class="hljs-subst"&gt;${post}&lt;/span&gt;`&lt;/span&gt;, &lt;span class="hljs-string"&gt;`&lt;span class="hljs-subst"&gt;${dest}&lt;/span&gt;/post/&lt;span class="hljs-subst"&gt;${post}&lt;/span&gt;/index.html`&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; captureApi(&lt;span class="hljs-string"&gt;`&lt;span class="hljs-subst"&gt;${host}&lt;/span&gt;/api/post/&lt;span class="hljs-subst"&gt;${post}&lt;/span&gt;`&lt;/span&gt;, &lt;span class="hljs-string"&gt;`&lt;span class="hljs-subst"&gt;${dest}&lt;/span&gt;/api/post/&lt;span class="hljs-subst"&gt;${post}&lt;/span&gt;.json`&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;    }&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;13&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;14&lt;/code&gt;&lt;code class="code"&gt;    server.kill();&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;15&lt;/code&gt;&lt;code class="code"&gt;    process.exit();&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;16&lt;/code&gt;&lt;code class="code"&gt;})();&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;다만 여기서 또 문제가 생겼는데, puppeteer로 캡처하면 분당 GitHub API 최대 호출수를 초과하면서 댓글이 제대로 나오지 않았고(아마 이건 prerender를 사용할 때도 그랬을 것 같긴 하다) AJAX로 html만 긁어오는 방법을 사용하면 API를 콜하지 않기 때문에 홈페이지가 Client Side Rendering으로 구현되게 되어 버렸다. 정적 홈페이지를 표방하다 보니 API 서버를 따로 둘 수 없어 페이지마다 API 덤프를 따로 떠야 하는 건 덤. 빌드할 때마다 댓글에서 GitHub API를 호출해대는 게 좀 불안했기 때문에 약간의 성능 손실과 트래픽 두 배를 감수하고 AJAX 콜을 하는 방식으로 구현했다.&lt;/p&gt;
&lt;h2 id="최근-vue-ssr"&gt;최근: Vue SSR&lt;/h2&gt;
&lt;p&gt;API를 콜해야만 내용이 나와서 문제인 것이라면, &lt;em&gt;서버에서 미리 API를 콜하면 되는 것이 아닌가?&lt;/em&gt; 하는 생각이 들어 지난 주부터 Vue SSR을 적용해 보았다. 게시글 내용이나 메타정보는 서버에서 미리 렌더링하고, prerender에서 문제가 됐던 외부 스크립트 로딩은 클라이언트단에서 처리하는 방식으로, 화면을 렌더링하기 위한 AJAX 콜이 제거되기 때문에 트래픽과 렌더링 속도 양쪽에서 이점이 있다.&lt;/p&gt;
&lt;p&gt;물론 SSR도 단점은 존재한다. 서버에서 미리 DOM을 렌더링할 경우, 가만히 놔두면 클라이언트는 서버가 이미 렌더링을 끝냈다는 사실을 모르기 때문에 빈 데이터를 가지고 화면을 새로 렌더링한다. 이렇게 되지 않기 위해 서버-클라이언트 간 데이터 싱크가 필요한데, 문제는 이 과정에서 html 안에 &lt;code class="short-code"&gt;window.__INITIAL_STATE__&lt;/code&gt;라는 변수를 삽입하는데 게시글의 경우 이 변수가 상당히 길어진다는 것이다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;if&lt;/span&gt; (&lt;span class="hljs-built_in"&gt;window&lt;/span&gt;.__INITIAL_STATE__) { &lt;span class="hljs-comment"&gt;// 서버에서 렌더링 완료 시 html에 삽입해주는 데이터. 매우 길다&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-comment"&gt;// vuex store의 state를 교체한다&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;    store.replaceState(&lt;span class="hljs-built_in"&gt;window&lt;/span&gt;.__INITIAL_STATE__);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;더 짧은 html을 만들기 위해서는 아예 Vue를 버리고 템플릿 엔진으로 갈아타거나, 외부 스크립트를 분석해서 iframe 등을 직접 삽입하는 방법이 있을 듯한데 이 역시 나름의 제약이 있어 일단 여기서 마무리하려고 한다.&lt;/p&gt;
&lt;h2 id="여전히-남아-있는-것"&gt;여전히 남아 있는 것&lt;/h2&gt;
&lt;p&gt;이렇게 해서 뭔가 그럴싸한 웹사이트 생성기를 만들긴 했는데 아직 갈 길이 멀다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code class="short-code"&gt;webpack-dev-server&lt;/code&gt;를 포기해 버려서 파일이 변경될 때 웹페이지가 자동으로 새로고침되지 않는다.&lt;/li&gt;
&lt;li&gt;Netlify에서 서브프로세스를 열면 빌드 스크립트가 인식하는 pid와 실제 pid가 달라지면서 프로세스가 종료되지 않는다.&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/66636946/netlify-has-different-pid-on-build-process" title="null" rel="noopener" target="_blank"&gt;질문&lt;/a&gt;을 했는데 아무도 받아주지 않는다...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이래저래 아쉬운 점이 많은 블로그이지만 이런 식으로 계속 최적화하는 데서 오는 재미도 있어서 놓지를 못하겠다.&lt;/p&gt;
]]&gt;</description><author>&lt;![CDATA[Kim Songhyun]]&gt;</author><pubDate>2025-02-05</pubDate></item><item><title>&lt;![CDATA[CSS 중앙 정렬의 역사]]&gt;</title><link>https://znagadeon.dev/post/center-css/</link><description>&lt;![CDATA[&lt;p&gt;가운데 정렬은 어렵다.&lt;/p&gt;
&lt;p&gt;근본적인 이유를 파고들면 부모 태그와 자식 태그가 어떤 속성을 가졌냐에 따라 가운데 정렬에 사용되는 속성이 무시되거나 다르게 해석될 여지가 있기 때문이다. 요새는 flex가 사실상 표준이라 새 사이트나 페이지를 만들 때엔 그걸 쓰면 되지만 옛날엔 그렇지 못했다. 오늘은 flex가 없던 시절부터 지금에 이르기까지 CSS에서 가운데 정렬을 어떻게 처리했는지를 이야기해보려고 한다.&lt;/p&gt;
&lt;p&gt;설명을 단순화하기 위해  &lt;code class="short-code"&gt;--webkit-&lt;/code&gt; 등의 폴리필은 사용하지 않는다고 가정한다. 또, 사용 가능한 속성이라고 해도 브라우저에 따라 보이는 모양은 조금씩 다를 수 있다는 사실을 미리 알린다. 가운데 정렬한 컴포넌트를 왼쪽에 보여주는 수준인 건 아니고, 적게는 1~2px, 많게는 한 글자 정도의 공간은 틀어질 수 있다는 뜻이다.&lt;/p&gt;
&lt;p&gt;또, html 꼼수가 들어가는(주석을 사용해서 마진을 제거하는 기법이 있다) 방식도 최대한 제외했다. 애초에 만들기도 불편하고 코드 포매터를 돌렸다간 다 깨지기 십상이다.&lt;/p&gt;
&lt;h2 id="가운데-정렬의-분류"&gt;가운데 정렬의 분류&lt;/h2&gt;
&lt;p&gt;무슨 분류까지 필요한가 생각할 수 있지만 부모자식의 속성과 브라우저 지원범위에 따라 3분카레 수준의 난이도가 될 수도, 대충 땜빵으로 가운데 정렬처럼 보이게 해야 하는 수준이 될 수도 있다. 일반적으로는 부모와 자식의 &lt;code class="short-code"&gt;display&lt;/code&gt; 속성에 따라 분류한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;부모가 &lt;code class="short-code"&gt;block&lt;/code&gt;/&lt;code class="short-code"&gt;inline-block&lt;/code&gt;일 때&lt;ul&gt;
&lt;li&gt;자식이 &lt;code class="short-code"&gt;inline&lt;/code&gt;/&lt;code class="short-code"&gt;inline-block&lt;/code&gt;일 때&lt;/li&gt;
&lt;li&gt;자식이 inline 계열이 아닐 때&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;부모가 &lt;code class="short-code"&gt;flex&lt;/code&gt;일 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 중 자식 태그가 &lt;code class="short-code"&gt;inline&lt;/code&gt; 혹은 &lt;code class="short-code"&gt;inline-block&lt;/code&gt;인 경우 모든 브라우저에서 &lt;code class="short-code"&gt;text-align&lt;/code&gt;을 이용해 가운데 정렬이 가능하므로, 그렇지 않은 케이스에 대해서 알아보도록 하자.&lt;/p&gt;
&lt;h2 id="ie-6-이상"&gt;IE 6 이상&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://ko.wikipedia.org/wiki/%EC%9D%B8%ED%84%B0%EB%84%B7_%EC%9D%B5%EC%8A%A4%ED%94%8C%EB%A1%9C%EB%9F%AC_6" title="null" rel="noopener" target="_blank"&gt;위키백과에 따르면&lt;/a&gt; IE 6는 윈도우 XP에 기본 탑재되는 브라우저로 2001년에 출시되었다. 라떼를 한 번 시전해 보자면 나는 그 때 초등학교에 막 입학했었는데, 그때도 네이버나 다음같은 유명한 웹 서비스들이 있었으니 당시에도 가운데 정렬은 많은 웹개발자들의 골치를 썩이고 있었을 것이다.&lt;/p&gt;
&lt;p&gt;재미있게도 이 정렬법은 요즘에도 가끔 사용한다. 무슨 고릿적 기술을 지금도 쓰느냐 하겠지만 왜 네이버는 아직도 &lt;a href="https://tools.naver.com/service/internet-explorer/index.nhn" title="null" rel="noopener" target="_blank"&gt;네이버 최적화 버전 IE&lt;/a&gt;를 제공하지 않는가. 여전히 구형 IE를 사용하는 유저들의 수요가 있고 이 사람들의 접근을 막을 때 가운데 정렬을 사용해야 한다면 채택해볼 법하다. 나 같은 경우엔 IE 지원종료 페이지를 만들 때 썼는데, 유저가 어떤 브라우저로 들어올 지 모르므로 최대한 보수적으로 작업했다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/ie-support.png" alt="다음카페 IE 8 이하 지원 종료 페이지. IE 6까지 대응한다."&gt;&lt;figcaption aria-hidden="true"&gt;다음카페 IE 8 이하 지원 종료 페이지. IE 6까지 대응한다.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;IE 6의 특징이라면 가운데 정렬에 자식의 너비/높이값을 사용한다는 것이다. Sass나 LESS를 사용하는 서비스라면 값을 변수에 저장해 두고 사용하는 방식이 유용할 수 있겠다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-selector-class"&gt;.parent&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;position&lt;/span&gt;: relative;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-selector-class"&gt;.child&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;position&lt;/span&gt;: absolute;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;top&lt;/span&gt;: &lt;span class="hljs-number"&gt;50%&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;left&lt;/span&gt;: &lt;span class="hljs-number"&gt;50%&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;width&lt;/span&gt;: &lt;span class="hljs-number"&gt;120px&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;height&lt;/span&gt;: &lt;span class="hljs-number"&gt;100px&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;margin-top&lt;/span&gt;: -&lt;span class="hljs-number"&gt;60px&lt;/span&gt;; &lt;span class="hljs-comment"&gt;/* -width/2 */&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;margin-left&lt;/span&gt;: -&lt;span class="hljs-number"&gt;50px&lt;/span&gt;; &lt;span class="hljs-comment"&gt;/* -height/2 */&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;13&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;보면 알겠지만, 자식 컴포넌트의 크기를 수정하면 반드시 마진을 다시 계산해야 하고 &lt;code class="short-code"&gt;position&lt;/code&gt; 값을 강제하는 등 번거로운 점이 많다.&lt;/p&gt;
&lt;p&gt;단, 세로 정렬이 필요없는 경우 깔끔하게 정렬하는 방법이 있다. 이 방법은 &lt;code class="short-code"&gt;flex&lt;/code&gt;가 평정한 지금도 단일 최상단 컨테이너를 가운데 정렬할 때 많이 쓴다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-selector-class"&gt;.child&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;width&lt;/span&gt;: &lt;span class="hljs-number"&gt;120px&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;height&lt;/span&gt;: &lt;span class="hljs-number"&gt;100px&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;margin&lt;/span&gt;: &lt;span class="hljs-number"&gt;0&lt;/span&gt; auto;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h2 id="ie-8-이상"&gt;IE 8 이상&lt;/h2&gt;
&lt;p&gt;IE 8부터 &lt;code class="short-code"&gt;display: table&lt;/code&gt; 속성이 지원된다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-selector-class"&gt;.parent&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;display&lt;/span&gt;: table;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;width&lt;/span&gt;: &lt;span class="hljs-number"&gt;100%&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;height&lt;/span&gt;: &lt;span class="hljs-number"&gt;100%&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-selector-class"&gt;.child&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;display&lt;/span&gt;: table-cell;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;text-align&lt;/span&gt;: center;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;vertical-align&lt;/span&gt;: middle;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;이전 버전과의 차이를 설명하자면 일단 &lt;code class="short-code"&gt;table&lt;/code&gt;은 기본적으로 너비값이 지정되어있지 않다(특별히 세팅하지 않은 &lt;code class="short-code"&gt;block&lt;/code&gt;의 너비 초기값은 &lt;code class="short-code"&gt;100%&lt;/code&gt;이다). 따라서 부모 컴포넌트에도 너비/높이값을 지정해 주어야 한다. 풀스크린인 경우 &lt;code class="short-code"&gt;html&amp;gt;body&amp;gt;...&amp;gt;.parent&lt;/code&gt;로 이어지는 모든 태그에 높이를 &lt;code class="short-code"&gt;100%&lt;/code&gt;로 지정해 줘야 하기 때문에 조금 귀찮다.&lt;/p&gt;
&lt;p&gt;또, &lt;code class="short-code"&gt;table-cell&lt;/code&gt;은 길이를 지정할 수 없으므로, 만약 자식 컴포넌트에 반드시 길이를 지정해야 한다면 손자 컴포넌트에 길이값을 추가하는 방식으로 회피해야 한다.&lt;/p&gt;
&lt;h2 id="ie-9-이상"&gt;IE 9 이상&lt;/h2&gt;
&lt;p&gt;IE 9부터 &lt;code class="short-code"&gt;transform&lt;/code&gt;이 일부 지원된다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-selector-class"&gt;.parent&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;position&lt;/span&gt;: relative;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-selector-class"&gt;.child&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;position&lt;/span&gt;: absolute;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;top&lt;/span&gt;: &lt;span class="hljs-number"&gt;50%&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;left&lt;/span&gt;: &lt;span class="hljs-number"&gt;50%&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;width&lt;/span&gt;: &lt;span class="hljs-number"&gt;120px&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;height&lt;/span&gt;: &lt;span class="hljs-number"&gt;100px&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;transform&lt;/span&gt;: &lt;span class="hljs-built_in"&gt;tranlate&lt;/span&gt;(-&lt;span class="hljs-number"&gt;50%&lt;/span&gt;, -&lt;span class="hljs-number"&gt;50%&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;IE 6 버전과 거의 차이가 없고 역마진을 &lt;code class="short-code"&gt;transform&lt;/code&gt;으로 대체한 것이다. 너비와 높이에 영향을 받지 않기 때문에 유지보수성이 늘어난다. 여타 다른 장단점은 IE 6와 동일하다.&lt;/p&gt;
&lt;h2 id="ie-11"&gt;IE 11&lt;/h2&gt;
&lt;p&gt;&lt;code class="short-code"&gt;flex&lt;/code&gt;가 지원된다. 유저가 구형 브라우저를 사용하지 않는다면 더할나위 없는 해결책이다. 모바일 전용 페이지에서는 거의 걱정없이 사용해도 된다(젤리빈 이하에서는 깨지긴 하지만, 카카오톡도 롤리팝 미만 OS 지원을 끊은 마당에...).&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-selector-class"&gt;.parent&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;display&lt;/span&gt;: flex;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;flex-direction&lt;/span&gt;: column; &lt;span class="hljs-comment"&gt;/* row일 땐 align-items와 justify-content가 바뀜 */&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;align-items&lt;/span&gt;: center;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attribute"&gt;justify-content&lt;/span&gt;: center;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;6&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code class="short-code"&gt;.child&lt;/code&gt;가 정의되지 않는다. 이게 무슨 의미를 가지냐면, &lt;code class="short-code"&gt;position&lt;/code&gt;을 잘못 주지 않는 한 자식 컴포넌트가 그 어떤 속성을 갖고 있더라도 가운데 정렬을 보장한다는 것이다. &lt;code class="short-code"&gt;flex&lt;/code&gt;는 앞서 설명한 정렬 방법 중 손자 컴포넌트 없이 가운데 정렬한 상태로 자식 컴포넌트를 여러 개 쌓을 수 있는 유일한 방식이다. 정렬 옵션도 다른 방식에 비해 월등히 많다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;&amp;lt;!-- 이런 구조일 때 가로세로 정렬이 모두 깨지지 않는 건 flex뿐이다 --&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;div&lt;/span&gt; &lt;span class="hljs-attr"&gt;class&lt;/span&gt;=&lt;span class="hljs-string"&gt;&amp;quot;parent&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;div&lt;/span&gt; &lt;span class="hljs-attr"&gt;class&lt;/span&gt;=&lt;span class="hljs-string"&gt;&amp;quot;child&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;div&lt;/span&gt; &lt;span class="hljs-attr"&gt;class&lt;/span&gt;=&lt;span class="hljs-string"&gt;&amp;quot;child&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;div&lt;/span&gt; &lt;span class="hljs-attr"&gt;class&lt;/span&gt;=&lt;span class="hljs-string"&gt;&amp;quot;child&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;&amp;lt;!-- 다른 방식은 이렇게 자식을 하나만 써야 한다 --&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;div&lt;/span&gt; &lt;span class="hljs-attr"&gt;class&lt;/span&gt;=&lt;span class="hljs-string"&gt;&amp;quot;parent&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;div&lt;/span&gt; &lt;span class="hljs-attr"&gt;class&lt;/span&gt;=&lt;span class="hljs-string"&gt;&amp;quot;child&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;div&lt;/span&gt; &lt;span class="hljs-attr"&gt;class&lt;/span&gt;=&lt;span class="hljs-string"&gt;&amp;quot;grandchild&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;div&lt;/span&gt; &lt;span class="hljs-attr"&gt;class&lt;/span&gt;=&lt;span class="hljs-string"&gt;&amp;quot;grandchild&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;13&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;div&lt;/span&gt; &lt;span class="hljs-attr"&gt;class&lt;/span&gt;=&lt;span class="hljs-string"&gt;&amp;quot;grandchild&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;14&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;15&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h2 id="요약"&gt;요약&lt;/h2&gt;
&lt;p&gt;브라우저에 따라 방법도 다양하고 각 방법마다 제약도 각각 다르다보니 이걸 전부 외워 쓰기는 힘들다. 다행히 이런 걸 알아서 세팅해주는 사이트가 하나 있다.&lt;/p&gt;
&lt;p&gt;&lt;a href="http://howtocenterincss.com/" title="undefined" rel="noopener" target="_blank"&gt;http://howtocenterincss.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;지원하는 브라우저에 따라 적당한 값을 설정하면 알아서 html 코드를 생성해주는 고마운 사이트이다. 방법마다 어떤 장단이 있는지만 가볍게 알아두고 코드는 알아서 만들어 주는 것을 갖다 쓰면 편하다.&lt;/p&gt;
]]&gt;</description><author>&lt;![CDATA[Kim Songhyun]]&gt;</author><pubDate>2025-02-05</pubDate></item><item><title>&lt;![CDATA[Node.js로 색깔 있는 콘솔 찍기]]&gt;</title><link>https://znagadeon.dev/post/console-log-style/</link><description>&lt;![CDATA[&lt;p&gt;인터넷을 돌아다니다 보면 &lt;code class="short-code"&gt;console.log&lt;/code&gt;를 이용해 예술을 하는 사람들을 가끔 만날 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/coupang-console.png" alt="쿠팡의 콘솔. 이쪽은 아스키 아트도 추가했다"&gt;&lt;figcaption aria-hidden="true"&gt;쿠팡의 콘솔. 이쪽은 아스키 아트도 추가했다&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;브라우저 콘솔은 CSS 스타일시트를 지원하기 때문에 조금 번거롭긴 하지만 이런 작품을 만들기 어렵지 않다. 예를 들어 매트릭스 스타일의 녹색 글씨를 찍고 싶다면 이렇게 하면 된다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;console&lt;/span&gt;.log(&lt;span class="hljs-string"&gt;&amp;#x27;%cHello, world!&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;#x27;color: #a4f644; background-color: #000;&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code class="short-code"&gt;%c&lt;/code&gt; 이후의 글자에는 모두 같은 스타일이 입혀지게 된다. 새로운 스타일을 씌우고 싶으면 &lt;code class="short-code"&gt;%c&lt;/code&gt;를 또 쓰면 된다.&lt;/p&gt;
&lt;p&gt;FE에서는 전혀 어려운 작업이 아니기 때문에 당연히 BE에서도 CSS까진 아니더라도 비슷한 방법론을 제공한다고 생각했는데, 알고 보니 전혀 아니었다. 생각해보면 당연한 일이다. 터미널에서는 색깔을 변경하기 위해서 &lt;a href="https://en.wikipedia.org/wiki/ANSI_escape_code" title="null" rel="noopener" target="_blank"&gt;ANSI escape code&lt;/a&gt;를 사용하기 때문이다. 심지어 이조차도 윈도에서는 10 버전 이후에나 정식으로 지원하기 시작했다.&lt;/p&gt;
&lt;p&gt;바퀴를 새로 만드는 데 관심이 없다면 이를 추상화한 구현체를 갖다 쓰면 된다. Node.js 진영에서는 &lt;a href="https://www.npmjs.com/package/chalk" title="null" rel="noopener" target="_blank"&gt;chalk&lt;/a&gt;가 사실상 표준 역할을 하고 있다. 최근 주간 다운로드 수가 무려 4400만 건! &lt;del&gt;부럽다&lt;/del&gt; 다만, 라이브러리의 힘을 빌려도 좋지만 생각보다 원리는 간단하므로 직접 만들어보는 것도 좋겠다.&lt;/p&gt;
&lt;p&gt;일단 ANSI escape code는 글자색 뿐 아니라 커서의 위치, 굵기, 배경색 등 터미널 전체에서 커서와 텍스트로 할 수 있는 모든 것을 다룰 수 있다. 한 위치에서 프로그레스 바를 계속 그려준다든지 하는 것도 이 이스케이프 코드를 이용해서 작업하는 것이다. 이것도 직접 만들어 보면 재밌겠지만 일단 글자를 자유롭게 포매팅하는 것부터 시작해 보자.&lt;/p&gt;
&lt;p&gt;일단 아래 예제를 보자.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;console&lt;/span&gt;.log(&lt;span class="hljs-string"&gt;&amp;#x27;\x1b[31mHello, world!\x1b[0m&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;여러분이 어떤 컬러스킴을 터미널에 적용했느냐에 따라 다르겠지만 빨간 색으로 헬로월드를 찍을 수 있었을 것이다.&lt;/p&gt;
&lt;p&gt;이 코드는 크게 세 부분으로 나눌 수 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code class="short-code"&gt;\x1b[31m&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class="short-code"&gt;Hello, world!&lt;/code&gt;: 실제로 내가 쓰고 싶었던 부분&lt;/li&gt;
&lt;li&gt;&lt;code class="short-code"&gt;\x1b[0m&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code class="short-code"&gt;\x1b[&lt;/code&gt;로 시작해서 &lt;code class="short-code"&gt;m&lt;/code&gt;으로 끝나는 코드가 바로 ANSI escape code이다. &lt;code class="short-code"&gt;\x1b&lt;/code&gt;는 16진수 1B, 즉 27인데, 아스키코드표에서 ESC가 27이기 때문에 이 값을 사용한다(escape code라는 이름이 붙은 건 이것 때문인 듯하다). 8진수인 &lt;code class="short-code"&gt;\033&lt;/code&gt;을 사용하기도 하는데 이건 나는 개인적으로 16진수로 적는 쪽을 선호한다.&lt;/p&gt;
&lt;p&gt;&lt;code class="short-code"&gt;[&lt;/code&gt;는 Control Sequence Introducer(CSI)라고 부르는데, 이 뒤에 오는 코드에 따라 커서를 움직인다든지 하는 기능을 수행할 수 있다. 맨 뒤에 오는 &lt;code class="short-code"&gt;m&lt;/code&gt;은 Select Graphic Rendition(SGR)이라고 부르고, &lt;code class="short-code"&gt;m&lt;/code&gt; 뒤에 오는 글자들에 각종 스타일을 제공한다.&lt;/p&gt;
&lt;p&gt;CSI와 SGR 사이에 있는 숫자 &lt;code class="short-code"&gt;31&lt;/code&gt;과 &lt;code class="short-code"&gt;0&lt;/code&gt;이 색상을 골라 준다. 31은 글자색을 빨간색으로 바꾼다는 뜻이고, 0은 지금까지 있던 설정을 리셋한다는 뜻으로, 더 이상 빨간 글씨가 찍히지 않도록 해 준다. 0은 생략하여 &lt;code class="short-code"&gt;\x1b[m&lt;/code&gt;이라고 적을 수 있다.&lt;/p&gt;
&lt;p&gt;글자색 말고도 다른 값을 줄 수도 있다. 이를테면 숫자 1은 굵은 글씨, 3은 기울임꼴이며 30~37, 90~97은 글자색, 40~47, 100~107은 배경색을 바꿔준다. 이렇게 상호 배타적인 설정은 세미콜론을 이용해 동시에 적용할 수 있다. 예를 들어 &lt;code class="short-code"&gt;\x1b[1;3;33;94m&lt;/code&gt;은 굵게(1) 기울임꼴(3) 노란색 글자(33)에 하늘색 배경(94)라는 뜻이다.&lt;/p&gt;
&lt;p&gt;정리하자면, 내가 작성하고 싶은 글자 앞뒤로 이스케이프 코드를 적어주면 Node.js로도 멋진 콘솔창을 만들 수 있다는 것이다. &lt;a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates" title="null" rel="noopener" target="_blank"&gt;Tagged template literal&lt;/a&gt;을 이용해 빨간 글자 로그를 찍는 방법을 소개하며 이번 글을 마치도록 하겠다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; red = &lt;span class="hljs-function"&gt;(&lt;span class="hljs-params"&gt;strs, ...values&lt;/span&gt;) =&amp;gt;&lt;/span&gt; { &lt;span class="hljs-comment"&gt;// values는 없다고 가정&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-string"&gt;`\x1b[31m&lt;span class="hljs-subst"&gt;${strs.join(&lt;span class="hljs-string"&gt;&amp;#x27;&amp;#x27;&lt;/span&gt;)}&lt;/span&gt;\x1b[0m`&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;console&lt;/span&gt;.log(red&lt;span class="hljs-string"&gt;`Hello, world!`&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;]]&gt;</description><author>&lt;![CDATA[Kim Songhyun]]&gt;</author><pubDate>2025-02-05</pubDate></item><item><title>&lt;![CDATA[나만의 웹팩 로더 만들기]]&gt;</title><link>https://znagadeon.dev/post/create-own-webpack-loader/</link><description>&lt;![CDATA[&lt;p&gt;도메인 값이 아까워서 블로그를 활성화시켜야겠다는 연초의 각오가 무색하게 또 블로그를 몇 달 묵혔다. 글감은 계속 쌓이는데 마음 잡고 글 쓸 일이 없던 찰나 지인의 소개로 &lt;a href="https://www.notion.so/ac5b18a482fb4df497d4e8257ad4d516" title="null" rel="noopener" target="_blank"&gt;글또&lt;/a&gt;라는 프로젝트를 알게 되어 5기로 참가하게 되었다. 회사 일이 크게 바쁘지 않으면 아마 2주에 한 번 꼴로 6달 정도 글을 발행하게 될 것이다.&lt;/p&gt;
&lt;p&gt;첫 글을 무엇으로 쓸까 고민하다 올해 사내에서 발표했던 웹팩 로더 튜토리얼에 생각이 미쳤다. 발표가 실습 방식이었기 때문에 이 글도 실습을 하면서 따라할 수 있도록 &lt;a href="https://github.com/znagadeon/webpack-loader-tutorial" title="null" rel="noopener" target="_blank"&gt;소스코드 전문&lt;/a&gt;을 준비했다. 2020년 4월에 진행한 실습인 관계로 웹팩 버전은 4이다. 또, 발표를 위해 최대한 단순화한 소스코드를 준비한 것이므로 이 코드를 그대로 현업이나 사이드 프로젝트에 사용하려면 몇 가지 추가 예외처리가 필요할 것이다. 실습에 참고하길 바란다.&lt;/p&gt;
&lt;h2 id="로더란"&gt;로더란?&lt;/h2&gt;
&lt;p&gt;ES6+, 혹은 타입스크립트가 프론트엔드 개발의 대세가 된 이후로 소스 번들러는 사실상 웹 개발에 반 필수가 되었다. 사이드 프로젝트에는 parcel처럼 zero-config를 지향하는 번들러도 많이 사용하지만, 현업이란 것이 항상 깔끔하고 아름답지만은 않기 때문에 이런저런 추가 설정을 붙이다 보면 아마 웹팩을 주로 사용하게 될 것이다.&lt;/p&gt;
&lt;p&gt;다른 번들러와 차별화되는 웹팩의 특징을 꼽자면 역시 &lt;strong&gt;로더(loader)&lt;/strong&gt; 다. 웹팩 설정 파일을 읽어본 적이 있다면 아마 이런 내용을 한 번쯤 봤을 것이다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;module&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;rules&lt;/span&gt;: [&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;            {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;                &lt;span class="hljs-attr"&gt;test&lt;/span&gt;: &lt;span class="hljs-regexp"&gt;/\.js$/&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;                loader: &lt;span class="hljs-string"&gt;&amp;#x27;babel-loader&amp;#x27;&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;            },&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;        ],&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;    },&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;저 파일 확장자같이 생긴 건 뭐고 &lt;code class="short-code"&gt;babel-loader&lt;/code&gt;는 또 무엇인가? 웹팩은 다양한 확장자의 파일들을 받아서 웹이 이해할 수 있는 포맷으로 바꾸어주는 역할을 수행한다. 다만 웹팩 자체에는 이런 능력이 없다. 그렇다면 지금까지 우리가 사용해 왔던 최신 언어들은 어떻게 알아서 잘 js 파일들로 바뀌었을까? 바로 이걸 해 주는 것이 로더이다. 로더는 &lt;strong&gt;파일의 이름을 기반으로 하여 해당 파일의 내용을 브라우저가 이해할 수 있는 언어, 정확히 말하자면 js 소스로 바꾸어 주는 역할&lt;/strong&gt; 을 수행한다.&lt;/p&gt;
&lt;p&gt;방금 설명한 로더의 역할을 좀 더 자세히 곱씹어보자.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;파일의 내용을 js 소스로 바꾸어준다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이걸 프로그래머식으로 해석하면 이렇게 된다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;문자열을 입력으로 받아 js 포맷 문자열을 반환한다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;즉, 로더는 문자열을 각각 입력과 출력으로 갖는 함수의 형태를 갖는다. 나중에 설명하겠지만, 로더가 가지고 있는 유틸 함수에 &lt;code class="short-code"&gt;this&lt;/code&gt;로 접근하기 때문에 arrow function은 사용하면 안 된다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;function&lt;/span&gt; (&lt;span class="hljs-params"&gt;source&lt;/span&gt;) &lt;/span&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;#x27;&amp;#x27;&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code class="short-code"&gt;source&lt;/code&gt;는 파일의 내용물을 문자열로 바꾼 것이고, 이 값을 적당히 주물러서 반환해주면 된다. 반환되는 문자열은 자바스크립트 엔진으로 해석할 수만 있다면 무엇이든 상관없다. 지금처럼 빈 문자열을 반환해도 상관없고, &lt;code class="short-code"&gt;console.log(&amp;#39;Hello, world!&amp;#39;);&lt;/code&gt; 같은 것도 된다. 하지만 보통 받아온 &lt;code class="short-code"&gt;source&lt;/code&gt;를 기반으로 뭔가를 쓸 수 있게 해 줘야 하기 때문에, 대개는 이런 형태를 띤다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;function&lt;/span&gt; (&lt;span class="hljs-params"&gt;source&lt;/span&gt;) &lt;/span&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-string"&gt;`module.exports = \`&lt;span class="hljs-subst"&gt;${doSomething(source)}&lt;/span&gt;\``&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;여기까지 알았다면 일단 로더를 작성하기 위해 필요한 최소한의 지식은 알게 된 것이다.&lt;/p&gt;
&lt;h2 id="로더-만들어-보기"&gt;로더 만들어 보기&lt;/h2&gt;
&lt;p&gt;이제부터 진짜로 로더를 만들어 보자. 우리가 이 글에서 만들어 볼 로더는 마크다운 로더로, 마크다운 파일을 받아 js에서 사용할 수 있는 포맷으로 변환해주는 것이다.&lt;/p&gt;
&lt;p&gt;시작하기 전에 미리 웹팩 설정 파일을 작성해야 한다. 요새는 페이즈별로 설정 파일을 따로 작성하는 게 일반적이지만, 우리는 여기서 로더를 만드는 것이지 설정 파일에 대해 다루는 것이 아니므로 설정 파일은 하나만 사용하도록 하자.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;// webpack.config.js&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;module&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;rules&lt;/span&gt;: [&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;            {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;                &lt;span class="hljs-attr"&gt;test&lt;/span&gt;: &lt;span class="hljs-regexp"&gt;/\.md$/&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;                loader: &lt;span class="hljs-string"&gt;&amp;#x27;./loaders/md-loader.js&amp;#x27;&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;            },&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;        ],&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;    },&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;13&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;// src/main.js&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; html = &lt;span class="hljs-built_in"&gt;require&lt;/span&gt;(&lt;span class="hljs-string"&gt;&amp;#x27;./sample.md&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;document&lt;/span&gt;.write(html);&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;내용은 간단하다. &lt;code class="short-code"&gt;sample.md&lt;/code&gt;의 내용을 불러와서 화면에 그려주면 된다. 변수명이 &lt;code class="short-code"&gt;html&lt;/code&gt;인 데서 눈치챘겠지만 단순히 텍스트를 불러올 뿐 아니라 이를 html 포맷으로 변환해주는 작업도 진행할 것이다. 이전 문단에서 작성했던 코드를 인용하자면 &lt;code class="short-code"&gt;doSomething&lt;/code&gt; 부분을 마크다운 컨버팅 함수로 바꿔주면 된다. 여기서는 &lt;code class="short-code"&gt;marked&lt;/code&gt;라고 하는 잘 만들어진 마크다운 컨버터를 사용하겠다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; marked = &lt;span class="hljs-built_in"&gt;require&lt;/span&gt;(&lt;span class="hljs-string"&gt;&amp;#x27;marked&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;function&lt;/span&gt; (&lt;span class="hljs-params"&gt;source&lt;/span&gt;) &lt;/span&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-string"&gt;`module.exports = \`&lt;span class="hljs-subst"&gt;${marked(source)}&lt;/span&gt;\``&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;이렇게 작성한 뒤 웹팩을 실행하면 원하는 결과를 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/loader-result-1.png" alt=""&gt;&lt;figcaption aria-hidden="true"&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;h3 id="option-사용하기"&gt;option 사용하기&lt;/h3&gt;
&lt;p&gt;위에서 사용한 &lt;code class="short-code"&gt;marked&lt;/code&gt;라는 라이브러리는 커스텀 옵션의 지원이 깔끔한 편이다. 예를 들어 Gitub-Flavored Markdown을 사용할지 여부 등을 옵션으로 받을 수 있다. 이 옵션을 로더를 사용하는 유저가 자율적으로 선택하게 하고 싶다면 어떻게 해야 할까? 로더는 웹팩으로부터 옵션을 받아 이 값을 함수를 실행하는 과정에서 사용할 수 있다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;// webpack.config.js&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;module&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;rules&lt;/span&gt;: [{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-attr"&gt;test&lt;/span&gt;: &lt;span class="hljs-regexp"&gt;/\.md$/&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;            loader: &lt;span class="hljs-string"&gt;&amp;#x27;./loaders/md-loader.js&amp;#x27;&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-attr"&gt;options&lt;/span&gt;: { &lt;span class="hljs-attr"&gt;gfm&lt;/span&gt;: &lt;span class="hljs-literal"&gt;false&lt;/span&gt; },&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;        }],&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;    },&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; marked = &lt;span class="hljs-built_in"&gt;require&lt;/span&gt;(&lt;span class="hljs-string"&gt;&amp;#x27;marked&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;function&lt;/span&gt; (&lt;span class="hljs-params"&gt;source&lt;/span&gt;) &lt;/span&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-string"&gt;`module.exports = \`&lt;span class="hljs-subst"&gt;${marked(source, &lt;span class="hljs-built_in"&gt;this&lt;/span&gt;.query)}&lt;/span&gt;\``&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/loader-result-2.png" alt=""&gt;&lt;figcaption aria-hidden="true"&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;code class="short-code"&gt;gfm&lt;/code&gt; 옵션을 껐더니 로더가 테이블을 렌더링하지 않도록 바뀌었다.&lt;/p&gt;
&lt;h3 id="md-loader-분해하기"&gt;md-loader 분해하기&lt;/h3&gt;
&lt;p&gt;사실 마크다운 로더 자체는 분해가 필요하지 않을 정도로 구조가 간단한 편이다. 1) 마크다운을 html로 변환해서 2) 그걸 js 문자열로 반환해주면 되기 때문이다. 하지만 어쨌든 마크다운 로더가 두 가지 일을 하는 것은 분명해 보인다. 실습도 해 볼 겸 각각의 기능을 따로 로더로 만들어 보자. 1번 로더를 &lt;code class="short-code"&gt;md-loader&lt;/code&gt;로, 2번 로더를 &lt;code class="short-code"&gt;html-loader&lt;/code&gt;라고 부를 수 있겠다.&lt;/p&gt;
&lt;p&gt;두 개 이상의 로더를 연속해서 사용할 때 웹팩에서는 &lt;code class="short-code"&gt;use&lt;/code&gt; 옵션을 사용한다. 순서는 오른쪽에서 왼쪽, 혹은 아래에서 위로 진행한다. 예를 들어 옵션을 아래와 같이 작성한다면 &lt;code class="short-code"&gt;md-loader&lt;/code&gt; 다음에 &lt;code class="short-code"&gt;html-loader&lt;/code&gt;가 실행된다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;// webpack.config.js&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;module&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;rules&lt;/span&gt;: [{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-attr"&gt;test&lt;/span&gt;: &lt;span class="hljs-regexp"&gt;/\.md$/&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;            use: [&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;                &lt;span class="hljs-string"&gt;&amp;#x27;./loaders/html-loader.js&amp;#x27;&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;                {&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;                    &lt;span class="hljs-attr"&gt;loader&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;#x27;./loaders/md-loader.js&amp;#x27;&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;                    &lt;span class="hljs-attr"&gt;options&lt;/span&gt;: { &lt;span class="hljs-attr"&gt;gfm&lt;/span&gt;: &lt;span class="hljs-literal"&gt;false&lt;/span&gt; },&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;                },&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;13&lt;/code&gt;&lt;code class="code"&gt;            ]&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;14&lt;/code&gt;&lt;code class="code"&gt;        }],&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;15&lt;/code&gt;&lt;code class="code"&gt;    },&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;16&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;17&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;로더를 연속해서 사용할 때 기존과 다른 점은 더이상 입출력값을 항상 로더가 요구하는 형식으로 고정하지 않아도 된다는 점이다. 함수 여러개를 합성한다고 생각하면 편하다. 위 설정 파일을 예시로 사용한다면 &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;mi&gt;e&lt;/mi&gt;&lt;mi&gt;w&lt;/mi&gt;&lt;mi&gt;L&lt;/mi&gt;&lt;mi&gt;o&lt;/mi&gt;&lt;mi&gt;a&lt;/mi&gt;&lt;mi&gt;d&lt;/mi&gt;&lt;mi&gt;e&lt;/mi&gt;&lt;mi&gt;r&lt;/mi&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mi&gt;h&lt;/mi&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;mi&gt;m&lt;/mi&gt;&lt;mi&gt;l&lt;/mi&gt;&lt;mi&gt;L&lt;/mi&gt;&lt;mi&gt;o&lt;/mi&gt;&lt;mi&gt;a&lt;/mi&gt;&lt;mi&gt;d&lt;/mi&gt;&lt;mi&gt;e&lt;/mi&gt;&lt;mi&gt;r&lt;/mi&gt;&lt;mo&gt;⋅&lt;/mo&gt;&lt;mi&gt;m&lt;/mi&gt;&lt;mi&gt;d&lt;/mi&gt;&lt;mi&gt;L&lt;/mi&gt;&lt;mi&gt;o&lt;/mi&gt;&lt;mi&gt;a&lt;/mi&gt;&lt;mi&gt;d&lt;/mi&gt;&lt;mi&gt;e&lt;/mi&gt;&lt;mi&gt;r&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;newLoader = htmlLoader \cdot mdLoader&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.02691em;"&gt;w&lt;/span&gt;&lt;span class="mord mathnormal"&gt;L&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.02778em;"&gt;er&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;h&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mord mathnormal"&gt;m&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.01968em;"&gt;l&lt;/span&gt;&lt;span class="mord mathnormal"&gt;L&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.02778em;"&gt;er&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;⋅&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;m&lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal"&gt;L&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.02778em;"&gt;er&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;이고, &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;mi&gt;e&lt;/mi&gt;&lt;mi&gt;w&lt;/mi&gt;&lt;mi&gt;L&lt;/mi&gt;&lt;mi&gt;o&lt;/mi&gt;&lt;mi&gt;a&lt;/mi&gt;&lt;mi&gt;d&lt;/mi&gt;&lt;mi&gt;e&lt;/mi&gt;&lt;mi&gt;r&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;newLoader&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.02691em;"&gt;w&lt;/span&gt;&lt;span class="mord mathnormal"&gt;L&lt;/span&gt;&lt;span class="mord mathnormal"&gt;o&lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.02778em;"&gt;er&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;만 문자열 입력/js 출력이 되도록 하면 되는 것이다. &lt;code class="short-code"&gt;md-loader&lt;/code&gt;의 출력을 js가 아닌 html로 바꿔 보자. 아까와 똑같은 결과가 나온다면 성공이다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;// loaders/md-loader.js&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; marked = &lt;span class="hljs-built_in"&gt;require&lt;/span&gt;(&lt;span class="hljs-string"&gt;&amp;#x27;marked&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;function&lt;/span&gt; (&lt;span class="hljs-params"&gt;source&lt;/span&gt;) &lt;/span&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; marked(source, &lt;span class="hljs-built_in"&gt;this&lt;/span&gt;.query);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;6&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;// loaders/html-loader.js&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;function&lt;/span&gt; (&lt;span class="hljs-params"&gt;source&lt;/span&gt;) &lt;/span&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-string"&gt;`module.exports = \`&lt;span class="hljs-subst"&gt;${source}&lt;/span&gt;\``&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h3 id="파일-저장하기"&gt;파일 저장하기&lt;/h3&gt;
&lt;p&gt;마크다운으로 글을 써 본 적이 있다면 알겠지만 마크다운에는 글 뿐 아니라 이미지도 넣을 수 있다. 하지만 지금까지 만든 로더로는 이미지를 가져올 수 없다! 설령 이미지를 넣더라도 깨진 이미지가 뜰 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/image-load-failed.png" alt=""&gt;&lt;figcaption aria-hidden="true"&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;이렇게 로더가 실행되는 도중 추가적인 파일을 불러오는 경우가 있기 때문에 로더 API에는 파일을 내보내주는 함수가 내장되어 있다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;// loaders/html-loader.js&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;function&lt;/span&gt; (&lt;span class="hljs-params"&gt;source&lt;/span&gt;) &lt;/span&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; images = getImagePaths(source);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;    images.forEach(&lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;image&lt;/span&gt; =&amp;gt;&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-built_in"&gt;this&lt;/span&gt;.emitFile(&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;            image,&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-built_in"&gt;this&lt;/span&gt;.fs.readFileSync(path.join(&lt;span class="hljs-built_in"&gt;this&lt;/span&gt;.resourcePath, &lt;span class="hljs-string"&gt;&amp;#x27;..&amp;#x27;&lt;/span&gt;, image)),&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;        );&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;    });&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-string"&gt;`module.exports = \`&lt;span class="hljs-subst"&gt;${source}&lt;/span&gt;\``&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;13&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;이렇게 하면 정상적으로 이미지를 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/image-load-success.png" alt=""&gt;&lt;figcaption aria-hidden="true"&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;h4 id="다른-로더-트리거하기"&gt;다른 로더 트리거하기&lt;/h4&gt;
&lt;p&gt;이런 식으로 직접 파일 핸들링을 해도 되지만, 기성(?) 라이브러리 중 &lt;code class="short-code"&gt;file-loader&lt;/code&gt;라는 좋은 라이브러리가 있으니 그걸 활용해보는 것도 좋을 것 같다. &lt;code class="short-code"&gt;module.exports&lt;/code&gt;를 사용했던 것과는 반대로 다른 로더를 트리거하고 싶을 때는 &lt;code class="short-code"&gt;require&lt;/code&gt;문을 사용한다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;// webpack.config.js&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;module&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;rules&lt;/span&gt;: [{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;        }, {&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-attr"&gt;test&lt;/span&gt;: &lt;span class="hljs-regexp"&gt;/\.(jpe?g|png|gif)$/&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;            loader: &lt;span class="hljs-string"&gt;&amp;#x27;file-loader&amp;#x27;&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-attr"&gt;options&lt;/span&gt;: { &lt;span class="hljs-attr"&gt;name&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;#x27;[name].[ext]&amp;#x27;&lt;/span&gt; },&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;        }],&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;    },&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;13&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;14&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;// loaders/html-loader.js&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;function&lt;/span&gt; (&lt;span class="hljs-params"&gt;source&lt;/span&gt;) &lt;/span&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; imageScripts = getImagePaths(source)&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;        .map(&lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;image&lt;/span&gt; =&amp;gt;&lt;/span&gt; &lt;span class="hljs-string"&gt;`require(&amp;#x27;&lt;span class="hljs-subst"&gt;${image}&lt;/span&gt;&amp;#x27;);`&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;6&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;7&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-string"&gt;`&lt;span class="hljs-subst"&gt;${imageScripts}&lt;/span&gt;module.exports = \`&lt;span class="hljs-subst"&gt;${source}&lt;/span&gt;\``&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;8&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h3 id="async"&gt;async&lt;/h3&gt;
&lt;p&gt;지금까지는 로더를 실행하면 비동기적인 실행 없이 바로 결과를 리턴해주는 함수만 작성했다. 하지만 자바스크립트 세계에서는 모든 코드가 항상 동기적으로 작동하지는 않는 법이다. 당장 아까 작성했던 로더에도 &lt;code class="short-code"&gt;readFileSync&lt;/code&gt;를 사용했는데, 이를 비동기 함수로 바꾼다면 로더가 제대로 동작하지 않을 것이다. 이런 경우에 대비해 로더 API에 &lt;code class="short-code"&gt;this.async()&lt;/code&gt;라는 함수가 존재한다.&lt;/p&gt;
&lt;p&gt;간단히 예시를 들어 보자. &lt;code class="short-code"&gt;mdlint-loader&lt;/code&gt;를 만들어 현재 마크다운 파일에 데드링크가 없는지 확인해볼 것이다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;// webpack.config.js&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;module&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;rules&lt;/span&gt;: [{&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-attr"&gt;enforce&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;#x27;pre&amp;#x27;&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-attr"&gt;test&lt;/span&gt;: &lt;span class="hljs-regexp"&gt;/\.md$/&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;            loader: &lt;span class="hljs-string"&gt;&amp;#x27;./loaders/mdlint-loader.js&amp;#x27;&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;        }, {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;        }],&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;    },&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;13&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;14&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;// loaders/mdlint-loader.js&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;function&lt;/span&gt; (&lt;span class="hljs-params"&gt;source&lt;/span&gt;) &lt;/span&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; callback = &lt;span class="hljs-built_in"&gt;this&lt;/span&gt;.async();&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; links = getLinks(source);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; promises = links.map(&lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;link&lt;/span&gt; =&amp;gt;&lt;/span&gt; axios({&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;method&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;#x27;GET&amp;#x27;&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;url&lt;/span&gt;: link,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;timeout&lt;/span&gt;: &lt;span class="hljs-number"&gt;300&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;    }));&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;13&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-built_in"&gt;Promise&lt;/span&gt;.all(promises).then(&lt;span class="hljs-function"&gt;() =&amp;gt;&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;14&lt;/code&gt;&lt;code class="code"&gt;        callback(&lt;span class="hljs-literal"&gt;null&lt;/span&gt;, source);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;15&lt;/code&gt;&lt;code class="code"&gt;    });&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;16&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;데드링크가 없으면 성공적으로 &lt;code class="short-code"&gt;callback&lt;/code&gt;을 실행할 것이다. &lt;code class="short-code"&gt;callback&lt;/code&gt;은 총 두 개의 인자를 받는데, 첫 인자로 &lt;code class="short-code"&gt;Error&lt;/code&gt;를, 두 번째 인자로 기존의 리턴값을 받는다. 만약 에러를 발생시킬 필요가 없다면 첫 번째 인자를 &lt;code class="short-code"&gt;null&lt;/code&gt;로 두면 된다. 그럼 이번에는 데드 링크가 있는 경우 예외를 발생시켜 보자.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;// loaders/mdlint-loader.js&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;function&lt;/span&gt; (&lt;span class="hljs-params"&gt;source&lt;/span&gt;) &lt;/span&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-built_in"&gt;Promise&lt;/span&gt;.all(promises).then(&lt;span class="hljs-function"&gt;() =&amp;gt;&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;        callback(&lt;span class="hljs-literal"&gt;null&lt;/span&gt;, source);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;    }).catch(&lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;err&lt;/span&gt; =&amp;gt;&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-comment"&gt;// warning을 뱉을 때&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-built_in"&gt;this&lt;/span&gt;.emitWarning(&lt;span class="hljs-keyword"&gt;new&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Error&lt;/span&gt;(&lt;span class="hljs-string"&gt;&amp;#x27;Dead link found!&amp;#x27;&lt;/span&gt;));&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;        callback(&lt;span class="hljs-literal"&gt;null&lt;/span&gt;, source);&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-comment"&gt;// error를 뱉을 때&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;13&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-built_in"&gt;this&lt;/span&gt;.emitError(&lt;span class="hljs-keyword"&gt;new&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Error&lt;/span&gt;(&lt;span class="hljs-string"&gt;&amp;#x27;Dead link found!&amp;#x27;&lt;/span&gt;));&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;14&lt;/code&gt;&lt;code class="code"&gt;        callback(err);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;15&lt;/code&gt;&lt;code class="code"&gt;    });&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;16&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;단순 경고처리만 필요하다면 첫 인자로 &lt;code class="short-code"&gt;null&lt;/code&gt;을, 에러를 발생시켜야 한다면 에러 객체를 전달하면 된다. 이제 마크다운 내부에 데드링크가 있다면 warning 혹은 error을 뱉게 만들 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/dead-link-found.png" alt=""&gt;&lt;figcaption aria-hidden="true"&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;h2 id="더-공부하고-싶다면"&gt;더 공부하고 싶다면&lt;/h2&gt;
&lt;p&gt;다시 한 번 말하지만 이 소스는 튜토리얼용으로 실습을 위해 제작한 것이고, 현업에서 바로 사용하기에는 무리가 있다(경우에 따라서는 내가 미처 몰랐던 안티패턴이 있을 수도 있겠다). 로더가 어떤 방식으로 동작하는지 이해하되, 실제로 로더를 직접 작성할 일이 생긴다면 이미 기존에 배포되어 있는 로더 소스나 공식 문서를 참고해가면서 작성하면 좋겠다. 개인적으로는 공식문서는 꼭 필요한 곳을 긁어주지 못해서(변수 타입이랑 이름은 있는데, &lt;em&gt;그래서 여기에 대체 무슨 값을 넣어야 되는데?&lt;/em&gt; 에 대한 고려가 부족한 느낌...) 기존 로더들을 더 많이 참고했던 것 같다.&lt;/p&gt;
&lt;h3 id="참고-링크"&gt;참고 링크&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://jeonghwan-kim.github.io/series/2019/12/10/frontend-dev-env-webpack-basic.html#3-%EB%A1%9C%EB%8D%94" title="null" rel="noopener" target="_blank"&gt;프론트엔드 개발환경의 이해: 웹팩(기본) - 김정환 블로그&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webpack.js.org/api/loaders/" title="null" rel="noopener" target="_blank"&gt;Loader Interface&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
]]&gt;</description><author>&lt;![CDATA[Kim Songhyun]]&gt;</author><pubDate>2025-02-05</pubDate></item><item><title>&lt;![CDATA[내 소스를 어디서나 깔끔하게 - editorconfig, prettier, eslint]]&gt;</title><link>https://znagadeon.dev/post/format-my-code/</link><description>&lt;![CDATA[&lt;p&gt;사람은 누구나 실수를 한다. 프로그래머도 사람이므로 마찬가지로 실수를 한다. 실수라고 해봐야 사실 별 볼 일 없는 것들이 대부분이지만 어쨌든 실수는 작게는 프로그래머 한 명을 패닉에 빠뜨릴 수 있고 심하게는 서비스 팀 전체가 장애대응 모드에 들게 만들 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/coffee.jpg" alt="주작이라고 하니 재미로만 봐주시길"&gt;&lt;figcaption aria-hidden="true"&gt;주작이라고 하니 재미로만 봐주시길&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;수만 명, 많게는 수십만, 수백만 명이 사용하는 서비스는 기도 메타로만 돌아갈 수 없기 때문에 이런 실수를 방지해 줄 여러 안전장치들이 필요해졌고 그렇게 해서 나오게 된 것이 &lt;strong&gt;정적 분석&lt;/strong&gt;이다. 정적 분석이란, 코드를 실제로 실행하지 않고 코드, 혹은 바이너리 자체의 내용만으로 코드의 버그나 결함을 잡아내는 분석 방식을 말한다. 자바스크립트 진영에서는 &lt;code class="short-code"&gt;eslint&lt;/code&gt;를 사실상 표준으로 사용하고 있다.&lt;/p&gt;
&lt;p&gt;코드적인 냄새 외에도, 여러 명의 작업자가 한 코드베이스를 수정할 때 동일한 코드 스타일을 유지할 수 있는 방법도 고안되었다. 에디터 자체가 지원하는 기능을 사용해도 좋지만, 모든 에디터에서 같은 설정파일을 사용하기 위해 &lt;code class="short-code"&gt;eslint&lt;/code&gt;와 마찬가지로 코드 포매팅을 전문으로 하는 라이브러리를 사용할 수 있다. 이 글에서는 &lt;code class="short-code"&gt;editorconfig&lt;/code&gt;와 &lt;code class="short-code"&gt;prettier&lt;/code&gt;에 대해서 설명할 예정이다.&lt;/p&gt;
&lt;h2 id="editorconfig"&gt;editorconfig&lt;/h2&gt;
&lt;p&gt;&lt;code class="short-code"&gt;editorconfig&lt;/code&gt;는 언어에 상관 없이, 파일명 패턴만으로 파일의 인덴트 사이즈, 스페이스바 사용 여부 등 기초적인 코딩 스타일을 지정해 줄 수 있는 일종의 규약이다. 대부분의 에디터가 &lt;code class="short-code"&gt;editorconfig&lt;/code&gt; 플러그인을 제공하므로, 해당 플러그인을 설치하면 어디서든 같은 코딩 스타일을 유지할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/editorconfig-for-vscode.png" alt="VS Code에도 있다"&gt;&lt;figcaption aria-hidden="true"&gt;VS Code에도 있다&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;혹자는 이후 설명할 &lt;code class="short-code"&gt;prettier&lt;/code&gt;를 사용하면 되는데 왜 &lt;code class="short-code"&gt;editorconfig&lt;/code&gt;를 따로 사용하냐고 할 수도 있겠다. 일단 결론부터 말하자면 &lt;code class="short-code"&gt;prettier&lt;/code&gt; 하나만 써도 결과적으로 같은 코드를 생산하기는 하지만 적용되는 시점이 다르기 때문에 둘 다 사용하는 것이 좋다. &lt;code class="short-code"&gt;prettier&lt;/code&gt;는 저장 시점, 혹은 커밋 직전 시점에 마지막으로 코드를 포맷하는 것이고, &lt;code class="short-code"&gt;editorconfig&lt;/code&gt;는 작성 시점에 일관적인 스타일을 제공한다고 생각하면 된다.&lt;/p&gt;
&lt;p&gt;폴더 최상단에 &lt;code class="short-code"&gt;.editorconfig&lt;/code&gt; 파일을 생성한 뒤, 아래 포맷에 맞추어 설정 파일을 작성한다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;[파일 패턴]&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;charset = utf-8&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;end_of_line = lf&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;# ...&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;파일 패턴은 우리가 유닉스에서 일반적으로 사용하는 와일드카드 패턴을 사용한다. 예를 들어 &lt;code class="short-code"&gt;*.js&lt;/code&gt;는 폴더 내 모든 js 파일을 의미한다. 범용적으로 사용하는 옵션은 7개 + 루트 폴더 여부를 체크하는 옵션까지 총 8개이며, 그 이외에도 추가적인 옵션이 있지만 모든 에디터에서 동작을 보장하지는 않는다. 이후 설명할 &lt;code class="short-code"&gt;prettier&lt;/code&gt;가 세밀한 포매팅 옵션을 추가로 제공하므로 여기에서는 범용 옵션만을 사용하자. &lt;a href="https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties" title="null" rel="noopener" target="_blank"&gt;여기&lt;/a&gt;에서 전체 옵션 리스트를 확인할 수 있다.&lt;/p&gt;
&lt;h2 id="prettier"&gt;prettier&lt;/h2&gt;
&lt;p&gt;이렇게 해서 앞으로 작성할 코드에 대해서는 일관적인 스타일을 적용할 수 있게 되었다. 하지만 &lt;code class="short-code"&gt;editorconfig&lt;/code&gt;는 이미 작성한 코드에 대한 스타일 교정은 지원하지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/editorconfig-limit.gif" alt=""&gt;&lt;figcaption aria-hidden="true"&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;이렇게, 이미 작성한 코드에 대해서 추가적으로 코드 포매팅을 적용하기 위해서, 또 자바스크립트만의 추가적인 규칙 지정을 위해서 &lt;code class="short-code"&gt;prettier&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;# 설치&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;npm i -D prettier&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;# prettier 설정 파일 추가&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;echo&lt;/span&gt; {} &amp;gt; .prettierrc.json&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;6&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;echo&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;#x27;ignore-this-file.js&amp;#x27;&lt;/span&gt; &amp;gt; .prettierignore&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;7&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;8&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;# 코드 포매팅&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;9&lt;/code&gt;&lt;code class="code"&gt;npx prettier --write .&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code class="short-code"&gt;editorconfig&lt;/code&gt;에 비하면 훨씬 많지만 &lt;code class="short-code"&gt;prettier&lt;/code&gt;도 &lt;a href="https://prettier.io/docs/en/options.html" title="null" rel="noopener" target="_blank"&gt;옵션&lt;/a&gt; 자체는 그렇게 많지 않은 편이기 때문에 직접 읽어보면서 세팅해보기를 권한다.&lt;/p&gt;
&lt;h3 id="파일별로-각각-다른-포맷을-지정하기"&gt;파일별로 각각 다른 포맷을 지정하기&lt;/h3&gt;
&lt;p&gt;&lt;code class="short-code"&gt;prettier&lt;/code&gt;도 &lt;code class="short-code"&gt;overrides&lt;/code&gt; 속성을 이용해서 파일별로 각각 지정하고 싶은 포맷을 따로 지정해 줄 수 있다. 아래 설정 파일에서는 yaml/json 포맷의 파일에는 탭을 사용하지 않고, 나머지 파일에는 탭을 사용하도록 하고 있다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;&amp;quot;useTabs&amp;quot;&lt;/span&gt;: &lt;span class="hljs-literal"&gt;true&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;&amp;quot;overrides&amp;quot;&lt;/span&gt;: [{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;&amp;quot;files&amp;quot;&lt;/span&gt;: [&lt;span class="hljs-string"&gt;&amp;quot;*.json&amp;quot;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;quot;*.babelrc&amp;quot;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;quot;*.yml&amp;quot;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;quot;*.yaml&amp;quot;&lt;/span&gt;],&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;&amp;quot;options&amp;quot;&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;6&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-attr"&gt;&amp;quot;useTabs&amp;quot;&lt;/span&gt;: &lt;span class="hljs-literal"&gt;false&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;7&lt;/code&gt;&lt;code class="code"&gt;        }&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;8&lt;/code&gt;&lt;code class="code"&gt;    }]&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;9&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h2 id="eslint"&gt;eslint&lt;/h2&gt;
&lt;p&gt;마지막으로 &lt;code class="short-code"&gt;eslint&lt;/code&gt;를 세팅해보자. 앞서 설명했듯 &lt;code class="short-code"&gt;eslint&lt;/code&gt;는 정적 분석 도구로 &lt;code class="short-code"&gt;prettier&lt;/code&gt;나 &lt;code class="short-code"&gt;editorconfig&lt;/code&gt; 같은 포매팅 툴과는 궤가 조금 다른데, 예를 들어 아래 코드를 보자.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; a = &lt;span class="hljs-number"&gt;1&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;a = &lt;span class="hljs-number"&gt;2&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;이런 코드가 있을 때, 코드에 세미콜론을 찍지 않았으니 세미콜론을 찍어주는 역할이 &lt;code class="short-code"&gt;prettier&lt;/code&gt;라면, &lt;code class="short-code"&gt;const&lt;/code&gt;로 선언된 변수가 변경되고 있다고 경고해 주는 것이 &lt;code class="short-code"&gt;eslint&lt;/code&gt;이다. 역할이 다르므로 둘 다 사용해야 한다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;# 설치&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;npm i -D eslint&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;npm i -D eslint-config-prettier &lt;span class="hljs-comment"&gt;# eslint와 prettier를 함께 사용하는 경우 설치&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;# 최초 설정&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;6&lt;/code&gt;&lt;code class="code"&gt;npx eslint --init&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;7&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;8&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;# 실행&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;9&lt;/code&gt;&lt;code class="code"&gt;npx eslint .&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;lint는 설정이 워낙 방대해서 여기에 모든 케이스의 해결 방안에 대해서는 적을 수는 없다. 대신 내가 겪었던 케이스와 그 해결책을 몇 가지 공개해보려고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/eslint-errors.png" alt=""&gt;&lt;figcaption aria-hidden="true"&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;h3 id="eslint와-prettier를-함께-사용하기"&gt;eslint와 prettier를 함께 사용하기&lt;/h3&gt;
&lt;p&gt;앞서 &lt;code class="short-code"&gt;eslint&lt;/code&gt;는 코드 정적 분석 도구라고 설명했는데, 정적 분석은 크게 보면 사실 코드 포매팅이 잘 되어있는지 평가하는 과정도 포함한다. 이 역할이 &lt;code class="short-code"&gt;prettier&lt;/code&gt;와 중복되기 때문에, 둘 중 한 쪽의 설정을 무효화하는 과정이 필요하다. &lt;code class="short-code"&gt;prettier&lt;/code&gt; 공식 문서에서 추천하는 방법은 &lt;code class="short-code"&gt;eslint-config-prettier&lt;/code&gt;를 이용하여 &lt;code class="short-code"&gt;eslint&lt;/code&gt;의 코드 포매팅 체크를 무효화하는 것이다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;npm i -D eslint-config-prettier&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;&amp;quot;extends&amp;quot;&lt;/span&gt;: [&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-string"&gt;&amp;quot;prettier&amp;quot;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;    ]&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h3 id="jest와-함께-사용하기"&gt;jest와 함께 사용하기&lt;/h3&gt;
&lt;p&gt;&lt;code class="short-code"&gt;jest&lt;/code&gt;는 여러 테스트 함수를 전역에 놓고 사용한다. &lt;code class="short-code"&gt;eslint&lt;/code&gt;의 기본 설정은 미리 선언되지 않은 함수를 사용하는 것을 금지하고 있으므로 여기에서 에러가 발생한다(스크린샷의 &lt;code class="short-code"&gt;no-undef&lt;/code&gt; 일부). 이를 해결하기 위해서는 플러그인을 하나 설치해햐 한다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;npm i -D eslint-plugin-jest&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;&amp;quot;env&amp;quot;&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;&amp;quot;jest/globals&amp;quot;&lt;/span&gt;: &lt;span class="hljs-literal"&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;    },&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;&amp;quot;plugins&amp;quot;&lt;/span&gt;: [&lt;span class="hljs-string"&gt;&amp;quot;jest&amp;quot;&lt;/span&gt;]&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;6&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h3 id="전역-변수와-함께-사용하기"&gt;전역 변수와 함께 사용하기&lt;/h3&gt;
&lt;p&gt;템플릿 엔진이나 &lt;code class="short-code"&gt;webpack.DefinePlugin&lt;/code&gt; 등 js파일 외부에서 전역 변수를 선언하여 사용하는 경우 해당 전역변수가 선언되어 있지 않다는 오류를 뱉는다(스크린샷의 &lt;code class="short-code"&gt;IS_DEV&lt;/code&gt; 관련 에러). 해당 변수가 전역변수이므로 외부에 선언되어 있다는 설정을 추가해 주면 해결된다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;&amp;quot;globals&amp;quot;&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;&amp;quot;IS_DEV&amp;quot;&lt;/span&gt;: &lt;span class="hljs-literal"&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;    }&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h3 id="체크하고-싶지-않은-설정-무효화하기"&gt;체크하고 싶지 않은 설정 무효화하기&lt;/h3&gt;
&lt;p&gt;마지막으로 이 모든 경우에 해당하지 않으나 그냥 해당 규칙을 회피하고 싶은 경우이다. 나 같은 경우 함수 패러미터를 사용하지 않더라도 언젠가 사용할지 모르겠다는 생각이 들 때 그냥 남겨두는 편인데 &lt;code class="short-code"&gt;eslint&lt;/code&gt; 추천 설정에서는 이것도 에러로 잡는다. 이 에러를 그냥 경고 표시로 바꿔보자.&lt;/p&gt;
&lt;p&gt;에러 옆에 &lt;code class="short-code"&gt;no-unused-vars&lt;/code&gt;라는 표시가 보이는가? 이 이름이 해당 에러를 일으키는 룰의 이름이다. &lt;a href="https://eslint.org/docs/rules/no-unused-vars" title="null" rel="noopener" target="_blank"&gt;&lt;code class="short-code"&gt;eslint&lt;/code&gt; 공식 홈페이지에서 해당 룰을 검색해 볼 수 있다.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;페이지에 들어가면, 해당 룰이 어떤 역할을 하는지, 어떤 코드에서 에러를 일으키고 어떤 코드일 때 정상적으로 통과하는지, 어떤 옵션을 줄 수 있고 이 옵션이 어떤 역할을 하는지가 자세히 설명되어 있다. 영어의 압박을 이겨낸다면 여러분에게 필요한 룰을 금방 익힐 수 있을 것이다.&lt;/p&gt;
&lt;p&gt;다시 &lt;code class="short-code"&gt;no-unused-vars&lt;/code&gt;로 돌아와서, 이 룰을 경고 표시로 바꾸고 몇 가지 옵션을 추가해 주자.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-attr"&gt;&amp;quot;rules&amp;quot;&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;&amp;quot;no-unused-vars&amp;quot;&lt;/span&gt;: [&lt;span class="hljs-string"&gt;&amp;quot;warn&amp;quot;&lt;/span&gt;, { &lt;span class="hljs-attr"&gt;&amp;quot;vars&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;all&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;args&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;after-used&amp;quot;&lt;/span&gt; }]&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;    }&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;에러의 레벨은 &lt;code class="short-code"&gt;off&lt;/code&gt;, &lt;code class="short-code"&gt;warn&lt;/code&gt;, &lt;code class="short-code"&gt;error&lt;/code&gt;로 총 세 가지이며, 그 옆에 룰에 해당하는 옵션값을 전달하면 된다. 작성하지 않은 옵션은 기본값으로 들어간다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/lint-complete.png" alt=""&gt;&lt;figcaption aria-hidden="true"&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;이렇게 해서 모든 에러가 해결되었다.&lt;/p&gt;
&lt;h2 id="정리"&gt;정리&lt;/h2&gt;
&lt;p&gt;앞으로 이 레포에 접근 가능한 모든 유저가 같은 스타일의 코드를 유지하고, 장애가 발생하기 전에 그 요인을 최대한 줄일 수 있게 되었다. 코드베이스가 작을 때는 사람이 코드의 문제를 직접 추적하고 해결할 수 있지만, 코드의 규모가 커질수록 이런 도구의 도움 없이는 사전에 버그를 찾아내는 것이 점점 어려워진다. 자신이 개발 중인 기능의 규모가 크다면 이런 정적 분석 툴을 적극적으로 도입해보자.&lt;/p&gt;
]]&gt;</description><author>&lt;![CDATA[Kim Songhyun]]&gt;</author><pubDate>2025-02-05</pubDate></item><item><title>&lt;![CDATA[highlight.js에 줄 번호를 넣어 보자]]&gt;</title><link>https://znagadeon.dev/post/line-number-with-highlight-js/</link><description>&lt;![CDATA[&lt;p&gt;개발 블로그에 코드 블럭은 필수다. 열 줄의 설명보다 한 줄의 코드가 나을 때가 있다. 괜히 개발자는 코드로 말한다는 말이 있는 것이 아니다.&lt;/p&gt;
&lt;p&gt;덜렁 &lt;code class="short-code"&gt;pre&lt;/code&gt; 태그 안에 코드를 넣어 버리면 가독성이 크게 떨어지므로 보통 소스 코드를 넣으면 자동으로 하이라이트해주는 라이브러리를 사용한다. 나는 &lt;a href="https://highlightjs.org/" title="null" rel="noopener" target="_blank"&gt;&lt;code class="short-code"&gt;highlight.js&lt;/code&gt;&lt;/a&gt;를 선호하는 편인데, Gatsby 같은 데서는 &lt;a href="https://prismjs.com/" title="null" rel="noopener" target="_blank"&gt;&lt;code class="short-code"&gt;Prism.js&lt;/code&gt;&lt;/a&gt;를 쓰는 것 같더라.&lt;/p&gt;
&lt;p&gt;왜 굳이 Gatsby에서 뭘 쓰는지를 찾아봤냐면, 일단 디자인이 깔끔해서 보기 좋았고, 각종 부가 기능을 제공하고 있었기 때문이다. &lt;code class="short-code"&gt;highlight.js&lt;/code&gt;도 더할 나위 없이 좋은 라이브러리지만 &lt;a href="https://highlightjs.readthedocs.io/en/latest/line-numbers.html" title="null" rel="noopener" target="_blank"&gt;부가 기능은 지원을 안 하려고 한다&lt;/a&gt;. 단순성을 침해한다고 생각하는 것 같다.&lt;/p&gt;
&lt;p&gt;귀찮아졌지만 방법이 없는 것은 아니다. 개발진의 말마따나 결과물의 단순성을 최대한 보장하려고 하기 때문에, 아웃풋이 어떻게 나오는지만 판단한다면 입맛대로 뜯어고치기 딱 좋다. 이 글에서는 &lt;code class="short-code"&gt;highlight.js&lt;/code&gt;가 지원하지 않는 줄번호 기능을 구현해보려고 한다.&lt;/p&gt;
&lt;p&gt;나는 이 기능을 내 개발 블로그(여기)에서 쓸 생각으로 만들었는데, 이 사이트는 마크다운을 사전에 빌드하여 html 파일만 서빙하고 있다. 문서가 로드된 뒤에 로딩되는 건 Disqus로 충분하다고 생각하기 때문에 여기서는 &lt;code class="short-code"&gt;highlight.js&lt;/code&gt;로 사전에 소스코드를 변환하고 서빙할 때는 css만으로 동작한다고 가정한다. 즉, &lt;a href="https://highlightjs.org/usage/" title="null" rel="noopener" target="_blank"&gt;공식 문서에서 제공하는 방법&lt;/a&gt;과는 좀 차이가 있다. 공식 문서에서는 문서가 로드된 뒤 동적으로 소스코드를 변환하기 때문이다.&lt;/p&gt;
&lt;h2 id="동작"&gt;동작&lt;/h2&gt;
&lt;p&gt;백엔드에서 &lt;code class="short-code"&gt;highlight.js&lt;/code&gt;를 사용할 땐 이렇게 쓴다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; hljs = &lt;span class="hljs-built_in"&gt;require&lt;/span&gt;(&lt;span class="hljs-string"&gt;&amp;#x27;highlight.js&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; convertedCode = hljs.highlight(lang || &lt;span class="hljs-string"&gt;&amp;#x27;plaintext&amp;#x27;&lt;/span&gt;, code).value;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code class="short-code"&gt;hljs.highlight&lt;/code&gt;는 &lt;code class="short-code"&gt;lang&lt;/code&gt;과 &lt;code class="short-code"&gt;code&lt;/code&gt; 이렇게 두 개의 인수를 갖는다. 이미 너무 명확한 네이밍이라 굳이 설명이 필요하지는 않을 것 같다. 나는 마크다운에서 코드 변환을 하려고 하고 있기 때문에, 코드 블럭에 언어를 지정하지 않는 경우도 대응해 줘야 해서 기본 인수로 &lt;code class="short-code"&gt;plaintext&lt;/code&gt;를 전달하기로 했다.&lt;/p&gt;
&lt;p&gt;결과는 어떨까? 이런 js 코드 문자열이 있다고 가정하자.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;for&lt;/span&gt; (&lt;span class="hljs-keyword"&gt;let&lt;/span&gt; i = &lt;span class="hljs-number"&gt;0&lt;/span&gt;; i &amp;lt; &lt;span class="hljs-number"&gt;10&lt;/span&gt;; i++) {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-built_in"&gt;console&lt;/span&gt;.log(&lt;span class="hljs-string"&gt;&amp;#x27;Hello, world!&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;결과물은 이렇다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;span&lt;/span&gt; &lt;span class="hljs-attr"&gt;class&lt;/span&gt;=&lt;span class="hljs-string"&gt;&amp;quot;hljs-keyword&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;for&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;span&lt;/span&gt;&amp;gt;&lt;/span&gt; (&lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;span&lt;/span&gt; &lt;span class="hljs-attr"&gt;class&lt;/span&gt;=&lt;span class="hljs-string"&gt;&amp;quot;hljs-keyword&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;let&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;span&lt;/span&gt;&amp;gt;&lt;/span&gt; i=&lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;span&lt;/span&gt; &lt;span class="hljs-attr"&gt;class&lt;/span&gt;=&lt;span class="hljs-string"&gt;&amp;quot;hljs-number&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;0&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;span&lt;/span&gt;&amp;gt;&lt;/span&gt;; i&lt;span class="hljs-symbol"&gt;&amp;lt;&lt;/span&gt;&lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;span&lt;/span&gt; &lt;span class="hljs-attr"&gt;class&lt;/span&gt;=&lt;span class="hljs-string"&gt;&amp;quot;hljs-number&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;10&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;span&lt;/span&gt;&amp;gt;&lt;/span&gt;; i++) {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;span&lt;/span&gt; &lt;span class="hljs-attr"&gt;class&lt;/span&gt;=&lt;span class="hljs-string"&gt;&amp;quot;hljs-built_in&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;console&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;span&lt;/span&gt;&amp;gt;&lt;/span&gt;.log(&lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;span&lt;/span&gt; &lt;span class="hljs-attr"&gt;class&lt;/span&gt;=&lt;span class="hljs-string"&gt;&amp;quot;hljs-string&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;&amp;#x27;Hello, world!&amp;#x27;&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;span&lt;/span&gt;&amp;gt;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;태그가 길어져서 읽기 어렵지만, 스타일을 적용하기 위해 예약어를 &lt;code class="short-code"&gt;span&lt;/code&gt; 태그로 감싸고 이스케이프 처리 정도만 했을 뿐 원본이 되는 문자열과 거의 차이가 없다는 것을 알 수 있다.&lt;/p&gt;
&lt;p&gt;재미있는 것은 변환이 끝난 후에도 문자열이 &lt;code class="short-code"&gt;\n&lt;/code&gt;으로 줄바꿈을 한다는 것이다. 특별히 설정을 만지지 않는다면 &lt;code class="short-code"&gt;pre&lt;/code&gt; 태그 안에 들어갈 것이라고 가정하기 때문이다(공식 문서를 뒤져보면 줄바꿈 문자를 &lt;code class="short-code"&gt;&amp;lt;br&amp;gt;&lt;/code&gt;로 바꿔주는 옵션이 있긴 한데, 여기서 사용할 것은 아니므로 넘어가자.). 하여간 이런 특징으로 인해 라인 단위로 코드를 분리하기 아주 쉬워졌다. 그냥 split만 한 번 해 주면 되니까.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; hljs = &lt;span class="hljs-built_in"&gt;require&lt;/span&gt;(&lt;span class="hljs-string"&gt;&amp;#x27;highlight.js&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; convertedCode = hljs.highlight(lang || &lt;span class="hljs-string"&gt;&amp;#x27;plaintext&amp;#x27;&lt;/span&gt;, code).value;&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; splittedCode = convertedCode.split(&lt;span class="hljs-string"&gt;&amp;#x27;\n&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h2 id="줄-번호-넣기"&gt;줄 번호 넣기&lt;/h2&gt;
&lt;p&gt;이미 라인 단위로 코드를 전부 잘라냈기 때문에 줄번호 넣기는 아주 쉽다. 배열의 인덱스값을 적절히 활용하면 된다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; hljs = &lt;span class="hljs-built_in"&gt;require&lt;/span&gt;(&lt;span class="hljs-string"&gt;&amp;#x27;highlight.js&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; convertedCode = hljs.highlight(lang || &lt;span class="hljs-string"&gt;&amp;#x27;plaintext&amp;#x27;&lt;/span&gt;, code).value;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; splittedCode = convertedCode.split(&lt;span class="hljs-string"&gt;&amp;#x27;\n&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; lineAttachedCode = splittedCode.map(&lt;span class="hljs-function"&gt;(&lt;span class="hljs-params"&gt;code, i&lt;/span&gt;) =&amp;gt;&lt;/span&gt; &lt;span class="hljs-string"&gt;`&lt;span class="hljs-subst"&gt;${i+&lt;span class="hljs-number"&gt;1&lt;/span&gt;}&lt;/span&gt; | &lt;span class="hljs-subst"&gt;${code}&lt;/span&gt;`&lt;/span&gt;).join(&lt;span class="hljs-string"&gt;&amp;#x27;\n&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/code-with-line-number.png" alt="기능적으론 완벽한데, 뭔가..."&gt;&lt;figcaption aria-hidden="true"&gt;기능적으론 완벽한데, 뭔가...&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;잘 동작하긴 하지만 뭔가 불만족스럽다. 하나하나 수정해보자.&lt;/p&gt;
&lt;h3 id="맞지-않는-줄번호"&gt;맞지 않는 줄번호&lt;/h3&gt;
&lt;p&gt;줄번호의 자릿수가 바뀔 때마다 코드가 점점 밀려난다. 줄번호 영역이 숫자의 자릿수가 늘어나도 일정한 크기를 유지하도록 작은 숫자에 패딩을 넣어 주자.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; hljs = &lt;span class="hljs-built_in"&gt;require&lt;/span&gt;(&lt;span class="hljs-string"&gt;&amp;#x27;highlight.js&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; convertedCode = hljs.highlight(lang || &lt;span class="hljs-string"&gt;&amp;#x27;plaintext&amp;#x27;&lt;/span&gt;, code).value;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; splittedCode = convertedCode.split(&lt;span class="hljs-string"&gt;&amp;#x27;\n&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;6&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; padder = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;length&lt;/span&gt; =&amp;gt;&lt;/span&gt; &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;num&lt;/span&gt; =&amp;gt;&lt;/span&gt; num.toString().padStart(length, &lt;span class="hljs-string"&gt;&amp;#x27; &amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;7&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; padNumber = padder(splittedCode.length.toString().length);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;8&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;9&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; lineAttachedCode = splittedCode.map(&lt;span class="hljs-function"&gt;(&lt;span class="hljs-params"&gt;code, i&lt;/span&gt;) =&amp;gt;&lt;/span&gt; &lt;span class="hljs-string"&gt;`&lt;span class="hljs-subst"&gt;${padNumber(i+&lt;span class="hljs-number"&gt;1&lt;/span&gt;)}&lt;/span&gt; | &lt;span class="hljs-subst"&gt;${code}&lt;/span&gt;`&lt;/span&gt;).join(&lt;span class="hljs-string"&gt;&amp;#x27;\n&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/padding-attached.png" alt="훨씬 깔끔해졌다"&gt;&lt;figcaption aria-hidden="true"&gt;훨씬 깔끔해졌다&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;물론 이 정도로도 개발 블로그를 운영하는 데는 무리가 없지만, 아직 좀 더 개선할 여지가 남아 있다.&lt;/p&gt;
&lt;h3 id="코드를-복사하려는데-줄번호도-같이-복사된다"&gt;코드를 복사하려는데 줄번호도 같이 복사된다&lt;/h3&gt;
&lt;p&gt;인터넷에서 내가 찾는 아주 멋진 솔루션 코드를 찾았다고 해 보자. 적당히 소스를 긁어다가 내 입맛대로 고쳐보고 싶은데, 내가 앞서 설명한 방법으로 만든 블로그라면 코드를 복사할 때 줄번호도 같이 복사된다. html 마크업 상으로는 줄번호와 코드를 구분하는 마땅한 기준이 없기 때문이다.&lt;/p&gt;
&lt;p&gt;따라서, 한 줄을 &lt;strong&gt;줄번호 영역&lt;/strong&gt;과 &lt;strong&gt;코드 영역&lt;/strong&gt;으로 나누어 줄 필요가 있다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; hljs = &lt;span class="hljs-built_in"&gt;require&lt;/span&gt;(&lt;span class="hljs-string"&gt;&amp;#x27;highlight.js&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; convertedCode = hljs.highlight(lang || &lt;span class="hljs-string"&gt;&amp;#x27;plaintext&amp;#x27;&lt;/span&gt;, code).value;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; splittedCode = convertedCode.split(&lt;span class="hljs-string"&gt;&amp;#x27;\n&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; padder = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;length&lt;/span&gt; =&amp;gt;&lt;/span&gt; &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;num&lt;/span&gt; =&amp;gt;&lt;/span&gt; num.toString().padStart(length, &lt;span class="hljs-string"&gt;&amp;#x27; &amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; padNumber = padder(splittedCode.length.toString().length);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; lineAttachedCode = splittedCode&lt;/code&gt;&lt;/div&gt;
&lt;div class="highlighted"&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;    .map(&lt;span class="hljs-function"&gt;(&lt;span class="hljs-params"&gt;code, i&lt;/span&gt;) =&amp;gt;&lt;/span&gt; &lt;span class="hljs-string"&gt;`&amp;lt;code class=&amp;quot;line-number&amp;quot;&amp;gt;&lt;span class="hljs-subst"&gt;${padNumber(i+&lt;span class="hljs-number"&gt;1&lt;/span&gt;)}&lt;/span&gt;&amp;lt;/code&amp;gt;&amp;lt;code class=&amp;quot;code&amp;quot;&amp;gt;&lt;span class="hljs-subst"&gt;${code}&lt;/span&gt;&amp;lt;/code&amp;gt;`&lt;/span&gt;)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;    .join(&lt;span class="hljs-string"&gt;&amp;#x27;\n&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;물론 이렇게 한다고 바로 줄번호가 복사할 수 없는 상태가 되지는 않는다. 여기서부터는 css의 도움이 필요하다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-selector-class"&gt;.line-number&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    user-select: none;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;}&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code class="short-code"&gt;user-select&lt;/code&gt; 속성은 텍스트를 드래그하여 선택할 수 있는지 여부를 결정해 준다. 여기에 &lt;code class="short-code"&gt;none&lt;/code&gt;을 주면 선택할 수 없는 상태가 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/user-select.png" alt="이제 마음껏 코드를 복사할 수 있다"&gt;&lt;figcaption aria-hidden="true"&gt;이제 마음껏 코드를 복사할 수 있다&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;h2 id="정리"&gt;정리&lt;/h2&gt;
&lt;p&gt;내가 실제로 사용하는 코드는 여기에 라인 강조 기능 같은 것을 추가한다든지 해서 좀 더 마개조한 것이다. 라이브러리 자체의 단순성 덕에 오히려 이것저것 시도해 보기 좋은 것 같다. 이미 다 만들어진 기능을 커스텀하는 것이 까다롭다면, 아예 단순한 라이브러리로 시작해 기능을 쌓아 가는 것도 좋은 경험일 듯하다. &lt;del&gt;그리고 잘 만든 건 공유해 주세요 만들기 귀찮아요&lt;/del&gt;&lt;/p&gt;
]]&gt;</description><author>&lt;![CDATA[Kim Songhyun]]&gt;</author><pubDate>2025-02-05</pubDate></item><item><title>&lt;![CDATA[메이플스토리 강화 효율을 큰 수의 법칙으로 시뮬레이션해보자]]&gt;</title><link>https://znagadeon.dev/post/maple-strengthening/</link><description>&lt;![CDATA[&lt;p&gt;갑자기 생각나서 메이플스토리를 다시 잡은지 대략 5달이 좀 넘게 지났다. 나는 게임은 좀 아쉽게(?) 해야 향상심이 생긴다는 주의라 일부러 현질도 안 하고 경매장 기능도 안 쓰면서 게임을 하고 있다. 원래는 적당히 돈을 모아서 상위 보스가 드랍하는 장비를 경매장에서 사 쓰는 게 일반적인 방식이다. 경매장을 안 쓰고 있으니 내가 잡을 수 있는 수준의 보스 장비만 끼는 것이다. 그래도, 워낙 오래된 게임이다 보니 비교적 쉬운 방법으로 강해질 수 있는 시스템이 천지사방에 깔려 있어서 이래저래 메린이(메이플+어린이, 입문자 수준이라는 뜻) 레벨까지는 도달한 듯하다.&lt;/p&gt;
&lt;p&gt;메린이를 넘어 메청소년으로 가기 위해서는 좀 더 어려운 방법을 써야 한다. 구체적으로 말하자면 더 많은 돈을 써서 더 낮은 확률로 강해져야 한다는 뜻이다. 종류도 엄청나게 많지만, 장비 강화에 한해서만 말하자면 대략 이렇다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/strengthening-methods.png" alt="17년 된 게임은 장비 강화 시스템도 이렇게 많다"&gt;&lt;figcaption aria-hidden="true"&gt;17년 된 게임은 장비 강화 시스템도 이렇게 많다&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;큰 틀에서 보면 둘 중 하나다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;실패 확률이 있는 강화를 한다.&lt;/li&gt;
&lt;li&gt;랜덤 옵션이 붙는 강화를 마음에 드는 옵션이 뜰 때까지 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;둘 다 무작정 자본을 들이붓다 보면 언젠가 원하는 수준으로 강해질 수 있다는 점에서는 똑같지만, 어쨌든 원하는 옵션이 뜰 확률이 n%로 귀결되는 2번 방법에 비해 1번 방법은 생각해볼 여지가 좀 더 있다. 메이플스토리 세상에서는 강화를 실패하면 실패 1회 복구권이나 강화 초기화권을 이용해 지금까지 있었던 실패를 없던 일로 할 수 있다. 한 가지 문제가 있다면 복구권과 초기화권은 같은 재화를 사용하며, 필요 재화량이 무시할 수 없을 만큼 크고, 성공 확률도 좀 낮은 편이라는 것이다. 따라서 확률 소리만 들으면 일단 계산부터 해 봐야 하는 나같은 사람들에 의해서 어떤 방식이 효율적인지에 대한 연구가 계속되어 왔다.&lt;/p&gt;
&lt;p&gt;시스템상 정식 명칭은 업그레이드이지만, &amp;#39;주문의 흔적&amp;#39;이라는 재화를 사용하기 때문에 유저들은 일반적으로 업그레이드를 &amp;#39;주흔작&amp;#39;이라고 부른다. 실패 1회 복구권은 &amp;#39;순백의 주문서&amp;#39;(줄여서 순줌)라고, 강화 초기화권은 &amp;#39;이노센트 주문서&amp;#39;(줄여서 이노)라고 부른다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/upgrade.png" alt="여러분이 오늘 이 글을 통해 간접적으로 체험해볼 시스템"&gt;&lt;figcaption aria-hidden="true"&gt;여러분이 오늘 이 글을 통해 간접적으로 체험해볼 시스템&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;너무 복잡한 상황이 되지 않도록 하기 위해서 약간의 제약조건을 추가했다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;강화 횟수는 부위당 8회라고 가정한다.&lt;/li&gt;
&lt;li&gt;피버타임, 버프나 스킬 등으로 인한 강화 성공률 추가값은 무시한다.&lt;/li&gt;
&lt;li&gt;강화 성공률은 30%에 주흔 500개를 소모한다.&lt;/li&gt;
&lt;li&gt;순줌 성공률은 5%에 주흔 2,000개를 소모한다. 성공 시 실패한 강화를 1회 복구하여 다시 강화할 수 있는 상태가 된다.&lt;/li&gt;
&lt;li&gt;이노 성공률은 30%에 주흔 5,000개를 소모한다. 성공 시 모든 강화를 초기화한다(성공한 강화도 초기화된다).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;퍼센티지와 소모 재화량을 보면 알겠지만, 강화 시도보다는 강화횟수 복구에 훨씬 더 많은 재화를 소모하는 구조다. 실제 인게임상에서는 일요일 피버타임(1회 시도 시 소모하는 주흔 갯수 절반, 확률 상승), 강화확률 버프(강화 성공률 최대 14% 추가, 순줌/이노에는 적용되지 않음) 등을 덕지덕지 발라서 여기서 진행하는 시뮬레이션보다는 훨씬 싼 가격에 강화를 마무리할 수 있긴 하다. 실제로 메이플을 즐기시는 분들은 적당히 재미로만 보고 필요하다면 자신의 상황에 맞게 확률을 조정하기 바란다.&lt;/p&gt;
&lt;h2 id="첫-번째-방법-각-경우의-수에-대한-주흔-소모량-구하기"&gt;첫 번째 방법: 각 경우의 수에 대한 주흔 소모량 구하기&lt;/h2&gt;
&lt;p&gt;일단 4번, 5번 제약조건에 대해 생각해보자. &lt;code class="short-code"&gt;주흔 소모량*1/확률&lt;/code&gt;을 계산하여 1회 성공 시 주흔 소모량의 기댓값을 구해볼 수 있겠다. 따라서, 순줌 1회 성공까지 평균적으로 &lt;code class="short-code"&gt;2000/0.05 = 40000&lt;/code&gt;개, 이노 1회 성공까지 평균적으로 &lt;code class="short-code"&gt;5000/0.3 = 16666&lt;/code&gt;개 정도의 주흔을 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/happy-circuit.jpg" alt=""&gt;&lt;figcaption aria-hidden="true"&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;일단 1회차, &lt;em&gt;난 운이 좋으니 한 번에 못해도 6개 정도는 성공하겠지&lt;/em&gt;라는 행복회로를 돌리며 8번의 강화 시도를 모두 사용했을 때 일어나는 사건은 총 9가지(0~8)이며, 실패 횟수를 모두 복구하는 데 드는 재화의 소모량은 다음과 같다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align="center"&gt;성공 횟수&lt;/th&gt;
&lt;th align="right"&gt;실패를 복구할 때&lt;/th&gt;
&lt;th align="right"&gt;강화 자체를 초기화할 때&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align="center"&gt;0&lt;/td&gt;
&lt;td align="right"&gt;8*40000&lt;/td&gt;
&lt;td align="right"&gt;16666&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align="center"&gt;1&lt;/td&gt;
&lt;td align="right"&gt;7*40000&lt;/td&gt;
&lt;td align="right"&gt;16666&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align="center"&gt;2&lt;/td&gt;
&lt;td align="right"&gt;6*40000&lt;/td&gt;
&lt;td align="right"&gt;16666&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align="center"&gt;...&lt;/td&gt;
&lt;td align="right"&gt;...&lt;/td&gt;
&lt;td align="right"&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align="center"&gt;7&lt;/td&gt;
&lt;td align="right"&gt;1*40000&lt;/td&gt;
&lt;td align="right"&gt;16666&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align="center"&gt;8&lt;/td&gt;
&lt;td align="right"&gt;0&lt;/td&gt;
&lt;td align="right"&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;적당한 수치가 나와서 만족한 찰나 무언가 이상하다는 생각이 들었다. &lt;em&gt;7번이나 성공했는데도 초기화하고 다시 강화하는 게 낫다고?&lt;/em&gt; 당연히 말이 안 된다. 잠깐 고민해 보니 1회차 이후 2회차 강화비용을 완전히 무시하고 계산했다는 사실을 깨달았다. 만약 7번 성공한 뒤 강화를 초기화하고 2회차 강화를 시도했는데 모든 강화를 실패한다면? 당연히 한 번 많은 양의 재화가 깨지더라도 순줌으로 복구하는 쪽이 나을 것이다. 이쯤 되었을 때 문제가 생각보다 까다롭다는 걸 알았다. 실제 상황이라면 n회차 시도를 전제해야 한다. 내 수준에서는 이 모든 경우의 수를 전부 계산할 수는 없다는 결론이 나왔다.&lt;/p&gt;
&lt;h2 id="두-번째-방법-시뮬레이션"&gt;두 번째 방법: 시뮬레이션&lt;/h2&gt;
&lt;p&gt;확률 공부를 열심히 했다면 뭔가 다른 해결책이 있었을지도 모르겠지만 나는 뼛속까지 컴공이기 때문에 다른 방법을 사용해 보기로 했다. n번 이상 강화가 실패할 경우 무조건 이노를 사용하는 가상의 게이머를 만들어서 강화가 완료될 때까지 드는 주흔 갯수를 세면 되지 않을까? 여기까지 생각이 닿았을 때 시뮬레이터를 구현하기 시작했다(링크는 아래에).&lt;/p&gt;
&lt;p&gt;돌려보니 잘 나오는 것 같다! 다만 문제가 좀 있었다.&lt;/p&gt;
&lt;h2 id="두-번째-방법-αalphaα-큰-수의-법칙"&gt;두 번째 방법 &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mo&gt;+&lt;/mo&gt;&lt;mi&gt;α&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;+\alpha&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6667em;vertical-align:-0.0833em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;+&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.0037em;"&gt;α&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;: 큰 수의 법칙&lt;/h2&gt;
&lt;p&gt;랜덤함수가 세 군데나 들어가는 시뮬레이터답게 운이 좋으면 금방 끝나는 케이스도, 숫자에 압도될 정도로 엄청난 양의 주흔이 들어가는 케이스도 있게 마련이다. 적당한 경향성이 있으면 좋겠는데 적게는 몇십만 개, 많게는 몇백만 개의 재화 차이가 나서 신뢰성이 좀 떨어졌다.&lt;/p&gt;
&lt;p&gt;이 때 뇌리에 떠오른 것이 큰 수의 법칙이었다. &lt;a href="https://ko.wikipedia.org/wiki/큰_수의_법칙" title="null" rel="noopener" target="_blank"&gt;위키백과&lt;/a&gt;에서는 큰 수의 법칙을 이렇게 설명한다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;큰 모집단에서 무작위로 뽑은 표본의 평균이 전체 모집단의 평균과 가까울 가능성이 높다는 통계와 확률 분야의 기본 개념이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;말이 좀 어려운데, 시행 횟수를 늘리면 알려지지 않은 실제 확률에 근접한다는 뜻이다. 동전 던지기를 예로 들면, 운이 정말 좋으면 5번 동전을 던져서 다섯 번 다 앞면이 나올 수 있지만, 5만 번 던지면 24000~26000번 정도로 앞면이 나올 확률 50%에 수렴해 간다는 뜻이다. 시행횟수를 늘리면 늘릴수록 근사치는 점점 실제 확률에 가까워진다. 지금처럼 정확한 확률을 계산하기 어려운 경우 쓰기 좋은 방법이다.&lt;/p&gt;
&lt;p&gt;대충 각각 만 번씩 테스트하는 &lt;a href="https://gist.github.com/znagadeon/b7cf2dc6e394e839e6cb87cc728ae2ad" title="null" rel="noopener" target="_blank"&gt;시뮬레이터&lt;/a&gt;를 다시 만들었다.&lt;/p&gt;
&lt;p&gt;시뮬레이션 결과물은 아래와 같다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align="center"&gt;n회 실패 시 초기화&lt;/th&gt;
&lt;th align="right"&gt;평균 재화 소모량&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align="center"&gt;1회&lt;/td&gt;
&lt;td align="right"&gt;187,777,968.3개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align="center"&gt;2회&lt;/td&gt;
&lt;td align="right"&gt;10,199,560개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align="center"&gt;3회&lt;/td&gt;
&lt;td align="right"&gt;1,464,792.15개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align="center"&gt;4회&lt;/td&gt;
&lt;td align="right"&gt;591,846.05개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align="center"&gt;5회&lt;/td&gt;
&lt;td align="right"&gt;&lt;strong&gt;532,979.3개&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align="center"&gt;6회&lt;/td&gt;
&lt;td align="right"&gt;584,959.3개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align="center"&gt;7회&lt;/td&gt;
&lt;td align="right"&gt;658,991.2개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align="center"&gt;순줌만 사용&lt;/td&gt;
&lt;td align="right"&gt;703,259.4개&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;5회 실패했을 때 이노를 바르는 것이 가장 효율적이며, 이 때 대충 53만 개 정도의 주흔을 소모한다는 것을 알 수 있다.&lt;/p&gt;
]]&gt;</description><author>&lt;![CDATA[Kim Songhyun]]&gt;</author><pubDate>2025-02-05</pubDate></item><item><title>&lt;![CDATA[Netlify Large Media로 이미지 호스팅하기]]&gt;</title><link>https://znagadeon.dev/post/netlify-large-media/</link><description>&lt;![CDATA[&lt;p&gt;사정을 설명하자면, 이 블로그는 원래 이미지를 &lt;a href="http://flickr.com/" title="null" rel="noopener" target="_blank"&gt;flickr&lt;/a&gt;에서 호스팅하고 있었다. 블로그에 올리는 사진은 단순 캡처도 있고, 핸드폰으로 찍은 사진을 올리는 경우도 있었는데, 어느 쪽이든 이미지가 너무 커지면 블로그가 엄청나게 느려지고, 쓸데없이 트래픽을 소모하며, 이미지가 제멋대로 돌아간다. 플리커는 이미지 공유 기능을 사용할 시 이미지를 적당한 사이즈로 리사이즈해주는 기능이 있기 때문에, 블로그에 이미지를 많이 올려도 쾌적한 속도를 보장할 수 있다.&lt;/p&gt;
&lt;p&gt;다만 이 작업은 매우 귀찮다. 일단 인터넷이 끊어져 있으면 사용할 수 없으며, 매번 찍은 사진을 flickr에 올려 줘야 하고, 나는 사진이 섞이는 것을 싫어하기 때문에 매 포스트마다 앨범을 새로 만들어주는데 이것도 귀찮은 작업이다.&lt;/p&gt;
&lt;p&gt;다른 방법이 없나 찾아보다가 Netlify Large Media라는 기능을 발견했다. 유레카! 이 글을 쓰는 현재 블로그는 이 방식으로 이미지를 서빙하고 있으며, 한국어로 된 튜토리얼은 없는 것 같길래 적당히 살을 붙여 설명해보려 한다.&lt;/p&gt;
&lt;h2 id="세팅"&gt;세팅&lt;/h2&gt;
&lt;p&gt;Netlify Large Media 자체는 그냥 Git LFS 위에 추가로 지원하는 서비스이다. LFS로 트래킹하는 파일들은 빌드 서버에 포함되는 게 아니라, Netlify의 자체 미디어 서버로 업로드되며, 이 작업은 병렬적으로 진행되기 때문에 미디어의 숫자가 많더라도 비교적 적은 시간 안에 업로드를 완료할 수 있다고 한다.&lt;/p&gt;
&lt;p&gt;따라서, 이 기능을 사용하기 위해서는 Git LFS가 필요하다. 요구하는 최소 버전은 2.5.1버전이며, 맥에서는 brew로 편리하게 설치할 수 있다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;brew install git-lfs&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;설치가 완료되면 원하는 레포에 가서 아래 커맨드를 입력한다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;git lfs install&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;git lfs track &lt;span class="hljs-string"&gt;&amp;quot;*.png&amp;quot;&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;*.jpg&amp;quot;&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;*.gif&amp;quot;&lt;/span&gt; &lt;span class="hljs-comment"&gt;# 원하는 확장자를 추가해준다&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;완료하면 &lt;code class="short-code"&gt;.gitattribute&lt;/code&gt; 파일이 새로 생기거나 변경된다. 이 파일을 레포에 추가하면 Git LFS 준비는 끝이다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;다음으로, Netlify CLI를 설치한다. 최소 요구 버전은 2.6.4이다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;npm i -g netlify-cli&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;netlify login&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;이제 Netlify Large Media를 사용할 수 있도록 설정해주자.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;netlify plugins:install netlify-lm-plugin&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;netlify lm:install&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;netlify link&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;netlify lm:setup&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code class="short-code"&gt;.netlify/state.json&lt;/code&gt;, &lt;code class="short-code"&gt;.lfsconfig&lt;/code&gt; 파일이 생성된다면 성공이다. &lt;code class="short-code"&gt;.netlify&lt;/code&gt; 디렉토리는 ignore 처리되므로, 업데이트된 &lt;code class="short-code"&gt;.gitignore&lt;/code&gt; 파일과 &lt;code class="short-code"&gt;.lfsconfig&lt;/code&gt; 파일을 레포에 추가해주면 된다.&lt;/p&gt;
&lt;h3 id="lmsetup이-계속-실패한다면"&gt;&lt;code class="short-code"&gt;lm:setup&lt;/code&gt;이 계속 실패한다면&lt;/h3&gt;
&lt;p&gt;netlify config 파일이 제대로 업데이트되지 않는 경우이다. 맥은 해당 config 파일을 두 곳에 저장하는데 우리가 접근해서 사용할 &lt;code class="short-code"&gt;~/.netlify/config.json&lt;/code&gt;이 제대로 업데이트되지 않아 해당 스크립트가 실패하며, 아직까지는 맥에서 문제가 고쳐지지 않은 것으로 보인다. &lt;a href="https://community.netlify.com/t/cant-push-after-trying-to-setup-large-media/31125/6" title="null" rel="noopener" target="_blank"&gt;이 링크&lt;/a&gt;를 참고해서 해결했다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;cp ~/Library/Preferences/netlify/config.json ~/.netlify/config.json&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h3 id="배포-폴더가-root가-아닌-경우-설정"&gt;배포 폴더가 root가 아닌 경우 설정&lt;/h3&gt;
&lt;p&gt;이 설정이 빠져 있어서 6시간동안 삽질했다. 나는 홈페이지의 배포판을 &lt;code class="short-code"&gt;/dist&lt;/code&gt;에 저장하는데, &lt;strong&gt;만약 이 디렉토리에 이미지 파일이 없다면 Netlify Large Media가 파일이 생성된 사실을 인지하지 못한다.&lt;/strong&gt; 반드시 배포용 디렉토리에 내 이미지 파일이 포함되도록 설정을 조정하거나 이미지를 복사하는 스크립트를 끼워 넣자. 나는 빌드를 웹팩으로 하고 있어서, &lt;a href="https://github.com/znagadeon/homepage/pull/12/commits/0b5380b8c0144cd411ce5e94c1b38ac1e18cd2f3" title="null" rel="noopener" target="_blank"&gt;OnBuildPlugin&lt;/a&gt;을 하나 추가해서 해결했다.&lt;/p&gt;
&lt;h2 id="이미지-리사이즈"&gt;이미지 리사이즈&lt;/h2&gt;
&lt;p&gt;이제 파일을 배포해 보자. &lt;code class="short-code"&gt;https://app.netlify.com/sites/{프로젝트-이름}/large-media&lt;/code&gt;에 접속하여 실제로 업로드된 파일을 확인할 수 있다. 여기까지 성공했다면, 이제 본래 사용하려고 했던 Transform 기능을 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/file-list.png" alt="이 화면을 보기 위해 얼마나 고생했던지"&gt;&lt;figcaption aria-hidden="true"&gt;이 화면을 보기 위해 얼마나 고생했던지&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;사용법은 간단하다. url에 쿼리스트링을 추가해 주면 된다. 사용할 수 있는 패러미터는 아래 세 가지다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class="short-code"&gt;nf_resize&lt;/code&gt;(필수): 이미지를 리사이즈할 방식을 선택할 수 있으며, &lt;code class="short-code"&gt;fit&lt;/code&gt;과 &lt;code class="short-code"&gt;smartcrop&lt;/code&gt; 두 가지 옵션 중 하나를 선택할 수 있다. 단어가 설명하듯, &lt;code class="short-code"&gt;fit&lt;/code&gt;은 이미지의 비율을 조정하여 크기를 줄이는 방식이고, &lt;code class="short-code"&gt;smartcrop&lt;/code&gt;은 Netlify의 이미지 인식 알고리즘으로 이미지에서 가장 의미있는 부분을 자동으로 추출하여 크롭해준다.&lt;/li&gt;
&lt;li&gt;&lt;code class="short-code"&gt;w&lt;/code&gt;, &lt;code class="short-code"&gt;h&lt;/code&gt;: 이미지의 너비, 높이 값을 정할 수 있다. &lt;code class="short-code"&gt;smartcrop&lt;/code&gt;을 사용할 경우 두 값을 모두 지정해 주어야 하며, &lt;code class="short-code"&gt;fit&lt;/code&gt;인 경우 최소 한 개의 값을 지정해 주어야 한다. 단, w, h 두 값이 기존 이미지의 비율에 맞지 않으면 이미지가 늘어나거나 찌그러지니, &lt;code class="short-code"&gt;fit&lt;/code&gt;을 사용할 땐 하나의 패러미터만 사용하는 것을 권장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다만 이런 식이다 보니 막연히 중간 사이즈로 잘라야지... 하는 시도는 하기 어렵다. 플리커에서 이미지 공유를 한다면 플리커 이미지 사이즈를 기반으로 알아서 적당한 사이즈를 추천해 줬는데,&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/size-presets.png" alt="플리커의 이미지 크기 선택 화면"&gt;&lt;figcaption aria-hidden="true"&gt;플리커의 이미지 크기 선택 화면&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;이 서비스는 그렇지 않기 때문에 적당한 사이즈를 찾는 시행착오가 좀 필요하다. 나는 그냥 블로그 최대 길이를 넘어가는 사진만 적당히 줄였다. 룰이 단순하다면 빌드 과정에서 이미지의 사이즈를 측정한 뒤 알아서 적당한 길이를 넣어주는 것도 좋고, 아예 스마트크롭으로 다 같은 사이즈로 잘라버린 뒤 원본 링크를 제공하는 것도 방법이다.&lt;/p&gt;
&lt;h2 id="주의사항"&gt;주의사항&lt;/h2&gt;
&lt;p&gt;단순히 파일 호스팅용으로 사용한다면 상관없지만, 이미지 리사이즈 용으로 Netlify Large Media를 사용할 경우 Transform 기능의 월별 사용량을 확인하는 것이 좋다. 무료 계정을 사용할 경우 리사이즈는 한 달에 최대 2,500회까지만 지원한다(캐싱을 지원하기 때문에 매 방문시마다 리사이즈를 새로 하지는 않는다고 한다). 매일같이 이미지가 백 장씩 들어간 포스팅을 찍어내는 게 아니기 때문에 사실 기술 블로그를 운영하기엔 충분한 숫자긴 하다. &lt;code class="short-code"&gt;https://app.netlify.com/sites/{프로젝트-이름}/settings/large-media&lt;/code&gt;에서 내 한 달 사용량을 확인할 수 있으니, 남은 횟수가 간당간당하다면 충분히 작다고 생각하는 이미지는 원본 사이즈를 사용해서 최대한 Quota를 아끼는 것이 좋겠다.&lt;/p&gt;
&lt;p&gt;당연한 것이지만, 글을 발행하기 전에 로컬에서는 리사이즈 기능을 사용할 수 없다는 것이 불편할 수 있다. 정적 웹 사이트는 보통 watch 기능을 제공하므로 한 쪽엔 에디터를, 다른 한쪽엔 페이지를 띄워놓고 작업하는 경우가 많은데, 커다란 파일 하나가 렌더링을 가로막고 있으면 좀 답답해지긴 한다. 이미지 로딩과 렌더링에 걸리는 시간은 줄일 수 없지만, css에 이미지의 최대 길이를 지정해 주면 미리보기 화면 자체가 깨지지는 않게 할 수 있으니 지정해 두면 좋다. 실서비스에 배포하기 전에 Netlify의 &lt;a href="https://www.netlify.com/blog/2016/07/20/introducing-deploy-previews-in-netlify/" title="null" rel="noopener" target="_blank"&gt;Deploy Previews&lt;/a&gt; 기능으로 실서비스에 배포하기 전에 미리 사이즈가 적당한지 확인한다면 배포된 뒤 사진에 이상이 있음을 알고 부랴부랴 수정할 일을 줄일 수 있다.&lt;/p&gt;
&lt;h2 id="다른-방법은-없을까"&gt;다른 방법은 없을까?&lt;/h2&gt;
&lt;p&gt;이 홈페이지가 정적 웹사이트라는 사실을 이용하는 방법이다. js로 만들어진 훌륭한 이미지 프로세싱 라이브러리가 분명 있을 것이므로, 빌드 과정에 이미지를 받아다 리사이즈해주면 된다. 할 생각이 없었기 때문에 자료조사를 대충 했는데 &lt;a href="https://github.com/lovell/sharp" title="null" rel="noopener" target="_blank"&gt;sharp&lt;/a&gt;라는 라이브러리가 유명한 것 같다.&lt;/p&gt;
&lt;p&gt;서비스에 대해 독립적이며, 무료 플랜 같은 쓸데없는 고민 안 해도 되고, 로컬에서도 사용할 수 있다는 매력적인 장점이 있지만, 이미지가 많아질수록 빌드 시간이 길어진다는 단점이 있으며, 무엇보다 개발이 귀찮아서 그만두었다. 나중에 블로그가 충분히 원숙해지고 최적화도 충분히 됐다는 확신이 들면 그때쯤 도전해 보고 싶다.&lt;/p&gt;
]]&gt;</description><author>&lt;![CDATA[Kim Songhyun]]&gt;</author><pubDate>2025-02-05</pubDate></item><item><title>&lt;![CDATA[재택근무 일기]]&gt;</title><link>https://znagadeon.dev/post/remote-work/</link><description>&lt;![CDATA[&lt;p&gt;전국이 코로나 바이러스로 난리다. 회사 안에서는 지난 주쯤부터 재택근무 논의가 되고 있었는데, 2월 25일 드디어(?) 전사 단위의 재택근무 공지가 났다. 꽤 긴 시간을 재택근무로 보내게 되었고, 기존 업무와는 다른 새로운 점, 출퇴근하면서는 몰랐던 점들을 정리해 보면 좋을 것 같아 이 글을 쓰게 되었다.&lt;/p&gt;
&lt;p&gt;이 글을 마무리짓고 있는 3월 4일, 나는 여전히 재택근무 중이다. 원래는 매일 일기처럼 글을 남기고 재택근무가 끝난 뒤 정리해서 글을 올리려고 했는데, 이제 신박한 것은 다 접한 것 같고 더 이상 쓸 말도 생각나지 않아서 이 정도에서 끝내기로 했다. 더 쓸 말이 떠오르면 나중에 후기로 적든지 해야겠다.&lt;/p&gt;
&lt;h2 id="2월-25일"&gt;2월 25일&lt;/h2&gt;
&lt;p&gt;재택근무라면 &lt;a href="https://www.facebook.com/permalink.php?story_fbid=1395693333897551&amp;id=100003706936172" title="null" rel="noopener" target="_blank"&gt;2018년에 태풍 때문에&lt;/a&gt; 한 번 경험해본 적이 있다. 다만, 당시엔 어쨌든 하루만 버티면 된다! 라는 마음가짐이었기 때문에 조금 불편해도 상관없었지만, 이번 재택근무는 언제 끝날지도 정확히 모르는 상황이었다. 재택근무가 결정된 날이 25일이었는데, 그 주 주말쯤 재택근무를 얼마나 이어가야 할지 다시 한 번 논의해보겠다고 했다. 최소 3일, 길게는 2주 이상 집에서 일해야 할지도 모를 일이다. 릴리즈가 얼마 남지 않은 상황이라 회사 안에서 일하는 만큼은 아니더라도 하루에 &lt;code class="short-code"&gt;0.8*회사&lt;/code&gt; 정도의 생산성은 나와 줘야 원활한 업무가 가능할 것 같았다.&lt;/p&gt;
&lt;p&gt;회사 차원에서는 어떻게 생각했을지 모르겠지만, 최소한 조직장 레벨에서는 이 기회에 재택근무 시스템이 원활하게 돌아갈 수 있을지 시험해보는 듯한 느낌을 받았다. 혼자 일하는 느낌을 덜기 위해 파트 톡방에서 잡담을 권장하거나, 매일 아침 근무시간을 공유받거나(시스템적으로 공유가 되기는 하지만 업무중에 유동적으로 근무시간을 변경하는 경우가 많아서 직접 공유하는 편이 정확하다), 재택근무 후기에 대한 서베이를 받기도 했다. 좋은 리모트 워크 아이디어를 공유하는 움직임도 있었다. 나도 사내 커뮤니티에 리모트 워크 꿀팁 공유 스레드를 하나 만들었는데, 생각만큼 흥하지는 않았지만 그래도 좋아요를 좀 받았다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/good-tip.png" alt="화상회의 꿀팁을 특히 좋아해 주시더라"&gt;&lt;figcaption aria-hidden="true"&gt;화상회의 꿀팁을 특히 좋아해 주시더라&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;h3 id="재택-준비"&gt;재택 준비&lt;/h3&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/desk.jpg?nf_resize=fit&amp;w=600" alt=""&gt;&lt;figcaption aria-hidden="true"&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;일단 회사에서 노트북을 챙겨왔다. 바로 전날 회사가 너무 더워서 체온이 37.5도를 찍는 바람에(집에 오니 정상 체온으로 돌아왔다. &lt;del&gt;이게 다 회사 때문이다&lt;/del&gt;) 잠깐 재택근무를 체험해볼 기회가 생겼기 때문에 좀 더 여유롭게 준비물을 챙길 수 있었다.&lt;/p&gt;
&lt;p&gt;우리 회사는 특별한 이유가 없으면 무조건 맥북을 쓰도록 하고 있기 때문에 윈도 환경인 집과는 좀 차이가 있다. 아마 최근 생긴 IT기업이라면 거의 비슷할 것 같다. 나처럼 집과 회사의 개발환경에 차이가 날 경우 챙길 것이 좀 더 늘어난다. 모니터 변환 젠더와, 맥용 USB 허브를 챙겨와서 집에 있는 주변기기와 연결했다. 별 문제는 없었지만, 사람 욕심이 끝이 없다고 윈도 키맵으로 맥에서 타이핑하려니 조금 불편하다. 다음에 길게 재택할 일이 생기면 귀찮아도 키보드까지 챙겨야겠다. 다행히 이틀 뒤 도저히 이 칩거를 견딜 수 없다며 회사까지 드라이브를 다녀온 파트장님이 키보드를 챙겨다 주셨다.&lt;/p&gt;
&lt;p&gt;며칠 이상 나와 업무적으로 함께할 책상이기 때문에 쌓아놨던 잡다한 물건들을 전부 다른 곳으로 치우고 먼지도 깨끗이 닦아냈다. 책상이 케이블로 엉망이라 일단 맥북으로 잘 감추고 쿠팡에서 케이블타이를 주문했다.&lt;/p&gt;
&lt;p&gt;우리 파트에서는 재택근무 환경 경진대회(?)를 열었다. 근무환경이 제일 좋은 한 명을 뽑아서 재택근무가 끝나는 날 그 사람이 커피를 쏘기로 했다.&lt;/p&gt;
&lt;h3 id="버킷-리스트"&gt;버킷 리스트&lt;/h3&gt;
&lt;p&gt;재택근무는 원래부터 재택근무가 메인인 회사가 아닌 이상 당연히 기존 근무형태보다는 불편할 수밖에 없지만, 반대로 재택근무이기 때문에 할 수 있는 일들도 있을 것 같았다. 생각나는 대로 버킷 리스트를 만들고 하나씩 해 보기로 했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input checked="" disabled="" type="checkbox"&gt; 일하면서 빨래 돌리기 (2월 26일)&lt;/li&gt;
&lt;li&gt;&lt;input checked="" disabled="" type="checkbox"&gt; 점심 시간에 샤워하기&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;del&gt;집 앞 카페에서 일하기&lt;/del&gt;&lt;/li&gt;
&lt;li&gt;&lt;input checked="" disabled="" type="checkbox"&gt; 집에서 야근하기 (2월 28일)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="2월-26일"&gt;2월 26일&lt;/h2&gt;
&lt;h3 id="업무-일지"&gt;업무 일지&lt;/h3&gt;
&lt;p&gt;지켜보는 눈이 없다보니 나태해질 것 같아서 업무일지를 작성하기로 했다. 평소에도 업무일지 비슷한 것을 적긴 하지만 뭘 해야 하는지, 몇 시간이 걸렸는지 정도만 적는 편이었는데, 재택근무를 계기로 좀 더 자세하고 체계적으로 작성해봤다.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://notion.so" title="null" rel="noopener" target="_blank"&gt;Notion&lt;/a&gt;, &lt;a href="http://kanbanflow.com/" title="null" rel="noopener" target="_blank"&gt;KanbanFlow&lt;/a&gt; 등등 이것저것 도전해 봤는데, 원래 쓰던 대로 &lt;a href="https://workflowy.com/" title="null" rel="noopener" target="_blank"&gt;Workflowy&lt;/a&gt;에 업무일지를 적고, 근무시간 트래킹을 위해 &lt;a href="https://www.focustodo.cn/" title="null" rel="noopener" target="_blank"&gt;Focus To-Do&lt;/a&gt; 앱을 추가로 유료결제하여 사용하기로 했다. 필요한 만큼은 다 동작하기 때문에 만족스럽게 쓰고 있다. 이 앱은 나중에 기회가 되면 소개해봐야겠다.&lt;/p&gt;
&lt;h3 id="일하면서-빨래-돌리기"&gt;일하면서 빨래 돌리기&lt;/h3&gt;
&lt;p&gt;조금 시끄럽다는 것만 빼면 괜찮았다.&lt;/p&gt;
&lt;p&gt;그나마 출퇴근, 식사시간 잠깐 사이에 짬을 내어 걷고 움직이던 것이 줄어들어서 집안일의 비중을 늘렸다. 좀 더 구석구석 쓸고 닦았다는 뜻이다. 나는 업무 집중을 위해 뽀모도로 타이머를 사용하고 있는데, 재택근무를 계속하다 보니 왠지 억지로라도 몸을 움직여 줘야 할 것 같아서 쉬는 시간 5분을 알차게 사용하고 있다. 할 집안일이 다 떨어지면(매일 계속 생겨서 그럴 일은 없을 것 같지만) 가볍게 운동도 해 보면 어떨까 싶다.&lt;/p&gt;
&lt;h3 id="첫-화상회의"&gt;첫 화상회의&lt;/h3&gt;
&lt;p&gt;우리 회사는 G Suite을 사용하고 있다. 자연히 화상회의도 G Suite Meet(행아웃)을 사용하여 진행했다. 행아웃 미팅은 대학 다닐 때 몇 달 정도 한 적이 있어서 익숙하다고 생각했는데, 매일 사무실에서 보던 사람들을 웹캠으로 보니까 왠지 좀 남사스러웠다(?). 마이크나 이어폰을 준비해두지 않아서 걱정했는데, 생각보다 맥북 하나만으로도 회의하는 데 나쁘지 않았다. 다만 하울링을 신경쓰는 팀원들은 있는 것 같다.&lt;/p&gt;
&lt;p&gt;단점은 회의를 끝내면 왠지 혼자 남는 기분이 돼서 쓸쓸해진다는 것이다.&lt;/p&gt;
&lt;h2 id="2월-27일"&gt;2월 27일&lt;/h2&gt;
&lt;h3 id="모각코모여서-각자-코딩"&gt;모각코(모여서 각자 코딩)&lt;/h3&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/meet-code.png" alt=""&gt;&lt;figcaption aria-hidden="true"&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;쓸쓸하다는 느낌을 받는 건 나뿐만이 아니었던 것 같다. 팀원이 심심하면 들어오라며 미팅을 하나 열었길래 들어가서 코딩했다. 처음엔 좀 신경쓰였는데 나중엔 거기 있는 줄도 모르게 되더라. 다만 홀로 재택근무의 심심함을 해소하기에 충분한 방법은 아니었다. 역시 그냥 사람을 만나는 게 좋겠어.&lt;/p&gt;
&lt;h2 id="2월-28일"&gt;2월 28일&lt;/h2&gt;
&lt;p&gt;컨디션이 안 좋은 날이었다. 평소였으면 이럴 때 간식타임을 갖거나 해서 좀 회복되고 돌아올 텐데, 집에 있으니 잠깐 쉬는 것도 왠지 어뷰징하는 것 같아서 죄책감이 든다.&lt;/p&gt;
&lt;p&gt;이렇게 되니 어디까지를 오프로 보고 어디까지를 잠깐 휴식으로 봐야 하는지 조금 고민이 되었다. 회사에 있으면 어쨌든 자리에 앉아 있으니 일한다는 느낌을 받지 않는가. 생각해보니 그렇게 대충 시간을 보냈던 적도 있어서 조금 반성하게 되었다. 그렇다고 완전히 고쳐질 것 같지는 않고, 그냥 앞으론 좀 덜 농땡이피워야겠다고 생각했다.&lt;/p&gt;
&lt;p&gt;회사에서는 이 날 전염병 재택근무 가이드라인을 배포했다. 밖에 공유해도 되는 정보인지 확실하지 않아 여기에 전문을 올리는 것은 조심스럽지만, 결론적으로 재택근무는 당분간 무기한이다. 팀원 중에는 진짜 유치원, 초/중/고등학교가 개학할 때까지 재택근무를 하는 게 아니냐고 전망하는 분도 계셨다. 이때쯤 슬슬 밖에 나가서 사람 좀 만나고 싶다고 생각했던 것 같다.&lt;/p&gt;
&lt;h3 id="집-앞-카페에서-일하기"&gt;&lt;del&gt;집 앞 카페에서 일하기&lt;/del&gt;&lt;/h3&gt;
&lt;p&gt;다른 이유로 재택근무를 하는 거였다면 한 번쯤 해 봤을 텐데, 아무래도 전염병 때문에 그런 것이다 보니 그냥 나가는 것도 조심스럽게 된다. 마스크도 써야 하고... 대신 도저히 참을 수 없게 되었을 때 집 앞 카페에 가서 단 음료를 하나 사 왔다.&lt;/p&gt;
&lt;h3 id="집에서-야근하기"&gt;집에서 야근하기&lt;/h3&gt;
&lt;p&gt;재택근무를 하면 한 번쯤 꼭 해봐야겠다고 생각했었는데... 회사에서야 일을 붙잡고 있다가 정신을 차려 보니 퇴근시간을 넘겼더라... 하는 일이 잦았는데, 집에 있으니까 그게 잘 안 된다. 야근을 한다 해도 30분 정도? 확실히 업무환경이 집이다 보니 집중력이 좀 떨어진다. 아래 단점 란에도 적었지만 일부러 업무시간이 끝나면 그냥 퇴근 찍고 업무하던 것들을 다 종료해버리기도 했고... 그냥 업무시간 집중력을 뽑아내는 게 합리적일 듯하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="총평"&gt;총평&lt;/h2&gt;
&lt;h3 id="장점"&gt;장점&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;출퇴근 시간이 없다는 것. 나는 출퇴근에 하루에 1시간씩 쓰고 있는데, 이게 길바닥에 시간을 갖다버리는 것으로 느껴질 만큼 할 수 있는 일이 많았다.&lt;/li&gt;
&lt;li&gt;몸단장에 좀 덜 신경써도 된다.&lt;/li&gt;
&lt;li&gt;집안일에 시간을 더 쏟게 된다. 집이 점점 깨끗해진다.&lt;/li&gt;
&lt;li&gt;노래를 열창하고 헤드뱅잉을 하면서 코딩을 해도 아무도 뭐라 하지 않는다.&lt;/li&gt;
&lt;li&gt;회사 입장에서 느낄 만한 메리트도 있다. 집에 보내놓으면 감시하는 눈이 없어서 놀고먹는 거 아닐까 생각할 법 한데, 오히려 딴짓을 하면 너무 본격적으로 노는 것 같아서 죄의식이 들어 다시 일로 돌아온다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="단점"&gt;단점&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;사람 보기가 힘들다. 이건 이 재택근무가 그냥 리모트 워크가 아니라, 코로나 바이러스에 의한 격리에 가까운 형태이기 때문에 특히 더 그런 듯하다. 근처 사는 팀원들이랑 모각코를 한다든지, 하다못해 근처 카페에 가서라도 일하면 훨씬 덜 외로울 것 같다.&lt;/li&gt;
&lt;li&gt;출퇴근 시간이 없다는 것. 일터와 휴식 공간이 분리되지 않는다. 퇴근을 했는데도 같은 공간에 있어야 하니 내가 퇴근을 한 건지 안 한 건지 마음으로 와닿지 않는다. &lt;strong&gt;뒤돌면 침대&lt;/strong&gt;라는 말은 &lt;strong&gt;눈 뜨면 일터&lt;/strong&gt;라는 말과 같은 것이라는 것을 뼈저리게 느꼈다. 어느 쪽이 더 자신에게 크게 다가오느냐에 따라 호불호가 갈릴 것 같다.&lt;ul&gt;
&lt;li&gt;이건 내가 원룸에 살아서 더 크게 느끼는 것 같다. 하다못해 투룸 정도만 됐어도 훨씬 덜했을 것이다.&lt;/li&gt;
&lt;li&gt;출근 준비, 퇴근 준비라는 게 단순히 준비하는 것도 있지만 내 머릿속의 업무 모드를 켜고 끄는 과정이라는 생각도 들었다. 나는 업무시간에는 되도록 침대에 가까이 가지 않고, 출근 전 꼭 세수는 하기로 했다. 퇴근하면 남은 할 일이 좀 아쉽더라도 미련없이 모든 업무 프로그램을 종료했다. 의식적으로 출퇴근한다는 느낌을 주는 것이 장기적으로는 좋을 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;아이가 있는 팀원들은 아이 보는 문제로 크게 고생하시는 것 같았다. 재택근무가 끝날 때까지 장기휴가를 진지하게 고민하시는 분들도 있었다.&lt;/li&gt;
&lt;li&gt;집의 업무 환경이 좋지 않은 경우 직접적으로 타격을 받는다. 앞에서 설명한 육아 문제도 있고, 방음이 안 된다든지, 집에 인터넷이 안 된다든지 하면 이래저래 곤란한 상황이 된다.&lt;/li&gt;
&lt;li&gt;억지로 몸을 움직여줘야 한다. 회사는 하다못해 화장실을 갈래도 1분 이상 걸어가야 하는데 집에 있으면 앞구르기를 해서도 갈 수 있다. 타이머를 맞춰 두고 30분에 한 번 정도 스트레칭을 하는데 이것만으로는 모자란 느낌이다. 출퇴근 전후로 맨몸운동을 하는 것도 좋겠다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;재택 근무라는 것이, 단순히 집에서 일하는 것도 있지만, 회사(정확히는 사무실)라는 것이 무엇인가에 대해 깊은 고찰을 할 수 있게도 해 주는 것 같다. 난생 처음 회사를 &amp;#39;일단 가는 곳&amp;#39;이 아니라. &amp;#39;업무할 수 있는 최적의 공간을 갖춰둔 곳&amp;#39;이라는 생각을 갖게 되었다.&lt;/p&gt;
&lt;p&gt;혼자 살면서 외로움을 많이 타는 사람이나, 집에 있으면 아이를 돌봐야 하는 사람들에게 리모트 워크는 장점보다는 단점이 많이 부각될 것 같다. 반대로 혼자서 잘 먹고 잘 사는 사람이라면 정말 꿈같은 시간이었을 것이다. 출퇴근 제도, 리모트 워크 제도 각각의 장단이 아주 뚜렷해서, 어느 한쪽이 우세하다고 말하기 힘들었다. 바꿔 말하면, 일주일에 하루는 자유롭게 재택근무를 하게 해 준다든지 하는 방식으로 두 제도를 상호 보완적으로 이용할 수 있다면, 구직자 입장에서 아주 큰 복지 내지는 메리트로 느끼지 않을까 하는 생각이다.&lt;/p&gt;
]]&gt;</description><author>&lt;![CDATA[Kim Songhyun]]&gt;</author><pubDate>2025-02-05</pubDate></item><item><title>&lt;![CDATA[새 맥북 세팅하기 - Homebrew를 중심으로]]&gt;</title><link>https://znagadeon.dev/post/setup-mac-with-homebrew/</link><description>&lt;![CDATA[&lt;p&gt;우리 회사는 2년 단위로 노트북을 새것으로 교체해준다. 재택근무 중이라 다시 회사로 돌아갔을 때나 교체받을 수 있을 줄 알았는데 회사에서 노트북을 퀵으로 쏴 줬다! 일단 정말 끔찍한 사용성을 보여줬던 나비식 키보드를 벗어난 것만으로 감지덕지다.&lt;/p&gt;
&lt;p&gt;회사에서 노트북을 교체했다면 빨리 원래 쓰던 노트북을 반납해야 하기 때문에 마이그레이션 툴로 후다닥 옮겨준 뒤 다시 일하러 갔겠지만, 재택근무 덕에 장비 반납이 유예돼서 천천히 세팅을 점검할 여유가 생겼다. 이왕 작업하는 김에 나중에 또 있을지도 모를 셋업을 준비하는 것도 괜찮겠다 싶어 Homebrew로 맥북 세팅하는 법을 정리해보기로 했다.&lt;/p&gt;
&lt;h2 id="설치"&gt;설치&lt;/h2&gt;
&lt;p&gt;설치는 이 한 줄이면 끝난다. 공식 홈페이지 맨 앞에서 확인해볼 수 있다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;/bin/bash -c &lt;span class="hljs-string"&gt;&amp;quot;&lt;span class="hljs-subst"&gt;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&lt;/span&gt;&amp;quot;&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;이후 설치하는 패키지는 &lt;code class="short-code"&gt;brew install&lt;/code&gt;을 사용한다. 예를 들면 git은 이렇게 설치할 수 있다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;brew install git&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h2 id="gui-툴을-설치해보자---cask와-mas"&gt;GUI 툴을 설치해보자 - cask와 mas&lt;/h2&gt;
&lt;p&gt;나는 개인적으로 cask와 mas가 있어서 맥 셋업이 극단적으로 간단해질 수 있었다고 생각한다. 이 페이지 저 페이지 찾아다니면서 다운받지 않고 스크립트 한 줄만으로 거의 모든 프로그램을 다운받을 수 있게 해 준다. 공식 홈페이지에 찾아가서 다운로드 버튼을 누르지 않아도 된다니 이건 혁명이다.&lt;/p&gt;
&lt;p&gt;설치는 역시 Homebrew로 한다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;brew install cask&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;brew install mas&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;간단한 예시를 들어보자. 구글 크롬과 카카오톡을 받아 보겠다. mas는 이름으로는 설치할 수 없고 해당 앱의 아이디를 찾아야 하는데, &lt;code class="short-code"&gt;mas search&lt;/code&gt;라는 커맨드로 검색 가능하다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;# 크롬&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;brew install --cask google-chrome&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;# 카카오톡&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;mas search kakaotalk &lt;span class="hljs-comment"&gt;# 869223134 KakaoTalk (2.8.4)&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;6&lt;/code&gt;&lt;code class="code"&gt;mas install 869223134&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;참고로, 2년 전에 사용하던 맥북에서는 &lt;code class="short-code"&gt;brew cask install&lt;/code&gt; 이런 식으로 설치했는데, 새 맥북에 테스트해보니 명령어가 &lt;strong&gt;&lt;code class="short-code"&gt;brew install --cask&lt;/code&gt;로 교체&lt;/strong&gt;되었다고 한다. 이런 식으로 가끔 명령어나 패키지명이 바뀌는 경우가 있으니 공식 홈페이지에서 해당 패키지를 미리 검색해보는 것이 좋다. &lt;a href="https://formulae.brew.sh/cask/google-chrome#default" title="null" rel="noopener" target="_blank"&gt;예를 들어 구글 크롬을 설치한다면 이렇게 검색해보면 된다.&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="brewfile로-미래에-대비하기"&gt;Brewfile로 미래에 대비하기&lt;/h2&gt;
&lt;p&gt;이 글을 읽고 감동한 어떤 독자가 나처럼 모든 패키지를 Homebrew만으로 관리하려고 결심했다고 해보자. 하지만 미리부터 준비하지 않았다면 내 컴퓨터에 무엇이 깔려있는지조차 모르는 상태일 것이다. 맥북을 싹 밀어버리고 아예 바닥부터 다시 시작해야 할까? &lt;del&gt;물론 그것도 좋겠지만&lt;/del&gt; 쉽지 않은 도전이 될 것이다. 기존에 깔아뒀던 패키지를 목록화하고, 이왕이면 나중에 다시 갖다 쓸 수 있다면 좋겠다. 이럴 때 Homebrew의 번들 기능을 사용하면 좋다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;brew bundle dump &lt;span class="hljs-comment"&gt;# Brewfile 생성&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;brew bundle &lt;span class="hljs-comment"&gt;# 현재 디렉토리 내의 Brewfile을 기반으로 패키지를 설치함&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;단, 이 명령어는 Homebrew로 설치하지 않은 패키지까지 전부 번들링해주지는 않는다(mas는 로컬에서 앱 목록을 긁어오는 건지 자동으로 번들링이 되었다). Brewfile을 찬찬히 살펴보면서 필요없는 건 삭제하고, 꼭 필요한데 brew로 설치하지 않았던 패키지는 삭제한 뒤 brew로 재설치하는 정도의 노력은 필요하다. 다행히 Brewfile은 파일 구조가 간단한 편이라 수정이 비교적 쉽다. 버전정보조차 표시하지 않는다.&lt;/p&gt;
&lt;p&gt;이렇게 해서 완성된 Brewfile을 클라우드 스토리지 같은 데 올려두면, 앞으로 맥을 새로 세팅할 때마다 프로그램을 설치하는 과정을 자동화할 수 있다.&lt;/p&gt;
]]&gt;</description><author>&lt;![CDATA[Kim Songhyun]]&gt;</author><pubDate>2025-02-05</pubDate></item><item><title>&lt;![CDATA[jest와 mock-fs로 fs 유닛 테스트하기]]&gt;</title><link>https://znagadeon.dev/post/test-fs/</link><description>&lt;![CDATA[&lt;p&gt;파일시스템 유닛 테스트는 까다롭다. DB/API 테스트와 마찬가지로, 원래라면 건드리면 안 되는 외부 시스템에 영향을 주기 때문이다. 보통 DB 테스트는 자신의 로컬 머신을 이용하든지, 테스트 전용 서버를 따로 파든지 해서 이 문제를 회피한다. 이 흐름을 파일시스템 테스트에 그대로 적용하면 테스트용 디렉토리를 따로 파야 한다. 못 할 건 아니지만 좀 지저분하고 번거롭다.&lt;/p&gt;
&lt;p&gt;그래서 보통 파일시스템 메서드를 모킹해서 테스트를 진행한다. node.js에서는 &lt;a href="https://github.com/tschaub/mock-fs" title="null" rel="noopener" target="_blank"&gt;mock-fs&lt;/a&gt;라는 라이브러리와 jest의 mock function을 이용하면 된다.&lt;/p&gt;
&lt;p&gt;사용법은 간단한 편이다. 일단 jest와 mock-fs를 설치하자.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-comment"&gt;# 작성 시점 기준으로 jest@^25.1.0, mock-fs@^4.10.4 이 설치됨&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;npm i -D jest mock-fs&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h2 id="fsreadfile-테스트"&gt;&lt;code class="short-code"&gt;fs.readFile&lt;/code&gt; 테스트&lt;/h2&gt;
&lt;p&gt;mock-fs는 &lt;code class="short-code"&gt;mock&lt;/code&gt;이라는 함수로 일종의 가상 파일시스템을 만들어 준다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; fs = &lt;span class="hljs-built_in"&gt;require&lt;/span&gt;(&lt;span class="hljs-string"&gt;&amp;#x27;fs&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; mock = &lt;span class="hljs-built_in"&gt;require&lt;/span&gt;(&lt;span class="hljs-string"&gt;&amp;#x27;mock-fs&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;describe(&lt;span class="hljs-string"&gt;&amp;#x27;sample test&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-function"&gt;() =&amp;gt;&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;    it(&lt;span class="hljs-string"&gt;&amp;#x27;mock-fs test&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-function"&gt;() =&amp;gt;&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;        mock({&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-attr"&gt;fake&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;                &lt;span class="hljs-attr"&gt;dir&lt;/span&gt;: {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;                    &lt;span class="hljs-string"&gt;&amp;#x27;file.txt&amp;#x27;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;#x27;content&amp;#x27;&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;                },&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;            },&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-string"&gt;&amp;#x27;file2.txt&amp;#x27;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;#x27;&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-comment"&gt;// 빈 파일 생성&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;13&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-attr"&gt;empty&lt;/span&gt;: {}, &lt;span class="hljs-comment"&gt;// 빈 디렉토리 생성&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;14&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-string"&gt;&amp;#x27;path/to/file.txt&amp;#x27;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;#x27;&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-comment"&gt;// full path를 써줄 수도 있다&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;15&lt;/code&gt;&lt;code class="code"&gt;        });&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;16&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;17&lt;/code&gt;&lt;code class="code"&gt;        fs.readFile(&lt;span class="hljs-string"&gt;&amp;#x27;./file2.txt&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-function"&gt;(&lt;span class="hljs-params"&gt;err, str&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;18&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-comment"&gt;// relative path 사용&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;19&lt;/code&gt;&lt;code class="code"&gt;            &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;20&lt;/code&gt;&lt;code class="code"&gt;        });&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;21&lt;/code&gt;&lt;code class="code"&gt;    });&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;22&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;23&lt;/code&gt;&lt;code class="code"&gt;    afterEach(&lt;span class="hljs-function"&gt;() =&amp;gt;&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;24&lt;/code&gt;&lt;code class="code"&gt;        mock.restore();&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;25&lt;/code&gt;&lt;code class="code"&gt;    });&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;26&lt;/code&gt;&lt;code class="code"&gt;});&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;주의할 점은, 테스트를 종료하기 전에 반드시 &lt;code class="short-code"&gt;mock.restore()&lt;/code&gt;를 실행해 주어야 한다는 점이다. 내가 테스트에 사용할 때는 없어도 딱히 문제는 없었으나, 공식 문서에 따르면 몇몇 상황에서 문제가 일어나는 경우가 있다고 한다. 내가 그 케이스일 수 있으니 꼬박꼬박 종료해주자.&lt;/p&gt;
&lt;h3 id="withfiletypes-문제"&gt;&lt;del&gt;&lt;code class="short-code"&gt;withFileTypes&lt;/code&gt; 문제&lt;/del&gt;&lt;/h3&gt;
&lt;p&gt;node 10.10.0 버전부터 &lt;code class="short-code"&gt;fs.readdir&lt;/code&gt;에 옵션으로 &lt;code class="short-code"&gt;withFileTypes&lt;/code&gt;를 줄 수 있게 되었다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;└─dir1&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;   ├─file1&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;   └─dir1&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;      └─file2&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;이런 파일 구조가 있다고 할 때,&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-built_in"&gt;console&lt;/span&gt;.log(&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    fs.readdirSync(&lt;span class="hljs-string"&gt;&amp;#x27;./dir1&amp;#x27;&lt;/span&gt;, { &lt;span class="hljs-attr"&gt;withFileTypes&lt;/span&gt;: &lt;span class="hljs-literal"&gt;true&lt;/span&gt; })&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;        .map(&lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;dirent&lt;/span&gt; =&amp;gt;&lt;/span&gt; dirent.isFile())&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;); &lt;span class="hljs-comment"&gt;// &amp;gt; [false, true] (파일명에 따라 순서는 바뀔 수 있음)&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;이런 식으로, 디렉토리 안에 있는 오브젝트가 파일인지, 디렉토리인지 등등을 쉽게 알 수 있어 편리하다.&lt;/p&gt;
&lt;p&gt;&lt;del&gt;다만 아직 &lt;code class="short-code"&gt;mock-fs&lt;/code&gt;가 이 기능을 지원하지 않아, 테스트 작성 시 테스트가 깨지는 문제가 있다.&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;&lt;del&gt;&lt;a href="https://github.com/tschaub/mock-fs/commit/ec9b2671884378e9a53554499891ca5c3f50b9a6" title="null" rel="noopener" target="_blank"&gt;해당 문제를 해결한 커밋&lt;/a&gt;이 마스터 브랜치에 머지된 것으로 보아, 아마 다음 버전쯤엔 해결되지 않을까 싶다. 일단 나는 해당 옵션을 사용하는 테스트는 테스트를 스킵하도록 처리해두었다.&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;해당 이슈는 mock-fs 4.11.0 버전에서 해결되었다. 이제는 걱정하지 말고 테스트 작업을 하도록 하자.&lt;/p&gt;
&lt;h2 id="fswritefile-테스트"&gt;&lt;code class="short-code"&gt;fs.writeFile&lt;/code&gt; 테스트&lt;/h2&gt;
&lt;p&gt;&lt;code class="short-code"&gt;mock-fs&lt;/code&gt;로는 파일 쓰기 테스트를 할 수 없다. 필요한 쓰기 함수를 jest로 모킹하여 해당 문제를 해결할 수 있다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;describe(&lt;span class="hljs-string"&gt;&amp;#x27;sample test&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-function"&gt;() =&amp;gt;&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;    fs.writeFileSync = jest.fn();&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; write = jest.spyOn(fs, &lt;span class="hljs-string"&gt;&amp;#x27;writeFileSync&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;    fs.mkdirSync = jest.fn();&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; mkdir = jest.spyOn(fs, &lt;span class="hljs-string"&gt;&amp;#x27;mkdirSync&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;    it(&lt;span class="hljs-string"&gt;&amp;#x27;write test&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-function"&gt;() =&amp;gt;&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;        fs.writeFileSync(&lt;span class="hljs-string"&gt;&amp;#x27;./test.txt&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;#x27;content&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;        expect(write).toHaveBeenCalledWith(&lt;span class="hljs-string"&gt;&amp;#x27;./test.txt&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;#x27;content&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;    });&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;13&lt;/code&gt;&lt;code class="code"&gt;    it(&lt;span class="hljs-string"&gt;&amp;#x27;mkdir test&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-function"&gt;() =&amp;gt;&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;14&lt;/code&gt;&lt;code class="code"&gt;        fs.mkdirSync(&lt;span class="hljs-string"&gt;&amp;#x27;dir&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;15&lt;/code&gt;&lt;code class="code"&gt;        expect(mkdir).toHaveBeenCalledWith(&lt;span class="hljs-string"&gt;&amp;#x27;dir&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;16&lt;/code&gt;&lt;code class="code"&gt;    });&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;17&lt;/code&gt;&lt;code class="code"&gt;});&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;]]&gt;</description><author>&lt;![CDATA[Kim Songhyun]]&gt;</author><pubDate>2025-02-05</pubDate></item><item><title>&lt;![CDATA[sharp.js로 자동 워터마크 이미지 제작하기]]&gt;</title><link>https://znagadeon.dev/post/watermark-with-sharp-js/</link><description>&lt;![CDATA[&lt;p&gt;최근에 카톡에 깃헙 링크를 공유하다가 멋진 오픈그래프를 발견했다. 처음엔 깃헙에 오픈그래프 이미지를 넣을 수 있는 기능이 생긴 건 줄 알았는데 깃헙에서 자동으로 만들어주는 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/opengraph.png" alt="내 레포도 잘 나온다"&gt;&lt;figcaption aria-hidden="true"&gt;내 레포도 잘 나온다&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;문득 이걸 내 블로그에도 적용해보면 멋지겠다고 생각했다. 예전에 &lt;a href="/post/netlify-large-media" title="null" rel="noopener" target="_blank"&gt;Netlify Large Media로 이미지 호스팅하는 법&lt;/a&gt;에 대해서 글을 썼던 적이 있는데 그 때 염두에 두고 있었던 라이브러리인 &lt;a href="https://sharp.pixelplumbing.com" title="null" rel="noopener" target="_blank"&gt;sharp.js&lt;/a&gt;를 사용해 보기로 했다. 내 목표는 오픈그래프를 제작하는 것이지만, 그러려면 글 메타정보도 불러와야 하고 맨 첫 이미지 가져오는 기능도 만들어야 하고 귀찮아질 것 같아서 이 글에서는 이미지 사이즈를 자동으로 조절하고 워터마크를 만들어 보기로 하겠다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;워터마크란?&lt;/p&gt;
&lt;p&gt;이미지의 저작자를 표시하기 위해 이미지 위에 추가하는 텍스트 내지는 이미지를 말한다. 보통 일러스트레이터나 만화가들이 자신이 만든 저작물을 홍보할 때 이미지 불펌을 막기 위해 결과물의 화질을 낮추고 그 위에 큰 텍스트를 덮어씌워서 배포하곤 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="설치하기"&gt;설치하기&lt;/h2&gt;
&lt;p&gt;sharp.js는 node 10 이상 버전부터 지원한다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;npm install sharp&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;직접적인 관련은 없지만, 텍스트, 그라디언트 등 개발자가 동적으로 생성한 컨텐츠를 집어넣기 위해서는 일반적으로 SVG를 이용한다. 여러가지 방법이 있겠지만 나는 &lt;a href="https://github.com/jsdom/jsdom" title="null" rel="noopener" target="_blank"&gt;jsdom&lt;/a&gt; 위에서 &lt;a href="https://d3js.org" title="null" rel="noopener" target="_blank"&gt;D3&lt;/a&gt;로 생성한 SVG를 사용하려고 한다. 이 글에서도 SVG 조작에는 D3를 사용하고 있다. D3를 사용할 줄 모르면 이해하기 어려운 부분이 있을 수 있으니 이 부분은 미리 양해를 구한다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;npm i d3 jsdom&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h2 id="이미지-불러오고-저장하기"&gt;이미지 불러오고 저장하기&lt;/h2&gt;
&lt;p&gt;가장 기본적인 이미지 객체 생성법은 파일 이름으로 불러오는 방식이다. 여기서는 이미 있는 이미지를 수정하는 것이므로 파일명으로 불러오는 방식을 사용하겠다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; sharp = &lt;span class="hljs-built_in"&gt;require&lt;/span&gt;(&lt;span class="hljs-string"&gt;&amp;#x27;sharp&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;(&lt;span class="hljs-keyword"&gt;async&lt;/span&gt; () =&amp;gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; image = &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; sharp(&lt;span class="hljs-string"&gt;&amp;#x27;./my-image.jpg&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;})();&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;완성된 이미지는 다양한 포맷으로 저장할 수 있는데 이 때도 파일로 저장할지, Buffer 객체를 받을지를 결정할 수 있다. 전자는 완성된 파일을 저장할 때 쓰고 후자는 불러온 이미지를 다른 이미지에 추가한다거나 할 때 사용한다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;(&lt;span class="hljs-keyword"&gt;async&lt;/span&gt; () =&amp;gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; image.png().toFile(&lt;span class="hljs-string"&gt;&amp;#x27;./output.png&amp;#x27;&lt;/span&gt;); &lt;span class="hljs-comment"&gt;// png 파일로 저장&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; image.jpeg().toFile(&lt;span class="hljs-string"&gt;&amp;#x27;./output.jpg&amp;#x27;&lt;/span&gt;); &lt;span class="hljs-comment"&gt;// jpg 파일로 저장&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; image.png().toBuffer(); &lt;span class="hljs-comment"&gt;// png 포맷 Buffer 객체로 저장&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;})();&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;어떤 포맷으로 입출력을 받을 수 있는지는 공식 문서에서 확인할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://sharp.pixelplumbing.com/api-constructor" title="undefined" rel="noopener" target="_blank"&gt;https://sharp.pixelplumbing.com/api-constructor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sharp.pixelplumbing.com/api-output" title="undefined" rel="noopener" target="_blank"&gt;https://sharp.pixelplumbing.com/api-output&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="보너스-axios로-웹에서-불러온-이미지를-sharp-객체로-만들기"&gt;보너스: axios로 웹에서 불러온 이미지를 sharp 객체로 만들기&lt;/h3&gt;
&lt;p&gt;옵션에 &lt;code class="short-code"&gt;responseType: &amp;#39;arraybuffer&amp;#39;&lt;/code&gt;를 추가하면 sharp.js에서 바로 사용할 수 있는 버퍼 스트링으로 이미지를 내려받을 수 있다. gravatar처럼 웹에 저장해 둔 이미지를 불러와야 하는 상황에서 유용하다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; getImageFromWeb = &lt;span class="hljs-keyword"&gt;async&lt;/span&gt; (url) =&amp;gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; axios.get(url, {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;responseType&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;#x27;arraybuffer&amp;#x27;&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;    }).then(&lt;span class="hljs-function"&gt;(&lt;span class="hljs-params"&gt;{ data }&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; sharp(data);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;6&lt;/code&gt;&lt;code class="code"&gt;    });&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;7&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h2 id="이미지-크기-줄이기"&gt;이미지 크기 줄이기&lt;/h2&gt;
&lt;p&gt;웹페이지에 넣을 이미지는 지나치게 크기가 크고 화질이 좋으면 남들의 소중한 데이터를 고갈시키기 쉽다. 개발자 블로그에 올라오는 이미지래봐야 참고 이미지에 가까우니 적절한 크기로 사이즈를 줄여서 데이터 부담을 줄여주도록 하자. width, height 중 한 값만 주어질 경우 나머지 값은 적절하게 조정된다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;(&lt;span class="hljs-keyword"&gt;async&lt;/span&gt;() =&amp;gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; resized = &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; image.resize({ &lt;span class="hljs-attr"&gt;width&lt;/span&gt;: &lt;span class="hljs-number"&gt;800&lt;/span&gt; });&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;})();&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h3 id="이미지-크기-재기"&gt;이미지 크기 재기&lt;/h3&gt;
&lt;p&gt;가로 길이가 800픽셀보다 작은 이미지를 800픽셀에 맞추면 이미지가 깨질 것이다. 이미지의 크기를 미리 알 수 있다면 이런 상황을 미연에 방지할 수 있다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;(&lt;span class="hljs-keyword"&gt;async&lt;/span&gt;() =&amp;gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; maxWidth = &lt;span class="hljs-number"&gt;800&lt;/span&gt;;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; { width } = &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; image.metadata();&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; (width &amp;gt; maxWidth) {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;6&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; resized = &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; image.resize({ &lt;span class="hljs-attr"&gt;width&lt;/span&gt;: maxWidth });&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;7&lt;/code&gt;&lt;code class="code"&gt;    }&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;8&lt;/code&gt;&lt;code class="code"&gt;})();&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;h2 id="이미지에-컨텐츠-추가하기"&gt;이미지에 컨텐츠 추가하기&lt;/h2&gt;
&lt;p&gt;이미지 크기를 줄였으니 진짜 본격적으로 이미지에 워터마크를 추가해 보자. 로고나 홈페이지 주소 같은 것을 넣으면 좋을 것 같으니 이미지와 텍스트 넣는 방법을 알면 되겠다.&lt;/p&gt;
&lt;h3 id="이미지에-다른-이미지-추가하기"&gt;이미지에 다른 이미지 추가하기&lt;/h3&gt;
&lt;p&gt;이미지를 다른 이미지에 추가하기 위해서는 추가할 이미지를 Buffer 객체로 바꾸어 주어야 한다. 이건 아래 소개할 텍스트 삽입에서도 마찬가지이다. 공식 문서에서는 일반 문자열도 지원한다고 되어 있는데 실제로 실행해 보면 에러가 난다. 워터마크가 워터마크를 붙이려는 이미지보다 크면 에러가 나니 적절히 사이즈를 조절해주도록 하자.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;(&lt;span class="hljs-keyword"&gt;async&lt;/span&gt;() =&amp;gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; image = &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; sharp(&lt;span class="hljs-string"&gt;&amp;#x27;./my-image.png&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; watermark = &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; sharp(&lt;span class="hljs-string"&gt;&amp;#x27;./watermark.png&amp;#x27;&lt;/span&gt;).toBuffer();&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;})();&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;이미지를 포함한 컨텐츠 삽입 시에는 &lt;code class="short-code"&gt;composite&lt;/code&gt;이라는 메서드를 사용한다. 입력으로는 배열을 받으며, 뒤에 삽입한 컨텐츠일수록 위쪽에 나타난다. 포토샵을 만져본 적 있는 사람이라면 쉽게 이해할 수 있을 듯하다. 컨텐츠는 특정 위치를 지정하여 삽입할 수도 있지만, &lt;code class="short-code"&gt;gravity&lt;/code&gt; 옵션으로 정중앙, 오른쪽 등 적절한 위치에 알아서 배치되게 만들 수도 있다. 물론 그랬다가는 마진 없이 너무 구석에 척 붙어버리므로 예쁘게 만들고 싶으면 &lt;code class="short-code"&gt;top&lt;/code&gt;, &lt;code class="short-code"&gt;left&lt;/code&gt;를 사용하자. 이미지가 너무 바깥으로 나가면 나간 만큼이 잘려나간다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt;1&lt;/code&gt;&lt;code class="code"&gt;(&lt;span class="hljs-keyword"&gt;async&lt;/span&gt;() =&amp;gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;2&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; watermarked = &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; image.composite([{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;3&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;input&lt;/span&gt;: watermark,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;4&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;gravity&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;#x27;southeast&amp;#x27;&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;5&lt;/code&gt;&lt;code class="code"&gt;    }]);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;6&lt;/code&gt;&lt;code class="code"&gt;})();&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/gravity.png" alt="gravity: southeast를 사용한 예시. 오른쪽 아래 끝부분에 딱 붙어 있다"&gt;&lt;figcaption aria-hidden="true"&gt;gravity: southeast를 사용한 예시. 오른쪽 아래 끝부분에 딱 붙어 있다&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;h3 id="이미지에-텍스트-집어넣기"&gt;이미지에 텍스트 집어넣기&lt;/h3&gt;
&lt;p&gt;마지막으로 이미지에 텍스트를 추가해 보자. sharp.js 자체에는 텍스트를 자동생성하는 기능 같은 것은 없다. 다만, 이미지 입력 포맷으로 SVG를 지원하기 때문에 아까 소개한 D3와 jsdom을 이용해 동적으로 텍스트 이미지를 생성해낼 수 있다. 이미지와 마찬가지로 완성한 SVG는 Buffer 객체로 변환하여 넣어주자. 글자 너비를 측정할 수 있다면 width를 넣지 않아도 될 텐데 아직 거기까지는 어떻게 하는지 모르겠다.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;div&gt;&lt;code class="line-number"&gt; 1&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; { JSDOM } = &lt;span class="hljs-built_in"&gt;require&lt;/span&gt;(&lt;span class="hljs-string"&gt;&amp;#x27;jsdom&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 2&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; d3 = &lt;span class="hljs-built_in"&gt;require&lt;/span&gt;(&lt;span class="hljs-string"&gt;&amp;#x27;d3&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 3&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 4&lt;/code&gt;&lt;code class="code"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; generateText = &lt;span class="hljs-function"&gt;(&lt;span class="hljs-params"&gt;text, width, height, fontSize, color&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 5&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; dom = &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; JSDOM(&lt;span class="hljs-string"&gt;&amp;#x27;&amp;lt;body&amp;gt;&amp;lt;/body&amp;gt;&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 6&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; body = d3.select(dom.window.document.body);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 7&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; svg = body.append(&lt;span class="hljs-string"&gt;&amp;#x27;svg&amp;#x27;&lt;/span&gt;)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 8&lt;/code&gt;&lt;code class="code"&gt;        .attr(&lt;span class="hljs-string"&gt;&amp;#x27;width&amp;#x27;&lt;/span&gt;, width)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt; 9&lt;/code&gt;&lt;code class="code"&gt;        .attr(&lt;span class="hljs-string"&gt;&amp;#x27;height&amp;#x27;&lt;/span&gt;, height)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;10&lt;/code&gt;&lt;code class="code"&gt;        .attr(&lt;span class="hljs-string"&gt;&amp;#x27;xmlns&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;#x27;http://www.w3.org/2000/svg&amp;#x27;&lt;/span&gt;);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;11&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;12&lt;/code&gt;&lt;code class="code"&gt;    svg.append(&lt;span class="hljs-string"&gt;&amp;#x27;text&amp;#x27;&lt;/span&gt;)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;13&lt;/code&gt;&lt;code class="code"&gt;        .text(text)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;14&lt;/code&gt;&lt;code class="code"&gt;        .attr(&lt;span class="hljs-string"&gt;&amp;#x27;x&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-number"&gt;0&lt;/span&gt;)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;15&lt;/code&gt;&lt;code class="code"&gt;        .attr(&lt;span class="hljs-string"&gt;&amp;#x27;y&amp;#x27;&lt;/span&gt;, fontSize)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;16&lt;/code&gt;&lt;code class="code"&gt;        .style(&lt;span class="hljs-string"&gt;&amp;#x27;font-size&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-string"&gt;`&lt;span class="hljs-subst"&gt;${fontSize}&lt;/span&gt;px`&lt;/span&gt;)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;17&lt;/code&gt;&lt;code class="code"&gt;        .style(&lt;span class="hljs-string"&gt;&amp;#x27;fill&amp;#x27;&lt;/span&gt;, color);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;18&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;19&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; body.html();&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;20&lt;/code&gt;&lt;code class="code"&gt;};&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;21&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;22&lt;/code&gt;&lt;code class="code"&gt;(&lt;span class="hljs-keyword"&gt;async&lt;/span&gt;() =&amp;gt; {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;23&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; meta = &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; image.metadata();&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;24&lt;/code&gt;&lt;code class="code"&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;25&lt;/code&gt;&lt;code class="code"&gt;    &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; watermarked = &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; image.composite([{&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;26&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-comment"&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;27&lt;/code&gt;&lt;code class="code"&gt;    }, {&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;28&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;input&lt;/span&gt;: Buffer.from(generateText(&lt;span class="hljs-string"&gt;&amp;#x27;Hello, world!&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-number"&gt;100&lt;/span&gt;, &lt;span class="hljs-number"&gt;16&lt;/span&gt;, &lt;span class="hljs-number"&gt;16&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;#x27;white&amp;#x27;&lt;/span&gt;)),&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;29&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;top&lt;/span&gt;: meta.height - &lt;span class="hljs-number"&gt;32&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;30&lt;/code&gt;&lt;code class="code"&gt;        &lt;span class="hljs-attr"&gt;left&lt;/span&gt;: &lt;span class="hljs-number"&gt;16&lt;/span&gt;,&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;31&lt;/code&gt;&lt;code class="code"&gt;    }]);&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code class="line-number"&gt;32&lt;/code&gt;&lt;code class="code"&gt;})();&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure&gt;&lt;img src="./assets/text-watermark.png" alt="텍스트 워터마크를 삽입한 이미지"&gt;&lt;figcaption aria-hidden="true"&gt;텍스트 워터마크를 삽입한 이미지&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;
&lt;h2 id="정리"&gt;정리&lt;/h2&gt;
&lt;p&gt;이렇게 해서 이미지를 적절한 사이즈로 줄이고, 이미지나 텍스트로 된 워터마크를 추가하여 내 블로그의 저작물임을 표시할 수 있도록 만들어 보았다. 이 밖에도 sharp.js에서 지원하는 여러가지 기능들이 있는데 잘만 이용하면 거의 포토샵을 코드로 짜는 수준까지 가능할 것 같다. 언제가 될지는 모르겠지만 다음에는 좀 더 미려한 예제를 만들어서 소개해보겠다.&lt;/p&gt;
]]&gt;</description><author>&lt;![CDATA[Kim Songhyun]]&gt;</author><pubDate>2025-02-05</pubDate></item></channel></rss>