<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>sming.log</title>
        <link>https://velog.io/</link>
        <description>딩구르르</description>
        <lastBuildDate>Tue, 25 Mar 2025 19:44:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>sming.log</title>
            <url>https://velog.velcdn.com/images/baby_dev/profile/d83b8c99-cf8a-4fd8-b17a-7f18bd3cfcd5/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. sming.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/baby_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Next.js app router 써야되나요?]]></title>
            <link>https://velog.io/@baby_dev/Next.js-app-router-%EC%8D%A8%EC%95%BC%EB%90%98%EB%82%98%EC%9A%94</link>
            <guid>https://velog.io/@baby_dev/Next.js-app-router-%EC%8D%A8%EC%95%BC%EB%90%98%EB%82%98%EC%9A%94</guid>
            <pubDate>Tue, 25 Mar 2025 19:44:42 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>매우매우 주관적인 글입니다. 저는 정답이 아닙니다. 제가 느낀 거를 작성한 일기라 생각하고 봐주세용</strong></p>
</blockquote>
<p>안녕하세요. 저는 next 13.0.0에서부터 production 환경에서 app router를 운영했던, app router를 가장 오랜 기간 사용한 사람 중 한 명입니다. 소박한 Next.js Contributor이기도 하죠.</p>
<p>현재의 app router에서 사용하는 메모리보다 2배 이상 소모될 때부터, 서버 컴포넌트에서 컴포넌트별 ISR이 되지 않았을 때부터 사용하고 있었죠. 물론 현재는 이와 같은 문제가 많이 해결된 상태입니다. 그래서 13.0.0이 나온 지 어느덧 2년이 넘었고, 이제는 사이드 프로젝트에서도, 기업에서도 app router를 많이 채택하고 있습니다.</p>
<p>그렇다면 이 app router, 이제는 쓰면 될까요?</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/1e423eb6-5d51-4eaf-a64d-53358b8d4eda/image.png" alt=""></p>
<p>뭐 제가 쓰란다고 쓰고, 제가 쓰지 말라고 안 쓰겠습니까.. 그냥 개인적으로 제가 느끼는 것을 얘기해보려고 합니다. 저는 app router를 사용하는 데에는 많은 트레이드오프를 고려해야 된다고 생각합니다.</p>
<p>먼저 app router를 사용했을 때 얻게 되는 이점은 어떤 게 있을까요? 가장 큰 장점은 컴포넌트별 렌더링 전략을 선택할 수 있다는 것이죠. 기존에는 page 단위로 통째로 SSR, ISR, SSG를 선택해야 했던 것을 컴포넌트별로 선택할 수 있다는 점이 매우 인상적입니다.</p>
<p>또 Parallel Routes, Intercepting Routes, Server Action, fetch 병렬 처리 같은 app router에서만 되는 특수 기능들도 제공하고 있습니다.</p>
<p>그리고 서버 컴포넌트를 이용했을 때 번들 사이즈가 전혀 없다라는 점도 있겠군요.</p>
<hr>
<p>그렇다면 app router를 썼을 때 단점은 뭐가 있을까요? 사실 저는 이것을 말하려고 이 글을 작성하고 있습니다 :)</p>
<p><strong>먼저 위에 작성한 저 이점들을 크게 이용하고 있지 않습니다.</strong></p>
<p>한 페이지에 ISR, SSR, SSG를 선택적으로 여러 개 이용하는 경우는 크게 없기 때문이죠.</p>
<p>Parallel Routes, Intercepting Routes도 유사합니다. 사용할 일도 그렇게 크게 많지 않으며, 충분히 이러한 UI 형태를 구현하기 위해 app router의 이 기능들을 사용하지 않아도 됩니다. Server action은 Next.js 하나로만 프론트, 백엔드를 모두 처리하는 상황이 아니면 크게 사용할 일은 없다고 생각됩니다.</p>
<p>또한 서버 컴포넌트에 우리가 실제로 넣을 수 있는 DOM은 그렇게 많지 않으며, 이로 얻을 수 있는 zero-bundle의 이점은 거의 없다고 생각됩니다.</p>
<p>또한 단점은 계속 쏟아집니다.</p>
<p><strong>app router의 사용을 위해 아예 스타일 라이브러리를 변경하는 경우도 있습니다.</strong></p>
<p>기존에 scss, module css 같이 사용을 해왔다면 page router → app router의 변경에도 그렇게 critical하지는 않을 것입니다.</p>
<p>하지만 styled-components, emotion과 같은 runtime css-in-js 들을 사용했을 때는 이를 서버 컴포넌트에서 사용할 수 없습니다. 그렇기에 서버 컴포넌트는 오직 데이터를 fetch하고 이를 client component에 넘겨주는 것밖에 할 수 없게 되죠.</p>
<p>그렇다고 이 app router 사용을 위해 기존에 사용하던 스타일 라이브러리를 scss, module css 혹은 tailwind css, panda css, vanilla-extract로 바꾸는 건 옳을까요? 물론 app router를 그 정도로 이용해야 될 이유가 있다면 괜찮다고 생각하지만, 기존에 쓰던 스타일 라이브러리를 모두 마이그레이션하면서까지 app router의 서버 컴포넌트에 스타일을 입혀야 할까요??</p>
<p><strong>또한 공통 라이브러리에서 app router를 한 번 더 고려해야 된다는 것도 생각보다 꽤 큰 문제입니다.</strong></p>
<p>대부분의 회사가 app router를 쓰더라도 page router + app router라고 생각됩니다. 모든 페이지가 app router로 마이그레이션된 곳은 크게 없다고 생각합니다. 이렇게 page router + app router로 되는 순간부터 공통적으로 사용하고 있는 라이브러리에서는 고려해야 될 부분이 2배가 됩니다.</p>
<p>일단 page router, app router 모두 Next.js라는 하나의 프레임워크에 의존성을 가지고 있지만, 단지 어떤 router를 사용하느냐에 따라 내부에서 참조하는 메서드들은 달라지기 때문입니다.</p>
<p>예를 들어 page router에서는 <code>next/router</code>를 이용해 router 처리를 하는데, app router에서는 <code>next/navigation</code>을 이용해 router 처리를 하죠. 그리고 <code>next/navigation</code>의 router에서는 <code>router.on</code>과 같은 router에 이벤트를 걸 수 없어 page router에서 처리하던 방식을 그대로 사용할 수 없죠.</p>
<p>또한 page router에서는 req를 통해 안에서 header 정보들을 가져와 처리하는데, app router에서는 <code>headers()</code>, <code>cookies()</code>라는 메서드를 제공하여 처리를 하죠.</p>
<p>그리고 compound component pattern과 같이 <code>&lt;Select.Option&gt;</code>처럼 사용하는 dot notation이 app router에서는 오류를 내뱉는 문제도 존재합니다. 이러한 이슈 때문에 이미 다른 radix ui와 같은 UI 라이브러리에서는 실제로 app router 대응을 위해서 dot notation을 떼내는 처리도 진행하였습니다. 그렇기에 사내 공통 라이브러리에 compound component pattern으로 dot notation을 사용했다면 제거해야 되는 문제도 존재합니다.</p>
<p>이는 생각보다 공통 라이브러리를 개발할 때 많이 피로를 가져오게 됩니다. Next.js에 의존이 없는 게 최고의 라이브러리겠지만, 어쩔 수 없이 의존이 생기는 경우에는 app router, page router 모두 고려하여 2벌을 만들어야 되기 때문이죠.</p>
<p><strong>무엇보다 그냥 개발이 피로합니다.</strong></p>
<p><code>프레임워크를 사용한다.</code> 이는 정말 개발의 용이성을 위해서 사용한다고 생각합니다. 실제로 Next.js 같은 경우 내부적으로 제공하고 있는 최적화 옵션이나 <code>next.config.js</code>로 모든 걸 제어하는 편의성은 개발을 많이 편하게 하죠.</p>
<p>하지만 app router는 기본적으로 server component로 사용되며, client component 이용을 위해서는 <code>use client</code>를 붙여야 하는 번거로움이 존재하며, app router를 사용하는 가장 큰 이유인 <code>server component</code>를 사용한 순간부터 개발의 난이도가 확 올라가게 됩니다.</p>
<p>먼저 최상위 <code>page.tsx</code>, <code>layout.tsx</code>는 반드시 서버 컴포넌트로 유지되어 있어야 할 것, 서버 컴포넌트를 이용했다면 클라이언트 컴포넌트에서는 서버 컴포넌트를 import할 수 없기에 반드시 children으로 넣어야 할 것, 이로 인해 하나의 컴포넌트를 만들 때 정말 데이터를 뿌려주는 server component wrapper가 존재하고, 클라이언트 컴포넌트는 그 데이터를 props에서 받아서 그리기만을 진행하죠. 즉, 컴포넌트 하나를 구현하기 위해 쓸데없이 2개의 다른 컴포넌트가 생겨나게 됩니다.</p>
<p><strong>이로 인해서 처음 app router를 개발하는 사람 입장에서는 실수를 많이 진행하고는 합니다.</strong><br>예를 들어 <code>page.tsx</code>, <code>layout.tsx</code>를 실수로 클라이언트 컴포넌트로 두고 시작하곤 합니다. 이런 경우에는 최상위 컴포넌트가 클라이언트 컴포넌트가 됐기에 절대로 이 route에서는 서버 컴포넌트를 이용할 수 없습니다. (클라이언트 컴포넌트에서는 서버 컴포넌트 import 불가)</p>
<p>그리고 서버 컴포넌트는 클라이언트 컴포넌트에서 반드시 children으로 사용해야 된다는 룰이 있는데, 이는 사실 안 지킨다고 해서 런타임에서 아무런 에러를 내지 않습니다. 그냥 아무도 모르게 동작이 이상하게 됩니다. 서버 컴포넌트인 줄 알고 썼는데 클라이언트 컴포넌트처럼 동작하고는 하죠. Next.js는 이를 에러로 잡아주지 않습니다.</p>
<hr>
<p>자 여기서 의견을 정리해보겠습니다.</p>
<p>보통 회사에서 app router를 사용하게 되면 page router와 함께 공존하는 경우가 대부분입니다.</p>
<ol>
<li>그렇다면 공통 라이브러리 만드는 개발자 A는 Next.js 의존성이 있는 라이브러리를 만들 때마다 app router, page router 두 벌을 만들어야 될 것입니다.  </li>
<li>app router를 주로 개발하던 B, page router를 주로 개발하던 C가 만약 팀이 바뀌어 각각 다른 router를 건드리게 된다면 각각의 app router, page router에서 지키고 있던 규칙들, 코딩 방식들을 다시 익혀야 할 것입니다. (같은 Next.js임에도 불구하고)  </li>
<li>app router를 위해 스타일 라이브러리를 변경하려고 합니다. 변경을 하면 꽤나 비용은 들겠지만 서버 컴포넌트에서도 스타일을 사용할 수 있게 됩니다. 하지만 useState, useEffect도 사용하지 못하고, event handler도 사용하지 못하고, window 객체에도 접근할 수 없는 초-정적인 서버 컴포넌트에 스타일을 입혀서 써봤자 얼마나 쓸 수 있을까요? 이를 위해 스타일 라이브러리를 변경하는 게 맞을까요?  </li>
<li>Parallel Routes, Intercepting Routes, Server action 실제로 많이 사용하나요..? 이것 사용을 위해 app router를 사용하는 게 맞을까요? 게다가 어마어마하게 구린 폴더 구조들을 보게 될 겁니다.. route group, intercepting routes, private folder, parallel routes가 섞인 순간부터 이게 장난치는 것과 같아 보이는 기괴한 폴더 구조의 연속들일 겁니다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/523ba4ca-9f5f-451c-9a9f-44d5af0427e5/image.png" alt="">  </p>
<blockquote>
<p>새로운 기능 추가 시 *folder라도 생길지 모름...</p>
</blockquote>
<p>아무튼 전 다음과 같은 이유로 app router 사용을 하고 싶지 않습니다. 제가 아직까지 느꼈던 경험으로는 app router를 쓸 이유가 쓰고 싶지 않은 이유보다 크지 않아 보이기 때문입니다.</p>
<hr>
<h3 id="진짜-문제는-nextjs-그-자체">진짜 문제는 Next.js 그 자체</h3>
<p>하지만 app router를 안 쓴다? 그거는 어려울지도 모릅니다.</p>
<p>Next.js 팀은 이제 page router는 단순한 유지보수, 그리고 앞으로 새로운 기능 추가, 개선 같은 것들은 app router 중심적으로 될 예정이기 때문이죠. 하지만 라이브러리가 더 이상 개선되지 않는다… 그건 죽은 라이브러리와 같죠. 결국 Next.js는 점진적인 app router 전환으로 모두를 이끌 것입니다. 아예 React 팀과 협업을 하여 server component를 이끌어가기 때문에 이 app router를 영원히 배척하겠다는 것도 참 어려워 보입니다.</p>
<p>마치 yarn v1, 그리고 yarn berry를 보는 것 같습니다. 대부분의 사람들이 아직 많이 이용하지만 업데이트는 유기된 yarn v1, 그리고 앞으로 모든 업데이트는 zero-install을 내세운 pnp 전략을 지닌 yarn berry가 받게 되죠… 이와 다를 게 없어 보입니다.</p>
<p>개발자들은 yarn berry의 pnp 기능의 사용하기 어려움과 <code>.yarn/cache</code>의 git 저장소에서의 관리, 가끔 충돌하는 의존성 문제로 인해 꺼려하곤 합니다. 그렇기에 그냥 yarn v1도 yarn berry도 아닌 <code>pnpm</code>을 많이 이용하곤 합니다.</p>
<p>이와 같이 Next.js도 사용하기에는 불편하게 하면서 기존 page router 대신 app router를 계속 발전시킨다면 <code>pnpm</code> 마냥 Next.js 대신에 다른 것을 이용하는 게 좋을지도 모릅니다.</p>
<hr>
<h3 id="nextjs-app-router-써야-되나요">Next.js <del>app router</del> 써야 되나요?</h3>
<p>앞에서 얘기했듯 사실 app router에서 더 나아가 Next.js를 써야 할까… 라는 의문이 들곤 합니다.</p>
<ul>
<li>리액트에 이미 의존돼 있는 우리의 프로젝트를 Next.js에 한 번 더 의존하여 나아가는 것이 좋은 방향일까요?</li>
<li>Next.js의 app router는 계속 어떤 방향으로 발전할까요?  </li>
<li>5년 뒤에 리액트는 쓸 수 있겠지만, 5년 뒤에 Next.js도 동일하게 사용하고 있을까요?  </li>
<li>정말 만약에 5년 뒤에 React에서 새로운 라이브러리로 메타가 바뀌면 어떻게 될까요? React만 쓰는 프로젝트는 React를 다른 라이브러리로 마이그레이션하면 될 것입니다. 하지만 Next.js까지 여기 엮여 있는 순간부터는 Next.js 걷어내기 작업까지 2배, 그 이상으로 힘든 마이그레이션이 되지 않을까요?</li>
</ul>
<p>뭐 그거 외에도 단점이라면 얼마든지 있습니다..</p>
<ul>
<li>Next.js의 내부 블랙박스. 우리가 모르는 너무 많은 게 Next.js 안에 숨어 있다. 사실 React만으로도 돌아가는데 지장은 없으나 너무 많은 것을 제공하고 있다. 최근에도 발생한 Next.js <a href="https://news.hada.io/topic?id=19922">보안 이슈</a>도 동일하다.</li>
<li><code>서버를 사용한다.</code> 이 하나만으로 프론트에서 신경 써야 할 것이 매우 많아진다. 백엔드 서버 운영함과 유사하게 프론트엔드의 서버도 지속적인 모니터링이 필요하고, 이에 대응하는 인프라 설정 역시 요구된다. <strong>물론 서버에 대한 추가적인 비용은 당연히 보너스</strong></li>
<li><code>next/image</code>와 같이 성능 최적화하는 것을 제공하면서 그 최적화를 SSR 서버와 동일한 서버가 한다는 거를 잊어서는 안 된다. 최적화를 하는 거는 공짜가 아니다. 그것도 Next의 서버가 하는 것이다. SSR과 이미지 최적화가 하나의 서버에서 같이 처리되고 있다면 메모리 이슈는 물론, 사용자가 많아질 경우 둘 중 하나는 하자가 생길 수 있다.</li>
<li>React 빌드에 비해 빌드 속도가 너무 느리다… 로컬 환경도 아무리 개선을 했다 하더라도 느리다… (영겁의 HMR)</li>
<li>버전 업데이트될수록 복잡성만 올라감</li>
</ul>
<hr>
<h2 id="결론">결론</h2>
<p>뭐 이렇게는 작성했지만 SEO가 정말 정말로 중요한 프로젝트 같은 경우에는 Next.js를 사용할 듯합니다. SEO가 별로 필요 없다면 무조건 React를 사용할 듯하고요. 물론 여유가 있어, 회사에서 내부 SSR 서버를 직접 node로 구축할 수 있다면 Next.js를 버리고 React + SSR 서버 스택으로 갈 듯합니다.</p>
<p>여러분들도 일단 Next.js를 사용하기보다는, 굳이 SEO가 필요하지 않아도 된다면 React를 사용해보면 어떨까요?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[nginx를 이용한 웹에서의 zstd 압축 활성화]]></title>
            <link>https://velog.io/@baby_dev/front-end-nginx%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9B%B9%EC%97%90%EC%84%9C%EC%9D%98-zstd-%EC%95%95%EC%B6%95-%ED%99%9C%EC%84%B1%ED%99%94</link>
            <guid>https://velog.io/@baby_dev/front-end-nginx%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9B%B9%EC%97%90%EC%84%9C%EC%9D%98-zstd-%EC%95%95%EC%B6%95-%ED%99%9C%EC%84%B1%ED%99%94</guid>
            <pubDate>Wed, 09 Oct 2024 18:46:27 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@baby_dev/front-end-zstd-%EC%9B%B9-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94%EC%9D%98-%EC%83%88%EB%A1%9C%EC%9A%B4-%EB%8F%84%EC%95%BD">지난 글</a>에 zstd의 존재, 그리고 장점에 대해서 알아보았고 이번에는 실제로 nginx를 이용하여 지원하는 환경에 따라 zstd compression을 지원하도록 해볼것입니다.</p>
<h2 id="zstd-nginx-module">zstd-nginx-module</h2>
<p>먼저 nginx에서 zstd 압축을 지원하는지부터 확인이 필요한데요. 다행히도 <a href="https://github.com/tokers/zstd-nginx-module">zstd-nginx-module</a>이라는 모듈이 존재하여 다음을 이용하여 zstd 압축을 이용할 수 있어보입니다.</p>
<p>이제 이것을 우리의 nginx &amp; docker 환경에서 한번 이용을 해보도록 하겠습니다. 먼저 zstd-nginx-module을 install을 해야합니다. </p>
<p><a href="https://nginx-extras.getpagespeed.com/modules/zstd/#__tabbed_1_2">https://nginx-extras.getpagespeed.com/modules/zstd/#__tabbed_1_2</a> 다음 문서에서 module을 install 하는 방법이 있는데요.</p>
<p><strong>문제는 <code>yum</code>, <code>dnf</code> 형식으로만 install할 수 있기에 기존에 사용하던 nginx base image를 사용할 수 없습니다. (nginx base image는 alpine linux 기반이기에 apt 방식)</strong></p>
<pre><code class="language-dockerfile"># 기존 Dockerfile

FROM nginx:1.25.0-alpine-slim

ARG PROFILE
ENV PROFILE=${PROFILE}

RUN mkdir /etc/nginx/env

COPY ./conf.d /etc/nginx/conf.d
COPY ./location /etc/nginx/location
COPY ./variable /etc/nginx/variable
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./env/.env.${PROFILE} /etc/nginx/env/.env.${PROFILE}</code></pre>
<p>그렇기에 우리는 기존 nginx base image 대신 fedora image에서 <code>dnf</code> 를 이용하여 nginx와 zstd-nginx-module을 이용해야합니다.</p>
<pre><code class="language-dockerfile"># zstd-nginx-module 이용

FROM fedora:latest

ARG PROFILE
ENV PROFILE=${PROFILE}

RUN dnf -y install https://extras.getpagespeed.com/release-latest.rpm \
    &amp;&amp; dnf -y install nginx nginx-module-zstd \
    &amp;&amp; dnf -y install gettext

RUN mkdir /etc/nginx/env

COPY ./conf.d /etc/nginx/conf.d
COPY ./location /etc/nginx/location
COPY ./variable /etc/nginx/variable
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./env/.env.${PROFILE} /etc/nginx/env/.env.${PROFILE}</code></pre>
<p>docker base image를 fedora를 이용하고 nginx-module-zstd, nginx를 설치하고 envsubst이용을 위한 gettext를 추가해줍니다.</p>
<hr>
<p>이제 설치가 되었으니 실제 <code>nginx.conf</code>에 적용이 필요한데요. 적용자체는 매우 간단합니다. load_module을 이용하여 
<code>ngx_http_zstd_filter_module.so</code>, <code>ngx_http_zstd_static_module.so</code> 2개의 모듈을 추가하면 됩니다.</p>
<pre><code class="language-nginx">user  nginx;
worker_processes  auto;

error_log  /dev/stdout;
pid        /var/run/nginx.pid;

load_module modules/ngx_http_zstd_filter_module.so; # 이 부분 추가
load_module modules/ngx_http_zstd_static_module.so; # 이 부분 추가

events {
    worker_connections  1024;
}</code></pre>
<p>그 후에는 nginx에 gzip을 추가하듯이 압축을 위한 zstd 설정도 추가하면 됩니다.</p>
<pre><code class="language-nginx">    zstd on;
    zstd_types text/plain text/css application/json application/javascript text/html text/xml application/xml;
    zstd_comp_level 6;
    zstd_min_length 256;</code></pre>
<ul>
<li><code>zstd on</code>: zstd 압축을 활성화합니다.</li>
<li><code>zstd_types</code>: zstd 압축을 적용할 mime types들을 추가합니다.</li>
<li><strong><code>zstd_comp_level</code></strong>: zstd의 압축 레벨입니다. 1 ~ 22사이의 수치로 정할 수 있고 숫자가 높아질수록 압축률이올라가지만 그에 비례하여 압축시간, cpu 사용률도 늘어나게 됩니다. (수치가 올라갈수록 올라가는 압축률보다 압축시간이 비약적으로 올라가기에 6으로 설정하였습니다.)</li>
<li><code>zstd_min_length</code>: zstd 압축을 적용할 최소 용량입니다. 256byte이상이면 zstd압축이 되도록 설정하였습니다.</li>
</ul>
<h2 id="브라우저-호환성을-고려한-압축-설정">브라우저 호환성을 고려한 압축 설정</h2>
<p>이제 zstd 압축이 가능한 상황까지는 만들었습니다. 하지만 이전 글에도 언급했듯이 아직 zstd 같은 경우 최신 브라우저에서만 이용가능하고 모든 브라우저에서 지원하는 상황은 아닙니다.</p>
<p>그렇기에 우리는 zstd를 지원하는 경우에는 zstd를, 지원하지 않는 경우에는 gzip을 이용하도록 설정할 것 입니다.</p>
<p>그러기 위해 <code>Accept-Encoding</code> 값을 본후 zstd가 있는 경우에는 zstd가 우선적으로 적용하고 그렇지 않는 경우에는 gzip을 적용하도록 하였습니다.</p>
<p>그리고 <code>Content-Encoding</code> Response header에 <code>gzip</code>, <code>zstd</code> 둘 중 하나를 넣어 브라우저에게 어떤 압축 알고리즘을 적용할것인지 알리면 됩니다.</p>
<pre><code class="language-nginx">location / {
    proxy_pass http://$web_ip:3000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme; 

    set $encoding_type &quot;&quot;;

    if ($http_accept_encoding ~* &quot;gzip&quot;) {
        set $encoding_type &quot;gzip&quot;;
    }

    if ($http_accept_encoding ~* &quot;zstd&quot;) {
        set $encoding_type &quot;zstd&quot;;
    }

    proxy_set_header Content-Encoding $encoding_type;
}</code></pre>
<h2 id="nextjs-compress-고려하기">Next.js compress 고려하기</h2>
<p>이렇게 적용후에 실제로 테스트를 해보니 zstd가 적용되지 않고 계속 gzip만 나오는 이슈가 있었습니다. 원인을 파악한 결과 바로 Next.js의 compress 설정과 연관이 있었습니다.</p>
<p><a href="https://nextjs.org/docs/app/api-reference/next-config-js/compress">https://nextjs.org/docs/app/api-reference/next-config-js/compress</a> 이 문서에도 나타나있듯 compress를 사용하면 nextjs가 내장해서 gzip 압축을 적용합니다. 그렇기에 nginx를 사용해서 gzip, zstd 압축을 이용하려면 <code>next.config.js</code>의 compress옵션을 false로 처리해야합니다.</p>
<h2 id="실제-적용-결과">실제 적용 결과</h2>
<p>다음과 같이 최신 크롬에서는 정적인 파일에 대해서 <code>Content-Encoding: zstd</code>가 적용되어 압축된 상태를 볼 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/ae0cbb41-968b-4752-bee3-d4e49e0f4043/image.png" alt=""></p>
<p>그렇다면 zstd를 지원하지 않는 safari 브라우저에선 어떨까요? 우리가 원하는대로 zstd대신에 gzip으로 압축된 상태를 볼 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/b649f91b-e157-461b-89a8-5388f53a635f/image.png" alt=""></p>
<p>그래서 실제로 많은 용량이 감소했을까요?</p>
<p><strong>사실 파일용량은 크게 줄지는 않았습니다.</strong> 이전글에서 용량별 <code>gzip vs zstd</code>를 비교한것을 보면 50kb대에서는 크게 gzip, zstd 압축 차이가 많이 나지는 않습니다. 또한 Next.js가 자체적 코드스플리팅으로 한 파일용량이 그렇게 커지지 않기때문이기도 하죠.</p>
<p>그렇다면 gzip을 쓰나 zstd를 쓰나 큰 차이가 없는걸까요?</p>
<p><strong>그것은 아닙니다. 왜냐하면 저희는 압축속도, 압축해제속도에 대해서 gzip에 비해 2배 빠른 속도를 얻기도 하고 <code>zstd</code>의 진가는 용량이 커졌을때 나타나기때문에 만약 용량이 큰 라이브러리들을 많이 사용하게 될 경우 큰 효과를 얻을 수 있다고 생각됩니다.</strong></p>
<p>현재 <code>tiptap</code>을 통하여 커스텀 웹 에디터를 서비스에 붙히고 있는데 용량이 꽤나 크기때문에 적용을 마친뒤 zstd를 통해 어느정도의 용량 이점을 얻었는지 추가로 공유해볼 예정입니다.</p>
<h2 id="결론">결론</h2>
<p>zstd 써보세요^^ 호환성이 유일한 단점이지만 설명했듯이 <code>Accept-Encoding</code>을 통하여 브라우저 지원에 따라 gzip, zstd를 모두 지원해준다면 안 쓸 이유는 없어보입니다. 👍</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[zstd, 웹 성능 최적화의 새로운 도약]]></title>
            <link>https://velog.io/@baby_dev/front-end-zstd-%EC%9B%B9-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94%EC%9D%98-%EC%83%88%EB%A1%9C%EC%9A%B4-%EB%8F%84%EC%95%BD</link>
            <guid>https://velog.io/@baby_dev/front-end-zstd-%EC%9B%B9-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94%EC%9D%98-%EC%83%88%EB%A1%9C%EC%9A%B4-%EB%8F%84%EC%95%BD</guid>
            <pubDate>Wed, 09 Oct 2024 17:01:22 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요. 또 근 5개월만에 돌아와서 글을 쓰네요. 오늘 공유드릴 내용은 <a href="https://github.com/facebook/zstd">zstd</a>라는 압축 알고리즘입니다.</p>
<p>다들 <code>gzip</code>, <code>brolti</code>는 웹 성능 최적화를 할때 한번쯤 들어봤다고 생각됩니다. 하지만 <code>zstd</code>라는 키워드는 생소하다고 생각됩니다.</p>
<p>저도 <code>zstd</code>에 대해서 알게된지는 오래되지 않았는데요. 사실 <code>zstd</code> 압축 알고리즘는 2015년에 나와 글 작성기준 9년이 됬습니다. 물론 <code>gzip</code>은 32년, <code>brolti</code>는 11년이 되었기에 비교적 최근 알고리즘이라고 할 수 있습니다.</p>
<p>하지만 우리는 왜 gzip, brolti에 대해서는 흔하게 알고 사용해왔지만 zstd는 들어본적이 없을까요?</p>
<p><strong>바로 그 이유는 웹이 <code>zstd</code> decompression을 지원한지는 얼마 되지 않기때문입니다.</strong> 최신 나오는 문법, 기술에 대해서 가장 빠른 대응을 하는 chrome 마저도 2023년 6월에 지원을 시작했기 때문이죠.</p>
<p>하지만 실제로 이러한 zstd를 웹에 적용한 사례는 거의 없기에 제가 이번에 직접 굴러서 실험해보았습니다.
<strong>만약 현재 웹에서 사용할 수 있다면 <code>웹 성능 최적화의 새로운 도약</code>을 할 수 있게 될겁니다.</strong></p>
<hr>
<h2 id="zstd">zstd</h2>
<p>일단 zstd에 대한 간단한 설명을 드리자면 zstd(Zstandard) 는 2015년에 Facebook에서 개발한 고속 압축 알고리즘입니다.</p>
<p>주요 특징이라고하면 gzip에 비해서 빠른 압축속도와 더 높은 압축률을 자랑합니다. 벤치 마킹상 2<del>5배 빠른 압축속도를 자랑한다고 합니다. 그리고 1</del>22레벨의 압축레벨을 가지고 있습니다.</p>
<p>실제 사례로는 facebook, aws, uber등의 큰 기업에서도 zstd를 도입하여 서버로그 및 테이블 용량을 최적화한 사례가 있습니다.</p>
<h2 id="zstd-vs-gzip">zstd vs gzip</h2>
<p>아래는 제가 직접 파일들을 zstd, gzip 최적화를 진행해본 결과입니다.</p>
<h3 id="31mb">3.1Mb</h3>
<p><strong>압축률</strong></p>
<ul>
<li><img src="https://velog.velcdn.com/images/baby_dev/post/866a4445-9106-4513-8dde-b7311ce3047d/image.png" alt=""></li>
</ul>
<p>gzip(740kb) vs zstd(632kb)</p>
<p><strong>압축시간</strong></p>
<ul>
<li><p><img src="https://velog.velcdn.com/images/baby_dev/post/dea2a043-de47-44f0-a63a-3e025405eaf1/image.png" alt=""></p>
</li>
<li><p><img src="https://velog.velcdn.com/images/baby_dev/post/4eea948a-0bf3-4f93-bc10-a11032123f01/image.png" alt=""></p>
</li>
</ul>
<p>gzip(0.099s) vs zstd(0.051s)</p>
<h3 id="18mb">1.8MB</h3>
<p><strong>압축률</strong></p>
<ul>
<li><img src="https://velog.velcdn.com/images/baby_dev/post/41824b67-4002-4f76-b7ca-7625a5093500/image.png" alt=""></li>
</ul>
<p>gzip(404kb) vs zstd(348kb)</p>
<p><strong>압축시간</strong></p>
<ul>
<li><p><img src="https://velog.velcdn.com/images/baby_dev/post/4b037b28-f6da-4f29-9b19-1c3aa1a1f659/image.png" alt=""></p>
</li>
<li><p><img src="https://velog.velcdn.com/images/baby_dev/post/6896a77a-0a44-4eae-ba30-60ef4f73a2ec/image.png" alt=""></p>
</li>
</ul>
<p>gzip(0.071s) vs zstd(0.080s)</p>
<h3 id="560kb">560kb</h3>
<p><strong>압축률</strong></p>
<ul>
<li><img src="https://velog.velcdn.com/images/baby_dev/post/57371fea-1f31-4fe3-8d47-adafe1e1b4a1/image.png" alt=""></li>
</ul>
<p>gzip(164kb) vs zstd(152kb)</p>
<p><strong>압축시간</strong></p>
<ul>
<li><img src="https://velog.velcdn.com/images/baby_dev/post/f33a3bdd-b6e3-4aea-97de-029c12894dfb/image.png" alt=""></li>
<li><img src="https://velog.velcdn.com/images/baby_dev/post/5470f216-a883-4fdf-8b85-8c86f45ea343/image.png" alt=""></li>
</ul>
<p>gzip(0.044s) vs zstd(0.047s)</p>
<h3 id="312kb">312kb</h3>
<p><strong>압축률</strong></p>
<ul>
<li><img src="https://velog.velcdn.com/images/baby_dev/post/cb5c0590-679b-4c09-a57b-18313f6ef722/image.png" alt=""></li>
</ul>
<p>gzip(68kb) vs zstd(64kb)</p>
<p><strong>압축시간</strong></p>
<ul>
<li><p><img src="https://velog.velcdn.com/images/baby_dev/post/6c2c8c5d-2716-49cb-aa33-61920b3991d0/image.png" alt=""></p>
</li>
<li><p><img src="https://velog.velcdn.com/images/baby_dev/post/9131fd4a-437d-49d3-b5b5-ee358dc618fc/image.png" alt=""></p>
</li>
</ul>
<p>gzip(0.048s) vs zstd(0.028s)</p>
<h3 id="156kb">156kb</h3>
<p><strong>압축률</strong></p>
<ul>
<li><img src="https://velog.velcdn.com/images/baby_dev/post/001f73b4-c728-436d-a6d2-54048bac04d1/image.png" alt=""></li>
</ul>
<p>gzip(52kb) vs zstd(52kb)</p>
<p><strong>압축시간</strong></p>
<ul>
<li><img src="https://velog.velcdn.com/images/baby_dev/post/a085d1e3-a82c-47c8-8e7c-64f726e86f8c/image.png" alt=""></li>
<li><img src="https://velog.velcdn.com/images/baby_dev/post/4d4389ba-59e8-4163-a11d-ead54ca7e20d/image.png" alt=""></li>
</ul>
<p>gzip(0.017s) vs zstd(0.014s)</p>
<hr>
<p>전체적으로 보면 zstd가 gzip보다 뛰어난 압축률을 자랑했고 압축시간도 빠른 경우가 많았습니다. (gzip이 빠를때도 있지만 전체 평균적으로 zstd가 빠른편입니다.) 그리고 용량이 커질 수록 압축률차이가 더욱 많이 발생했습니다.</p>
<p>하지만 zstd도 단점이 있기마련이죠. <strong>바로 현재 모든 브라우저에서 지원을 하지않고 최신 브라우저에서만 지원을 한다는 문제가 있습니다.</strong> 그렇기에 우리는 zstd를 사용하되 zstd를 지원하지 않는 브라우저에서는 gzip을 이용해야합니다.
<a href="https://caniuse.com/zstd">https://caniuse.com/zstd</a></p>
<h2 id="accept-encoding--content-encoding">Accept-Encoding / Content-Encoding</h2>
<p>browser가 어떠한 압축 알고리즘을 지원하는지 알기위해서는 요청헤더의 <code>Accept-Encoding</code> 정보가 필요합니다.</p>
<pre><code>Accept-Encoding: gzip, br, deflate</code></pre><p>보통 Accept-Encoding은 위의 예시처럼 <code>,</code> 로 나누어진 형태로 나오게 됩니다. zstd를 지원하는 브라우저 같은 경우에는 뒤에 <code>zstd</code>가 추가될 것 입니다.</p>
<p>그리고 응답헤더의 <code>Content-Encoding</code> 값으로 브라우저는 compress된 파일을 decompress 합니다. </p>
<pre><code>Content-Encoding: gzip</code></pre><p>Content-Encoding 값 같은 경우에는 <code>gzip</code>, <code>zstd</code>와 같이 특정한 하나의 알고리즘으로 응답이 오게됩니다.</p>
<p>이제 우리는 Accept-Encoding, Content-Encoding을 조절하여 웹에서 zstd를 지원하는 경우에는 zstd로 압축을, gzip만을 지원하는 경우에는 gzip으로 압축을 하도록 설정할 것 입니다.</p>
<h2 id="이제-실전으로">이제 실전으로..</h2>
<p>이제 이론에 대해서 알아봤으니 이 개념을 토대로 실제 웹에서 테스트를 해보아야합니다.</p>
<p>압축을 담당할 웹서버는 nginx, 그리고 프론트엔드 개발 환경은 nextjs로 zstd를 지원하는 크롬, zstd를 지원하지 않는 사파리 환경으로 나누어서 테스트를 해야합니다.</p>
<p>여기에 과정까지 적게 된다면 내용이 너무 길어질 것 같기에 <a href="https://velog.io/@baby_dev/front-end-nginx%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9B%B9%EC%97%90%EC%84%9C%EC%9D%98-zstd-%EC%95%95%EC%B6%95-%ED%99%9C%EC%84%B1%ED%99%94">다음 글</a>에 실전 과정을 추가하도록 하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 버그 픽스로 기여한 후기 🎉]]></title>
            <link>https://velog.io/@baby_dev/Next.js-%EB%B2%84%EA%B7%B8-%ED%94%BD%EC%8A%A4%EB%A1%9C-%EA%B8%B0%EC%97%AC%ED%95%9C-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@baby_dev/Next.js-%EB%B2%84%EA%B7%B8-%ED%94%BD%EC%8A%A4%EB%A1%9C-%EA%B8%B0%EC%97%AC%ED%95%9C-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sat, 18 May 2024 16:57:32 GMT</pubDate>
            <description><![CDATA[<p>이번에 Next.js의 버그 픽스를 하여 <strong>Contributor</strong>가 되었는데요.</p>
<p>그 과정을 함께 살펴보는 글을 작성해보았습니다.</p>
<h2 id="문제-원인---nextscript의-이상한-동작">문제 원인 - next/script의 이상한 동작</h2>
<p>현재 회사에서 <code>next/script</code>를 사용하여 패키지를 제공하고 있는데요. 
<code>next/script</code> 와 동일하게 strategy에 값을 넣지 않은 경우 default 동작인 <code>afterInteractive</code>를 넣어주고 값이 있는 경우 그 값(afterInteractive, beforeInteractive, worker)로 동작하도록 하고 있습니다.</p>
<p>*<em>그런데 nextjs <code>page router</code>의 <code>_document.tsx</code>의 <code>HEAD</code> 태그안에 strategy prop을 빈 값으로 주는 경우 동작하지 않았습니다.
*</em></p>
<pre><code class="language-jsx">// _document.tsx

export default Document() {
  return (
    &lt;HTML&gt;
      &lt;HEAD&gt;
        &lt;Script src=&quot;example.com&quot; /&gt; // not working
      &lt;/HEAD&gt;   
    &lt;/HTML&gt;
  )
}
</code></pre>
<p>그래서 이 원인을 파악하기 위해 여러가지 테스트를 해보았습니다.</p>
<h2 id="테스트-1---head가-아닌-다른-곳이라면">테스트 1 - HEAD가 아닌 다른 곳이라면?</h2>
<p>먼저 HEAD 태그가 아닌 다른 환경에서도 테스트를 해보았습니다.</p>
<h3 id="body">BODY</h3>
<p>동일한 _document.tsx내의 <strong>HEAD</strong>가 아닌 <strong>BODY</strong>태그에 Script를 넣어보니 동일하게 동작하지 않았습니다.</p>
<h3 id="pagesindextsx">pages/*/index.tsx</h3>
<p>하지만 _document.tsx를 벗어난 pages내의 페이지들에서는 정상적으로 동작하는 것을 확인할 수 있었습니다.</p>
<h2 id="테스트-2---다른-strategy-prop에서도-동일한가">테스트 2 - 다른 strategy prop에서도 동일한가?</h2>
<p>이어서 다른 strategy prop에서도 이렇게 동작을 하지 않는지 확인을 해보려고 beforeInteractive, worker 값을 넣어 테스트를 해보니 모두 정상적으로 동작을 하였습니다.</p>
<p>그리고 당연히 afterInteractive에서는 동작하지 않겠지라고 생각하고 afterInteractive값을 넣어 테스트를 해보니 동작을 하였습니다.</p>
<p>여기서 이상한 점을 발견했습니다. 
<strong>next/script 태그의 strategy prop은 기본이 <code>afterInteractive</code>인데 이 값을 넣지 않았을때는 동작하지 않고, <code>afterInteractive</code>라고 명시적으로 작성을 해줘야 동작을 하는 것이였습니다.</strong></p>
<h2 id="테스트-3---app-router의-layout에서는">테스트 3 - app router의 layout에서는?</h2>
<p>현재 page router의 _document.tsx에서 strategy값을 넘기지 않고 _document.tsx에 사용하는 경우 동작을 하지 않는 것까지 확인하였습니다. </p>
<p>그렇다면 page router의 _document.tsx가 아닌 app router의 _document.tsx와 비슷한 layout.tsx에서는 어떤 동작을 할까 싶어 테스트를 해보았습니다.</p>
<p>결과는 정상적으로 동작하였습니다.</p>
<h2 id="테스트-결과">테스트 결과</h2>
<p>최종적으로 확인한 결과는 <strong>app router가 아닌 page router 에서의 _document.tsx 파일 내에 Script가 어디에 위치하든 동작하지 않는다.</strong> 였습니다.</p>
<h2 id="브라우저-분석">브라우저 분석</h2>
<p>이제 나오는 원인은 확실히 확인하였으니 왜 이런 동작을 하는지 파악이 필요합니다. </p>
<p>일단 제가 생각한 가설은 다음과 같았습니다. </p>
<p><code>_document.tsx</code>는 next의 서버에 속하는데, next/script태그를 사용할때 정상적으로 스크립트를 로드해온다?
이것은 서버에서 브라우저로 무언가를 심어줘서 브라우저에서 그것을 실행하는 것이다. 라고 생각하였습니다.</p>
<p>*<em>스크립트를 서버에서 불러올 수는 없으니까요.
*</em>
보통 이렇게 서버에서 브라우저로 무언가를 공유시켜야될때 script 태그내에 이러한 메타데이터들을 넘겨서 브라우저에서 처리하는 경우가 있기에 next가 심는 metadata 태그가 있나 확인을 하였습니다.</p>
<p>확인한 결과 웹에서 <code>__NEXT_DATA__</code>라는 네이밍을 가진 script 태그를 발견했습니다.</p>
<pre><code class="language-jsx">&lt;script id=&quot;__NEXT_DATA__&quot; type=&quot;application/json&quot;&gt;
{
  &quot;props&quot;:{&quot;pageProps&quot;:{&quot;_sentryTraceData&quot;:&quot;xxx&quot;,&quot;xxx&quot;:&quot;sentry-environment=xxx,sentry-release=xxx,sentry-public_key=xxx,sentry-trace_id=xxx&quot;},&quot;userAgent&quot;:&quot;xxx&quot;},
  &quot;page&quot;:&quot;/&quot;,
  &quot;query:{},
  &quot;buildId&quot;:&quot;OHsykGRpFeEG_sfr_boVp&quot;,
  &quot;assetPrefix&quot;:&quot;&quot;,
  &quot;isFallback&quot;:false,
  &quot;appGip&quot;:true,
  &quot;scriptLoader&quot;:[]
}
&lt;/script&gt;</code></pre>
<p>이 부분을 이제 상황에 따라 확인해보았습니다.</p>
<p>*<em>strategy가 빈 값인 경우에는 다음과 같이 scriptLoader가 빈 값이지만 strategy가 afterInteractive로 명시한 경우에는 scriptLoader내에 이에 대한 메타데이터가 존재하는 것을 확인했습니다.
*</em></p>
<h2 id="nextjs-코드-분석">Next.js 코드 분석</h2>
<p>이제 문제 원인과 scriptLoader의 이상함까지 파악했으니 Next.js의 코드에서 직접 수정을 확인을 해주면 됩니다.</p>
<p>이 방대한 코드에서 어디를 수정해야되는지는 막막하지만 <code>scriptLoader</code>라는 키워드를 알아냈으니 이를 기반으로 검색하여 찾아가면 가능할 것 같아 진행했습니다.</p>
<p>그렇게 <code>packages/next/scr/pages/_document.tsx</code> 라는 파일을 발견했습니다. 확인해보니 _document.tsx에서 일어나는 동작을 처리하는 패키지로 보였습니다.</p>
<p>무려 1250줄이나 되는 어마어마한 코드더라고요. 하지만 저희는 여기서 <code>afterInteractive</code>를 검색해서 추려보도록 합시다.</p>
<p>드디어 이 로직을 처리하는 함수를 발견했습니다.</p>
<pre><code class="language-jsx">function handleDocumentScriptLoaderItems(
  scriptLoader: { beforeInteractive?: any[] },
  __NEXT_DATA__: NEXT_DATA,
  props: any
): void {
  if (!props.children) return

  const scriptLoaderItems: ScriptProps[] = []

  const children = Array.isArray(props.children)
    ? props.children
    : [props.children]

  const headChildren = children.find(
    (child: React.ReactElement) =&gt; child.type === Head
  )?.props?.children
  const bodyChildren = children.find(
    (child: React.ReactElement) =&gt; child.type === &#39;body&#39;
  )?.props?.children

  // Scripts with beforeInteractive can be placed inside Head or &lt;body&gt; so children of both needs to be traversed
  const combinedChildren = [
    ...(Array.isArray(headChildren) ? headChildren : [headChildren]),
    ...(Array.isArray(bodyChildren) ? bodyChildren : [bodyChildren]),
  ]

  React.Children.forEach(combinedChildren, (child: any) =&gt; {
    if (!child) return

    // When using the `next/script` component, register it in script loader.
    if (child.type?.__nextScript) {
      if (child.props.strategy === &#39;beforeInteractive&#39;) {
        scriptLoader.beforeInteractive = (
          scriptLoader.beforeInteractive || []
        ).concat([
          {
            ...child.props,
          },
        ])
        return
      } else if (
        [&#39;lazyOnload&#39;, &#39;afterInteractive&#39;, &#39;worker&#39;].includes(
          child.props.strategy
        )
      ) {
        scriptLoaderItems.push(child.props)
        return
      }
    }
  })

  __NEXT_DATA__.scriptLoader = scriptLoaderItems
}</code></pre>
<p>이 로직을 확인해보니 <code>React.Children</code>를 읽어와서 script태그를 읽어서 그 태그의 strategy를 읽어서 처리하는 로직으로 보입니다.</p>
<p>여기에 beforeInteractive, lazyOnLoad, afterInteractive, worker라는 strategy를 읽어 scriptLoaderItems에 push하고 마지막에 <code>__NEXT_DATA__</code>에 넣는 로직인데요.</p>
<p>여기서 안되는 이유를 확인했습니다. </p>
<p><strong>현재 Script tag에는 기본 동작이 afterInteractive로 걸려있는데 <code>React.Children</code> 같은 경우 그 태그 자체를 가져와서 읽는 거라서 default로 잡는 afterInteractive가 없을 수 밖에 없는 것입니다.</strong></p>
<h2 id="수정하기">수정하기</h2>
<p>이제 문제 해결법도 알았으니 코드를 수정하면 됩니다. 수정은 간단한데 이제 next.js측에 issue를 남겨야합니다.</p>
<p><a href="https://github.com/vercel/next.js/issues/65580">https://github.com/vercel/next.js/issues/65580</a></p>
<p>겪었던 이슈를 제보하고 코드를 수정하면 됩니다.</p>
<pre><code class="language-jsx">    // When using the `next/script` component, register it in script loader.
    if (child.type?.__nextScript) {
      if (child.props.strategy === &#39;beforeInteractive&#39;) {
        scriptLoader.beforeInteractive = (
          scriptLoader.beforeInteractive || []
        ).concat([
          {
            ...child.props,
          },
        ])
        return
      } else if (
        [&#39;lazyOnload&#39;, &#39;afterInteractive&#39;, &#39;worker&#39;].includes(
          child.props.strategy
        )
      ) {
        scriptLoaderItems.push(child.props)
        return
      } 
      // 이 부분!!
      else if (typeof child.props.strategy === &#39;undefined&#39;) {
        scriptLoaderItems.push({ ...child.props, strategy: &#39;afterInteractive&#39; })
        return
      }
  })</code></pre>
<p>간단하게 scriptLoaderItems에 메타데이터를 삽입하는 로직에 empty prop일때의 처리를 추가해주면 됩니다.</p>
<h2 id="회귀-테스트-추가">회귀 테스트 추가</h2>
<p>이제 로직을 추가했으니 이에 대한 회귀테스트를 추가해줘야합니다.</p>
<p>제가 추가한 로직이 없는 경우에는 테스트가 실패하고 제가 추가한 로직이 있는 경우에만 테스트가 통과하도록 하는 것이죠.</p>
<pre><code class="language-jsx">describe(&#39;empty strategy in document Head&#39;, () =&gt; {
  let next: NextInstance

  beforeAll(async () =&gt; {
    next = await createNext({
      files: {
        &#39;pages/_document.js&#39;: `
          import { Html, Head, Main, NextScript } from &#39;next/document&#39;
          import Script from &#39;next/script&#39;
          export default function Document() {
            return (
              &lt;Html&gt;
                &lt;Head&gt;
                  &lt;Script
                    src=&quot;https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js&quot;
                  &gt;&lt;/Script&gt;
                &lt;/Head&gt;
                &lt;body&gt;
                  &lt;Main /&gt;
                  &lt;NextScript /&gt;
                &lt;/body&gt;
              &lt;/Html&gt;
            )
          }
        `,
        &#39;pages/index.js&#39;: `
          export default function Home() {
            return (
              &lt;&gt;
                &lt;p&gt;Home page&lt;/p&gt;
              &lt;/&gt;
            )
          }
        `,
      },
      dependencies: {
        react: &#39;19.0.0-beta-4508873393-20240430&#39;,
        &#39;react-dom&#39;: &#39;19.0.0-beta-4508873393-20240430&#39;,
      },
    })
  })
  afterAll(() =&gt; next.destroy())

  it(&#39;Script is injected server-side&#39;, async () =&gt; {
    let browser: BrowserInterface

    try {
      browser = await webdriver(next.url, &#39;/&#39;)

      const script = await browser.eval(
        `document.querySelector(&#39;script[data-nscript=&quot;afterInteractive&quot;]&#39;)`
      )
      expect(script).not.toBeNull()
    } finally {
      if (browser) await browser.close()
    }
  })
})</code></pre>
<p>이렇게 제가 추가한 empty prop에 대한 처리가 없으면 터지고 있는 경우에만 성공하도록 하는 테스트를 추가했습니다.</p>
<h2 id="머지">머지!</h2>
<p>next.js팀에서 개인이 작업한 pr들은 아직 opened로 남아 있는 경우가 많고 리뷰가 안달리는 경우가 많더라고요.</p>
<p>다행히 5일만에 ok리뷰를 받고 다행히 머지가 되었습니다! </p>
<p><a href="https://github.com/vercel/next.js/pull/65585">관련 pr</a></p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/25dab59d-fcfe-4782-8751-684953096191/image.png" alt=""></p>
<h2 id="후기">후기</h2>
<p>이번에 첫 오픈소스 기여를 Next.js의 버그 픽스로 하게 되어 매우 좋은 경험이였습니다. 여러분들도 라이브러리에서 문제가 생기면 직접 해결을 해보는 경험도 좋을 것 같습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[front-end] 기술에 잡아먹히지 마세요.]]></title>
            <link>https://velog.io/@baby_dev/front-end-%EA%B8%B0%EC%88%A0%EC%97%90-%EC%9E%A1%EC%95%84%EB%A8%B9%ED%9E%88%EC%A7%80-%EB%A7%88%EC%84%B8%EC%9A%94</link>
            <guid>https://velog.io/@baby_dev/front-end-%EA%B8%B0%EC%88%A0%EC%97%90-%EC%9E%A1%EC%95%84%EB%A8%B9%ED%9E%88%EC%A7%80-%EB%A7%88%EC%84%B8%EC%9A%94</guid>
            <pubDate>Sun, 21 Apr 2024 19:38:38 GMT</pubDate>
            <description><![CDATA[<p>*<em>⚠️ 이 글은 매우 주관적인 의견을 작성한 것입니다. 
*</em></p>
<hr>
<p>요즘 사이드 프로젝트를 보다 보면 매력적인 기술, 아키텍처들이 많습니다. </p>
<p>사실 저도 여기서 키워드 정도와 사용 용도는 알지만 실제로 사용해보지는 않은 것들이 대부분입니다.</p>
<p>최근에 본 것이라고 하면 다음과 같은 기술, 아키텍처들이 핫한 것 같더라고요.</p>
<ul>
<li>funnel</li>
<li>xstate</li>
<li>feature slice design pattern</li>
<li>next 14(app router)</li>
<li>compound component pattern</li>
<li>monorepo</li>
<li>그 외 등등..</li>
</ul>
<p>벌써 모두 사용만 해본다면 취업을 당해버릴 거 같은 기술 스택이네요.. 기업의 자격 요건 및 우대 사항에도 본 것 같은 기술들도 간간이 있군요.</p>
<p>그렇다면 이것들을 모두 사용한다면 잘하는 개발자이며 좋은 곳으로 취직하는 사람일까요?</p>
<p>뭐 물론 그럴수도 있다고 생각합니다. 프론트엔드 개발자의 핵심 중 하나는 <strong>새로운 기술을 빠르게 익히고, 이러한 기술에 관한 탐구를 하는 것</strong> 이라고 생각하기 때문이죠.</p>
<p>하지만 이것은 <code>내가 어떤 이유로 사용했는지</code>, <code>제품을 정해진 기간에 ＇잘＇ 만들어야 하는 경우</code> 를 모두 전제돼야 한다고 생각합니다.</p>
<p>지난번에 작성한 <a href="https://velog.io/@baby_dev/front-end-%EA%B3%B5%ED%86%B5%EC%9D%98-%EC%A0%80%EC%A3%BC">공통의 저주</a> 글과 유사하게 너무 많은 기술을 알게 된 프론트엔드 개발자가 겪는 <code>기술의 저주</code>라고 할 수 있겠네요.</p>
<hr>
<p>이번에는 A라는 가상의 인물을 두어 한번 살펴보겠습니다.</p>
<blockquote>
<p><strong>A 씨는 프론트엔드 3명이서 간단한 페이지들을 개발하는데 nextjs 14(app router)를 사용하고 unit test 까지 달아주었습니다.</strong></p>
</blockquote>
<h2 id="기술의-저주---내가-이유에-맞게-사용했는가">기술의 저주 - 내가 이유에 맞게 사용했는가?</h2>
<p>먼저 얘기해볼것은 기술 선택의 이유입니다.</p>
<h3 id="nextjs-14-app-router">nextjs 14 (app router)</h3>
<p>A씨는 왜 nextjs 14(app router)는 사용했을까요? <strong>이에 먼저 nextjs는 왜 사용했을까요?</strong></p>
<p>react를 보다 쉽고 설정에 대해서 좀 덜 고민하면서 개발할 수 있고, ssr, isr, ssg 같은 렌더링 전략을 통해 api를 fetch할시 사용자가 깜빡이는 ui를 보지 않을 수 있고, 서버에서 완성된 html을 제공하기에 seo에도 도움이 될 수 있겠죠.</p>
<p>하지만 이와 반대로 서버를 이용하기에 기존 간단하게 s3로 설정하던 react 배포가 nextjs가 되며 ec2, 더 나아가서 ecr, ecs 설정까지 해야될 수 있으며 그에 따른 서버 비용마저 고려를 해야 합니다.</p>
<p>만약 A씨가 단점보다 장점을 더 크게보아 선택을 한다면 정당한 이유가 되겠죠? 충분히 개발의 용이함만으로도 nextjs를 선택할 수도 있을 것입니다.</p>
<p>그렇다면 <strong>nextjs 14(app router)</strong>는 어떨까요?</p>
<p>제가 생각하는 장점은 다음과 같습니다.
<strong>컴포넌트별로 isr, ssr, ssg의 전략을 선택하여 사용</strong>할 수 있다는 것 정도 있을 것 같네요. server component의 zero-bundle은 실제 개발해보면 크게 체감이 되지 않기에 큰 장점이라고 하기에는 어려울 것 같네요.</p>
<p>그렇다면 단점으로는 무엇보다 <strong>개발의 피로도 증가</strong>가 있겠네요. 서버 컴포넌트의 등장으로 개발의 난도가 올라가고, 잘못 서버 컴포넌트를 사용하는 때도 매우 많습니다.</p>
<p>만약 A씨가 app router를 저 장점을 목적으로 사용했고, app router 개발에 매우 익숙한 사람이라면 충분히 선택할만한 가치가 있겠군요.</p>
<p>하지만 fetch도 많지 않고 그런 고려까지 할 필요없는 간단한 사이트라면 이는 당연히 불필요한 기술이라고 생각이 되는군요.</p>
<h3 id="unit-test">unit test</h3>
<p>그렇다면 A씨는 왜 <strong>unit test</strong>를 이용하였을까요?</p>
<p>특정 모듈을 만들어 그것을 계속 검증해야 하거나 복잡한 상태를 지닌 컴포넌트에 대한 렌더 테스트를 진행할 때 unit test가 용이합니다.
하지만 간단한 상태만을 가진 페이지를 만들 때 굳이 unit test를 붙히는 것은 필요 없지 않을까라는 생각이 듭니다.</p>
<hr>
<p>지금은 간단하게 app router, unit test를 사용한 가상의 A씨를 기준으로 알아보았는데 실제로는 이것 외에도 다양한 기술이 오용되고 있는 경우가 많습니다.</p>
<ul>
<li>간단한 페이지, 구조에서의 feature sliced design (fsd)</li>
<li>확장성이 필요없는 컴포넌트에 compound component pattern</li>
<li>context api를 사용해도 충분할 규모에 recoil, jotai, zustand등의 상태관리 라이브러리 이용</li>
<li>굳이 monorepo로 분리하지 않고 관리해도 될 프로젝트를 monorepo로 관리</li>
</ul>
<h2 id="기술의-저주---제품을-고려하였는가">기술의 저주 - 제품을 고려하였는가?</h2>
<p>다음은 제품을 고려하였는가 입니다.</p>
<p>아까 가상의 A씨를 다시 한번 불러와 보겠습니다. </p>
<blockquote>
<p>A씨는 프론트엔드 3명이서 간단한 페이지들을 개발하는데 nextjs 14(app router)를 사용하고 unit test 까지 달아주었습니다.</p>
</blockquote>
<p>프론트엔드 3명이서 페이지를 개발하고 있는 상황이군요. 하나의 조건을 추가하여 <strong>3주 뒤에 mvp를 만드는 것이 목표</strong>라고 가정하겠습니다.</p>
<p>여기서 만약 A씨는 app router에 대한 이론만 가볍게 알고, 나머지 두 사람이 app router에 대해서 전무한 상황이면 어떨까요?</p>
<p>3주안에 개발을 해야 되는데 기술 사용에 대한 어려움 때문에 제대로 된 제품 개발을 하지 못할 수 도 있습니다.</p>
<p>비슷하게 당장 돌아가는 제품을 내야 될 때 unit test를 붙이는 것도 <strong>목표한 개발기간에 맞추지 못하게 되는 원인이 될 수 있습니다.</strong></p>
<p>물론 회사에서 넉넉한 기간을 잡고 기술적인 도전을 할 프로젝트인 경우 새로운 기술을 적용해보는 것이 좋은 선택일 겁니다.</p>
<p>하지만 다음과 같이 짧은 개발기간 안에 제품을 만들어야 하는 경우는 모든 팀원이 익숙한 기술이거나 러닝 커브가 적은 기술을 선택하는 게 최선일 것 같습니다.</p>
<p><strong>항상 기술은 제품을 위한 수단이지 기술이 우선 돼서는 안된다는 것을 명심하시기를 바랍니다.</strong></p>
<h2 id="학습을-위한-기술-사용">학습을 위한 기술 사용</h2>
<p>*<em>하지만 기술 사용의 목적이 <code>학습</code> 인 경우가 종종 있습니다.
*</em></p>
<p>물론 새로운 기술을 학습하기 위하여 개인 프로젝트로 실습을 해보는 것은 매우 좋은 방법이라고 생각합니다.</p>
<p>하지만 이렇게 학습을 목적으로 사용한 프로젝트는 <strong>이력서나 포트폴리오에 기재하지 않는 것을 추천합니다.</strong></p>
<p>새롭고 요즘 떠오르는 기술들을 사용하여 이력서에 기재한다면 당장은 서류가 아름다워 보일 수 있겠지만, 면접에서 그 프로젝트는 공격의 타겟이 될 가능성이 높습니다.</p>
<p>나에게 새롭고 떠오르는 기술이라면 당연히 면접관에게도 1순위 질문이 <strong>왜 이 기술을 사용하여 프로젝트를 구성하였는가?</strong> 일 수 있습니다.</p>
<p>만약 내가 어떠한 문제를 만났고 그 문제를 해결하기 위해서 이 기술을 선택하였고 그렇게 해결된 과정이 프로젝트에 녹여져 있다면 이 질문은 당연히 플러스가 될 것입니다.</p>
<p>반대로 그저 사용해보고 싶어서, 학습을 목적으로 사용한 프로젝트일 경우에는 최선의 대답이 <strong>사용해보고 싶었다, 학습을 목적으로 사용했다.</strong> 일 것입니다.
오히려 문제해결의 이유 없이 사용한 프로젝트에서 이러한 목적으로 해결했다고 하면 그것은 기술을 잘 모르고 사용했다라는 인식을 줄 수 있죠.</p>
<p>그렇기에 개인적인 프로젝트에서 특정 기술을 실습하면서 체화하고 실제 제품을 만들 때 문제를 만나면 그때 사용해봤던 기술을 적용해보는 게 좋다고 생각합니다.</p>
<h2 id="어떤-기준으로-기술을-선택할-것-인가">어떤 기준으로 기술을 선택할 것 인가?</h2>
<p>그러면 어떤 기준으로 기술을 선택할 수 있을까요? 제가 생각하는 기준은 다음과 같습니다.</p>
<ul>
<li>현재 프로젝트가 마주한 문제를 해결하기에 적합한 기술인가</li>
<li>팀원들이 당장 사용하는데 문제 없을 러닝 커브 or 이미 사용해본 기술인가</li>
<li>지속적으로 관리(contribute) 되고 있는 기술인가</li>
<li>장점만큼이나 따라오는 한계점에 대해서 생각해보았는가</li>
<li>현재 사용중인 메인 라이브러리 or 프레임워크와 잘 호환되는 기술인가</li>
</ul>
<h2 id="마무리">마무리</h2>
<p>잘하는 개발자는 기술을 잘 쓰는 개발자가 아니라 필요한 문제를 잘 해결하는 개발자라고 생각합니다.</p>
<p>빠르게 변화하는 기술을 따라가는 것도 좋지만, 그 기술에 휩쓸려서 더 중요한 것을 놓치지 않았으면 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[css 가두기]]></title>
            <link>https://velog.io/@baby_dev/style-%EC%BA%A1%EC%8A%90%ED%99%94</link>
            <guid>https://velog.io/@baby_dev/style-%EC%BA%A1%EC%8A%90%ED%99%94</guid>
            <pubDate>Mon, 01 Jan 2024 15:16:53 GMT</pubDate>
            <description><![CDATA[<p>오랜만입니다. 최근에 게임을 열심히 하느라 블로그를 쓰는게 지연됬네요..! 이번에는 제가 공통 모듈을 개발하며 겪었던 style 캡슐화에 대한 이슈를 다뤄볼것 입니당.🫠</p>
<h2 id="현재의-문제">현재의 문제</h2>
<p>일단 현재 저는 공통 모듈 라이브러리를 개발을 하고 있습니다.</p>
<p>기존에 style을 pure css (.css) 를 이용하고 있었는데요. 그러다 보니 이 프로젝트에 사용되는 특정 모듈을 빼내서 공통 모듈로 만들때 이 css를 그대로 옮겼어야 됬는데요. (css가 천줄 이상 존재하였습니다.)</p>
<p>하지만 <strong>css 특성상 global에도 오염되는 문제가 존재</strong>하여 해결이 필요했었는데요. </p>
<p>이 공통 모듈은 빠르게 만들어 다른 도메인에 제공해야했기에 이에 대해 해결을 빠르게 진행했어야 됬습니다. </p>
<p>*<em>그러면 단 하루만에 어떻게 캡슐화된 css를 적용시켰는지 저의 고민을 함께 보시죠.
*</em></p>
<h2 id="module-css">module css</h2>
<p>일단 처음에 고려했던것은 module css였습니다. 하지만 module css를 변환할때 문제가 있었는데요. 기존 css에서는 className을 부여하고 그거에 대한 stylesheet를 작성했었습니다. 그리고 태그의 자식요소, 형제요소와 같이 참조하도록 하는 선택자들이 꽤 존재하였습니다.</p>
<p>하지만 module css에서는 className을 다음과 같이 styles객체를 통해서 참조하기에 기존 css를 하나하나 이렇게 변경하기에는 무리가 있다고 생각하여 다음방법을 고려했습니다.</p>
<pre><code class="language-jsx">import styles from &#39;./index.module.css&#39;;

const Component = () =&gt; {
  return (
    &lt;div className={styles.component}&gt;&lt;/div&gt;
  )
}</code></pre>
<h2 id="styled-components-css-in-js">styled-components (css-in-js)</h2>
<p>그래서 다음생각한 방법은 <strong>styled-components</strong>였습니다. <strong>styled-components</strong>를 이용하면 역시 스타일이 저절로 캡슐화되고, 브라우저의 호환성에 맞도록 webkit등을 넣어주기때문에 이를 이용하여 스타일을 재구축하려고 했습니다.</p>
<p>하지만 역시 <code>common.css</code>를 포함하여 몇천줄이 되는 코드를 styled-components로 옮기는것은 꽤 많은 시간이 걸릴것이라고 예측이 되었습니다. (기존코드가 styled-components로 변경에 쉽지 않은 스타일 시트였습니다..🥲)</p>
<h2 id="shadow-dom-👻">shadow-dom 👻</h2>
<p>그래서 번뜩이며 생각해낸것이 shadow-dom이였습니다. shadow-dom이란 말그대로 <code>그림자 돔</code>입니다. 그렇기에 실제 돔에 붙어있는게 아니라 숨겨진 dom tree가 기존 dom tree에 붙는 방식으로 동작을 하며, <strong>이 숨겨진 dom tree는 내부의 스타일이 바깥에 영향을 끼치지 않도록 <code>캡슐화</code>를 해버립니다.</strong></p>
<p>그렇기에 Web Component라는 기술에서 핵심적으로 사용되는것이 shadow-dom입니다. 실제로 개발자도구에서 <input type="range" /> 이와 같은것을 찍어보면 shadow-dom으로 구성되어있는것을 확인할 수 있습니다.</p>
<pre><code class="language-jsx">const shadowRoot = this.attachShadow({ mode: &quot;open&quot; });</code></pre>
<p>사용방법은 다음과 같이 shadowRoot에 attachShadow를 이용하여 dom을 shadow dom으로 만들어 줄 수 있습니다.</p>
<p>그리고 이 안에 공통모듈을 css와 함께 붙인것을 넣어주면 캡슐화가 완료되는것이죠. <strong>자 이렇게 빠르게 목표를 달성한것 같지만 저는 다음방법을 선택하지 않았습니다. 어떤 이유때문일까요??</strong></p>
<h3 id="shadow-dom-호환성">shadow-dom 호환성</h3>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/adf3b059-23fd-4ac1-8a72-dceaa2c54e5f/image.png" alt=""></p>
<p>다음과 같이 <code>can-i-use</code>를 보시면 android chrome에서 120버전 이하의 지원을 하지 않는것을 볼 수 있습니다. ie는 이제는 크게 신경쓰지 않아도 되지만 android chrome은 신경써야될 부분이죠.</p>
<p>물론 shadow-dom polyfill을 이용하면 이를 해결할 수는 있죠. <code>@webcomponents/webcomponentsjs</code> 실제로 이 패키지에서 polyfill을 제공하는데요. </p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/776e2e8a-27fa-4082-b683-e7be4804c153/image.png" alt=""></p>
<p>하지만 이를 bundlephobia에 패키지를 검색해보면 <code>125kb</code> 가 나옵니다.</p>
<p>용량이 크다고 난리인 react,react-dom도 합쳐서 <code>135kb</code> 정도이니 매우 큰 용량이죠.
그리고 polyfill에 이 공통 모듈 패키지가 의존되는것은 좋지 않다고 생각되어서 <code>shadow-dom</code>말고 다른 방법으로 할 수 있을지 고민을 이어나갔습니다.</p>
<h2 id="styled-components-globalstyles">styled-components GlobalStyles</h2>
<p>그리고 마지막 고민이자, 현재 공통 모듈에서 이용을 하고 있는 방법인 styled-components의 GlobalStyles를 이용하는것을 채택하였습니다.</p>
<p>이를 선택하게 된것은 단순한 번뜩임이였는데요..! shadow-dom을 하기전 styled-components를 바꾸고 있을때 다음과 같은 생각을 해보았습니다.</p>
<blockquote>
<p>common.css는 globalStyle로 이용될텐데 과연 이 globalStyle을 이용하여 common.css를 넣은것은 캡슐화가 될까?</p>
</blockquote>
<p>그리고 실제로 확인을 한 결과 캡슐화 하는것이 확인되었습니다. 그래서 다음과 같이 생각을 해보았습니다.</p>
<p>현재 공통 모달을 만들때 이용되는 css를 globalStyles에 넣어서 사용한다면 기존 css를 건드리지 않은 상태로 캡슐화를 진행할 수 있지 않을까?</p>
<pre><code class="language-jsx">export const GlobalStyle = createGlobalStyle`
(common.css ...)

(관련도메인.css ...)
`
</code></pre>
<p>라고 생각하여 다음과 같이 common.css 와 함께 기존 css를 함께 넣어주니 제대로 동작하는것을 확인하였습니다.</p>
<h2 id="결론">결론</h2>
<p>이렇게 실제로 작성해보니 실제 css를 다시 module css, css-in-js처럼 변경하지 않고도 빠르게 캡슐화가 되는 공통 모듈을 만들 수 있었습니다.</p>
<p>목표는 이뤘지만 이것은 공통 모듈을 만들어서 사용하는측에 빠르게 전달하기 위한 야매(?) 방법이기때문에 추후에 pure css로 되어있는것은 styled-components 문법으로 변경할 예정입니다.</p>
<p>그럼에도 불구하고 이런글을 작성하는 이유는 스타일을 캡슐화하는 여러가지 방법에 대한 소개, 그리고 어떤 문제를 겪었을때 어떠한 방법으로든 해결이 가능하다라는것을 공유하고 싶었습니다.</p>
<h2 id="새해복-많이-받으세요-🎉">새해복 많이 받으세요~ 🎉</h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[[front-end] 공통의 저주]]></title>
            <link>https://velog.io/@baby_dev/front-end-%EA%B3%B5%ED%86%B5%EC%9D%98-%EC%A0%80%EC%A3%BC</link>
            <guid>https://velog.io/@baby_dev/front-end-%EA%B3%B5%ED%86%B5%EC%9D%98-%EC%A0%80%EC%A3%BC</guid>
            <pubDate>Tue, 21 Nov 2023 18:34:37 GMT</pubDate>
            <description><![CDATA[<p>이번에 작성해볼 주제는 <code>공통의 저주</code>입니다.</p>
<p>저도 프론트엔드 개발 초반에 빠졌던 이슈인데요. </p>
<p>처음 개발을 시작하면 <code>dry원칙(don&#39;t repeat yourself)</code> 이라는것을 많이 들으면서 개발을 진행할 것 입니다. 물론 이러한 원칙을 듣지않았더라도 반복되는 것은 <code>공통</code>으로 분리해야된다. 이정도는 많이 알게 되죠.</p>
<p>이러한 공통을 지키겠다는 생각에 잡혀있을때 발생하는 <code>문제(저주)</code>에 대해서 알아봅시다.</p>
<h2 id="똑같은-ui의-input">똑같은 ui의 input</h2>
<table>
<thead>
<tr>
<th>회원가입</th>
<th>로그인</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/baby_dev/post/52160c15-1fba-42d2-8958-1076c96edfd0/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/baby_dev/post/e6a2b21f-089a-4df7-8a9e-9cdd6790707c/image.png" alt=""></td>
</tr>
</tbody></table>
<p>간단하게 이렇게 로그인, 회원가입 페이지를 기준으로 한번 보겠습니다.</p>
<p>로그인에 존재하는 아이디, 비밀번호의 input과 회원가입에 존재하는 input들의 ui가 매우 유사한것을 볼 수 있습니다.</p>
<p>그리고 이것을 보는 우리는 생각하죠. <strong>&#39;공통 input을 이용하여 로그인, 회원가입 처리를 모두 해야겠다.&#39;</strong></p>
<p>현시점에서는 나쁘지 않은 시도입니다. 크게 저 input에 <strong>기능</strong>이 없다면 큰 문제가 없기때문이죠.</p>
<pre><code class="language-tsx">// 공통 input

type InputProps = InputHTMLAttributes&lt;HTMLInputElement&gt;;

export const Input = ({ type, ...props }: InputProps) =&gt; {
  const [input, setInput] = useState(&#39;&#39;);

  const handleChangeInput = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    setInput(e.target.value);
  };

  return &lt;input {...props} type={type} onChange={handleChangeInput} value={input} /&gt;;
};</code></pre>
<p>일단 간단하게 이렇게 input만 존재하는 공통이 있다고 가정해봅시다. (스타일도 적용된 상태라고 가정하겠습니다.)</p>
<h3 id="1️⃣-첫번째-요청">1️⃣ 첫번째 요청</h3>
<p>여기서 기획에서 <strong>회원가입할때만 validation</strong>을 달아주고 싶다고 합니다. </p>
<p>음 그러면 공통에서 <code>inputType</code>을 받아 회원가입인 경우만을 걸러낸다음, valid 한 값인지도 props로 받아서 처리하면 되겠네요.</p>
<pre><code class="language-tsx">type InputProps = {
  inputType: &#39;signup&#39; | &#39;signin&#39;;
  isValid?: boolean;
} &amp; InputHTMLAttributes&lt;HTMLInputElement&gt;;

export const Input = ({ type, inputType, isValid, ...props }: InputProps) =&gt; {
  const [input, setInput] = useState(&#39;&#39;);

  const handleChangeInput = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    setInput(e.target.value);
  };

  return (
    &lt;&gt;
      &lt;input {...props} type={type} onChange={handleChangeInput} value={input} /&gt;
      {inputType === &#39;signup&#39; &amp;&amp; isValid === false ? &lt;div&gt;유효하지 않습니다.&lt;/div&gt; : null}
    &lt;/&gt;
  );
};</code></pre>
<p>props에 2개의 값을 추가하고, 회원가입이고 isValid가 false일때만 저러한 메세지를 밑에 보여주기로 하죠.</p>
<h3 id="2️⃣-두번째-요청">2️⃣ 두번째 요청</h3>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/697bb92a-881a-45ee-adbc-f81e15f10a90/image.png" alt=""></p>
<p>만약 저 똑같은 인풋을 이용하여 이러한 포인트를 다루는 인풋을 구현한다고 가정해보겠습니다.(ui가 달라보일 수 있지만 실제로는 ui가 같다는 가정입니다.)</p>
<pre><code class="language-tsx">type InputProps = {
  inputType: &#39;signup&#39; | &#39;signin&#39; | &#39;point&#39;;
  isValid?: boolean;
  currentHasPoint: number;
} &amp; InputHTMLAttributes&lt;HTMLInputElement&gt;;

export const Input = ({ type, inputType, isValid, ...props }: InputProps) =&gt; {
  const [input, setInput] = useState(&#39;&#39;);

  const handleChangeInput = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    if(inputType === &#39;point&#39;) {
           const validPointInput = Number(e.target.value.replace(/[^0-9]+/g, &#39;&#39;));

        if (validPointInput &gt; currentHasPoint) {
          alert(`이용가능한 포인트는 ${currentHasPoint}입니다.`);
          setInput(currentHasPoint);
          return;
        }

        setInput(validPointInput);
        return;
    }

    setInput(e.target.value);
  };

  const inputFormat = inputType === &#39;point&#39; ? `${input.toLocaleString()}P` : input;

  return (
    &lt;&gt;
      &lt;input {...props} type={type} onChange={handleChangeInput} value={inputFormat} /&gt;
      {inputType === &#39;signup&#39; &amp;&amp; isValid === false ? &lt;div&gt;유효하지 않습니다.&lt;/div&gt; : null}
    &lt;/&gt;
  );
};
</code></pre>
<p>포인트를 다루는 인풋이기에 현재 보유한 포인트까지만 입력할 수 있는 예외를 추가해야합니다. 그러니까 inputType에 <code>point</code>를 추가한뒤에 onChange의 이벤트핸들러에 pointform일때의 예외를 추가해봅시다.</p>
<p>또, 포인트는 <code>100,000P</code> 이런형식으로 보여야하니 input의 format도 포인트일때는 다르게 변경해봅시다.</p>
<h2 id="🫠뭔가-이상하지-않나요">🫠뭔가 이상하지 않나요?</h2>
<p>공통의 <code>Input</code>태그를 이용해서 <code>로그인</code>, <code>회원가입</code>, <code>포인트</code> 폼을 이용을 했습니다. ui가 동일하니 공통으로 잘 처리가 된듯하죠.</p>
<p>하지만 뭔가 이상하지 않나요? 공통이긴하지만 공통이 너무나 많은 기능을 하고있죠. 그덕분에 불필요한 props, 그리고 많은 분기처리가 있는 모습을 볼 수 있습니다.</p>
<p>만약 여기서 <code>로그인</code>의 기능이 추가된다면 어떨까요? 여기에 또 로그인에 대한 props, 분기처리가 또 들어가게 되겠죠?</p>
<p>현재는 3개의 폼들로 예시를 들었지만 여기서 더 늘어난다면 훨씬 더 많은 분기처리를 하게 되죠. </p>
<hr>
<h3 id="3️⃣-ui변경-요청">3️⃣ ui변경 요청</h3>
<p>자 이제는 기능에 대한 추가가 아니라 ui변경 요청이 들어왔습니다. 포인트폼의 ui가 완전히 바꾼다고 가정해봅시다. 그렇다면 이 공통 <code>Input</code>으로 포인트 폼을 유지시킬 수 있을까요?</p>
<p>ui가 달라졌기에 또 다른 input을 만들어야겠죠? 하지만 이미 포인트는 <code>Input</code>이라는 공통 태그에 강하게 묶여있기때문에 이 기능만 빼내기가 쉽지가 않아졌습니다.</p>
<p>이 때문에 <code>Input</code>태그에 기능적 사이드 이펙트가 발생할수도 있습니다.</p>
<h2 id="👻-공통의-저주-👻">👻 공통의 저주 👻</h2>
<p>그래서 오늘 얘기하고싶은 주제 <code>공통의 저주</code> 입니다.</p>
<p>흔히 우리는 <code>button</code>, <code>input</code> 이러한 것을 공통으로 많이 만들고 합니다.</p>
<p>그리고 개발을 한지 얼마안된상태로 <strong>&#39;아 저런것들을 공통으로 빼서 관리하는구나&#39;</strong> 라는 지식정도만을 알고 개발이 들어가게 되면 <code>공통의 저주</code>에 빠지게 되는것이죠.</p>
<p><code>input</code>을 공통으로 만드는게 좋다고하여 비슷한 ui들을 공통으로 맞추기 위해서 억지로 공통<code>input</code>태그에 <strong>props가 덕지덕지 붙게되고</strong>, <strong>각 기능에 대한 분기처리</strong>가 넣어지는 아주 거대한 <code>input</code>창고가 완성되는것이죠.</p>
<p>위의 예시는 비교적 눈치채지 쉬운 <code>저주</code>이지만 실제로는 눈치채지 못한채 저렇게 강하게 묶여있는 컴포넌트들이 꽤 있을 수 있습니다.</p>
<h2 id="저주는-해주해야지">저주는 해주해야지</h2>
<p>그래서 이 저주를 해결 하는 방법은 어떤것이 있을까요? </p>
<p>간단합니다. 여러분이 <code>ui</code>를 기준으로 공통으로 묶으려는 것을 <code>도메인</code>으로 바라보시면 됩니다.</p>
<p><code>로그인</code> input과 <code>포인트</code> input 2개의 도메인(관심사)은 다릅니다. 그렇기에 <strong>현재</strong>로써는 동일한 ui로 유지될 수 있지만 추후에 둘중에 하나에 기능이 추가될 수도 있고, 하나의 ui가 변할수도있습니다.</p>
<p>그렇기 때문에 이런경우에는 <code>LoginInput</code>, <code>PointInput</code> 과 같은 식으로 동일한 ui를 가지지만 다른 컴포넌트로 분리를 해줘야합니다.</p>
<p>흔히 똑같은 코드를 다른 컴포넌트에 적용을하면 <code>반복을 하지마라</code> 라는 철학으로 불편하게 느껴지기 때문에 <code>저주</code>에 걸리는것입니다.</p>
<h2 id="그래도-불편하시죠">그래도 불편하시죠?</h2>
<p><code>LoginInput</code>, <code>PointInput</code> 이렇게 2개로 쪼개시면 당연히 클린코드라는것을 아시는 여러분들은 불편하게 느껴지실겁니다. 똑같은 코드가 다른 컴포넌트에 있으니까요 👻</p>
<p>물론 이것을 해결하는 방법이 존재합니다. 바로 <strong>기능이 존재하지 않고</strong> <strong>ui만 존재하는 컴포넌트</strong>를 만드는것입니다.</p>
<p>그런후에 <strong>custom hook</strong>을 통해서 사용하는 도메인 측에서 로직을 주입해주는것이죠.</p>
<p><strong>Input</strong></p>
<pre><code class="language-tsx">// 스타일이 적용되있다고 가정하겠습니다.

interface InputProps&lt;T&gt; extends InputHTMLAttributes&lt;HTMLInputElement&gt; {
  input: T
}

export const Input = &lt;T,&gt;({ type, input ...props }: InputProps&lt;T&gt;) =&gt; {
  return &lt;input {...props} type={type} onChange={handleChangeInput} value={input} /&gt;;
};</code></pre>
<hr>
<p><strong>LoginInput</strong></p>
<pre><code class="language-tsx">export const useLoginInput = () =&gt; {
    const [input, setInput] = useState(&#39;&#39;);
    const isValid = input.length &gt; 8;

    const handleChangeInput = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
        setInput(e.target.value);
    };

      return {input, isValid, handleChangeInput};
}

export const LoginInput = () =&gt; {
  const {input, isValid, handleChangeInput} = useLoginInput();

  return (
      &lt;&gt;
        &lt;Input input={input} onChange={handleChangeInput} type=&quot;text&quot;/&gt;
          {!isValid ? &lt;div&gt;유효하지 않습니다.&lt;div&gt; : null}
    &lt;/&gt;
  )
}
</code></pre>
<hr>
<p><strong>PointInput</strong></p>
<pre><code class="language-tsx">export const usePointInput = () =&gt; {
    const [input, setInput] = useState(&#39;&#39;);

    const handleChangeInput = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
        const validPointInput = Number(e.target.value.replace(/[^0-9]+/g, &#39;&#39;));

        if (validPointInput &gt; currentHasPoint) {
          alert(`이용가능한 포인트는 ${currentHasPoint}입니다.`);
          setInput(currentHasPoint);
          return;
        }

        setInput(validPointInput);
        return;
    };

      const inputFormat = `${input.toLocaleString()}P`;

      return {inputFormat, handleChangeInput};
}

export const PointInput = () =&gt; {
  const {inputFormat, handleChangeInput} = usePointInput();

  return (
      &lt;&gt;
        &lt;Input input={inputFormat} onChange={handleChangeInput} type=&quot;number&quot; /&gt;
    &lt;/&gt;
  )
}</code></pre>
<h2 id="결론">결론</h2>
<p>이러한 관심사의 분리에 익숙하신 분들은 <code>저주</code>에 잘 걸리지 않지만 익숙하지 않은분들은 자주 걸리는 <code>저주</code>라고 생각됩니다.</p>
<p><strong>ui를 보고 공통으로 분리하지말고 도메인(관심사)를 보고 분리하자</strong> 이정도만 생각하신뒤에 개발을 하시면 잘 걸리시지 않을겁니다. 🐳 🐳</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[chrome-extension] chrome extension 으로 쿠키 다루기]]></title>
            <link>https://velog.io/@baby_dev/chrome-extension-chrome-extension-%EC%9C%BC%EB%A1%9C-%EC%BF%A0%ED%82%A4-%EB%8B%A4%EB%A3%A8%EA%B8%B0</link>
            <guid>https://velog.io/@baby_dev/chrome-extension-chrome-extension-%EC%9C%BC%EB%A1%9C-%EC%BF%A0%ED%82%A4-%EB%8B%A4%EB%A3%A8%EA%B8%B0</guid>
            <pubDate>Thu, 02 Nov 2023 17:31:15 GMT</pubDate>
            <description><![CDATA[<h1 id="🍪-브라우저의-쿠키-옮기기">🍪 브라우저의 쿠키 옮기기</h1>
<p>다음과 같은 상황을 겪어보신적이 있으실까요? </p>
<p>쿠키는 도메인에 종속적이기 때문에 <code>example.com</code>에 <code>name=ming</code> 이런 쿠키가 심기면 <code>dev.exmaple.com</code>과 같은 서브도메인에서는 이 쿠키를 받아주지만 <code>test.com</code>과 같은 다른 도메인에서는 이 쿠키를 받을 수 없습니다.</p>
<p>가끔 운영에서의 도메인과 개발에서의 도메인이 아예 다르게 되거나 각종 이유로 도메인이 다른 곳에 <code>name=ming</code> 쿠키를 심고  싶어질때가 있죠. </p>
<p>그것을 위해서 만든 <strong>chrome extension 개발기</strong>를 오늘 얘기해보려고 합니다.</p>
<h1 id="chrome-extension으로-시작하는-이유">chrome extension으로 시작하는 이유</h1>
<p>기존에는 <code>bookmarklet</code>을 이용하여 북마크를 클릭하면 <strong><code>document.cookie</code></strong>를 조작하는식으로 간단하게 개발할 예정이었습니다. </p>
<p><a href="https://www.hahwul.com/2017/12/04/coding-bookmarklet-vs-browser-extension/">bookmarklet이란?</a></p>
<p>하지만 기존의 cookie는 <code>httpOnly</code>옵션이 붙어서 내려오기때문에 document.cookie를 통해서 조회를 할 수가 없었는데요.</p>
<p>반면에 chrome extension의 <code>chrome.cookies</code> 를 이용하면 <strong>httpOnly</strong>의 쿠키까지 조회를 할 수 있어 이 방법을 채택하였습니다.</p>
<h1 id="chrome-extension의-동작-방식">chrome extension의 동작 방식</h1>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/3bef81b3-fb95-45b7-b619-391bcc3da68b/image.png" alt=""></p>
<p>chrome extension은 크게 3가지로 popup, background, content_script로 나눠집니다.</p>
<p><strong>popup</strong>은 말 그대로 우리가 크롬에있는 아이콘을 클릭하면 팝업이 열리게 되죠? 그 ui에 대한것을 담당하는것이 popup입니다.</p>
<p>보통 Popup.js에서 특정 버튼을 클릭하면 <code>sendMessage</code>를 통하여 event를 emit하고 background나 content_script에서 받도록 많이 이용을 합니다.</p>
<p><strong>background</strong>는 service-worker라고도 불리는 요소입니다.</p>
<p>특징으로는 크롬 익스텐션이 켜질때 실행되며 여러개의 탭이 존재하더라도 단 한번만 초기화되는 녀석입니다. 주로 extension내에서 이벤트를 관리할때 이용합니다.</p>
<p>이는 익스텐션 내부에 존재하는 실제 외부의 dom을 건드리거나 할수는 없습니다.</p>
<p><strong>content_script</strong>는 외부 웹페이지에서 실행되는 스크립트입니다. </p>
<p>예를 들어 크롬 익스텐션을 <code>https://example.com</code>에서 켰다면 이 페이지의 dom에 접근하거나, storage정보같이 페이지의 정보를 가져올 수 있습니다.</p>
<p>이 3개에 대해서 간단하게 안 상태로 어떻게 chrome extension을 만들었는지 알아보도록 하죠.</p>
<h1 id="⚙️-manifestjson">⚙️ manifest.json</h1>
<p>가장 먼저 알아볼 것은 <code>manifest.json</code>입니다. chrome extension에서의 설정 파일이라고 생각하시면 됩니다.</p>
<p>각각의 manifest 속성을 설명하기에는 너무 많기에 주로 이용하는것에 대한것만 설명할 예정입니다. 나머지 속성들은 다음 docs에서 확인하시면 됩니다.</p>
<p><a href="https://developer.chrome.com/docs/extensions/mv3/manifest/">https://developer.chrome.com/docs/extensions/mv3/manifest/</a></p>
<hr>
<h3 id="name-version-manifest_version">name, version, manifest_version</h3>
<p>필수로 적어야되는 필드입니다.</p>
<p>name은 이 익스텐션의 이름, version은 익스텐션의 버전, manifetst_version은 현재 3까지 존재하며 말그대로 manifetst.json의 버전을 의미합니다.</p>
<p>위에 올려드린 docs는 3버전에 대한 것입니다.</p>
<h3 id="permission">permission</h3>
<p>다음은 permission 필드, 즉 권한에 관한 내용입니다.</p>
<p>chrome extesion에서는 사용자의 크롬에 얼마나 접근이 가능하게 할지 설정하는 permission tab이 존재합니다.</p>
<p><a href="https://developer.chrome.com/docs/extensions/mv3/declare_permissions/#permissions">https://developer.chrome.com/docs/extensions/mv3/declare_permissions/#permissions</a></p>
<p>역시 tabs에는 여러가지 종류가 있는데요. 이번에는 cookie를 다룰때 이용하게 된 permission들을 알아보겠습니다.</p>
<pre><code class="language-jsx">  &quot;permissions&quot;: [
    &quot;cookies&quot;,
    &quot;storage&quot;,
    &quot;tabs&quot;,
  ],</code></pre>
<p><code>chrome.tabs</code>를 위한 <code>tabs</code>,
chrome extesion내의 <code>storage</code>이용을 위한 <code>storage</code>,
<code>chrome.cookies</code>를 이용하기 위한 <code>cookies</code></p>
<p>이렇게 3개를 이용하였습니다. 추후에 각각 어떻게 사용됬는지는 popup, background, content_script 영역에서 다룰 예정입니다.</p>
<h3 id="icons">icons</h3>
<p>말그대로 chrome extension에서 사용될 icon을 의미합니다. <code>프로프트 지니</code>를 예시로 보면 chatgpt icon이 있는것을 확인할 수 있죠.</p>
<pre><code class="language-jsx">  &quot;icons&quot;: {
    &quot;16&quot;: &quot;images/16.png&quot;,
    &quot;32&quot;: &quot;images/32.png&quot;,
    &quot;48&quot;: &quot;images/48.png&quot;,
    &quot;128&quot;: &quot;images/128.png&quot;
  },</code></pre>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/136a07c4-0420-4446-94da-0b4c573a4b1c/image.png" alt=""></p>
<h3 id="host_permissions">host_permissions</h3>
<p>말그래돌 host에 대한 권한을 설정하는겁니다.</p>
<p>특정한 도메인을 지정하는게 아니라 매치 패턴을 이용하여 정의를 할 수 있습니다. 이렇게 설정하면 매치패턴에 해당되는 도메인만 크롬익스텐션을 사용할 수 있도록 설정하는겁니다.</p>
<pre><code class="language-jsx">&quot;host_permissions&quot;: [
  &quot;*://developer.mozilla.org/*&quot;,
  &quot;*://*.example.com/*&quot;
]
</code></pre>
<h3 id="background">background</h3>
<p>위에서 설명했던 background에 대한 파일을 정의하는곳입니다.</p>
<pre><code class="language-jsx">  &quot;background&quot;: {
    &quot;service_worker&quot;: &quot;./background.js&quot;,
  },</code></pre>
<p>다음과 같이 실제 background파일이 있는곳을 경로로 잡아주시면 됩니다.</p>
<h3 id="commands">commands</h3>
<p>이 크롬익스텐션에 대한 단축키를 설정할 수 있습니다. </p>
<p>저는 크롬익스텐션을 열기위한 단축키로 <code>command + I</code>를 이용중입니다.</p>
<pre><code class="language-jsx">  &quot;commands&quot;: {
    &quot;_execute_action&quot;: {
      &quot;suggested_key&quot;: {
        &quot;default&quot;: &quot;Ctrl+I&quot;,
        &quot;mac&quot;: &quot;Command+I&quot;
      }
    }
  },
</code></pre>
<h3 id="content_scripts">content_scripts</h3>
<pre><code class="language-jsx">  &quot;content_scripts&quot;: [
    {
      &quot;matches&quot;: [&quot;https//*.example.com&quot;],
      &quot;js&quot;: [&quot;./content_script.js&quot;]
    }
  ]</code></pre>
<p>역시 위에서 봤던 content_scripts입니다. </p>
<p><code>content_scripts</code>는 외부 웹페이지에서 실행하는 스크립트이기에 <strong>어떤 웹페이지에서만 스크립트를 실행할지</strong> <code>matches</code> 옵션을 통해서 설정할 수 있습니다.</p>
<p><code>js</code>는 위의 background와 같이 <code>content_script.js</code>의 파일경로를 적어주시면 됩니다.</p>
<h1 id="📦-설계">📦 설계</h1>
<p>일단 쿠키 옮기기를 어떤 방식으로 설계했는지 부터 보시죠.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/4a6a9c39-ff3f-4e75-b32a-33d202cccbd3/image.png" alt=""></p>
<h2 id="✨-popup">✨ popup</h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/31fa396a-7cec-4bae-92d4-c9d6ee0cac19/image.png" alt=""></p>
<p>popup에서는 간단하게 그 쿠키에 대한 <code>html</code>,<code>css</code>로 ui를 잡고 js에서 content_script, background와 연결되는 로직을 작성해주었습니다.</p>
<p>위의 설계와 함께 보면 popup에서는 먼저 초기화가 되자마자 <code>background</code>에 메시지를 보냅니다. </p>
<p><code>chrome.runtime.sendMessage</code>를 이용하면 background에 메시지를 보내면 <code>popupOpen: true</code>를 보내 <code>background</code>에서 로직을 처리하도록 진행합니다. <strong>그런 후 응답을 받으면 날짜, 도메인을 수정하여 cookieValue에 저장해줍니다.</strong></p>
<pre><code class="language-jsx">let cookieValue = null;
let originCookieValue = null;

chrome.runtime.sendMessage({popupOpen: true}, (res) =&gt; {
  chrome.storage.sync.get(&quot;cookie&quot;, ({cookie}) =&gt; {
    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);
    originCookieValue = cookie ? cookie : null;
    cookieValue = cookie ? `a=${cookie.value};expires=${tomorrow.toUTCString()};domain=.test.com` : null; // 도메인 변경하여 설정
  })
})</code></pre>
<h3 id="case-1-로그인버튼-클릭시">case-1. 로그인버튼 클릭시</h3>
<pre><code class="language-jsx">$loginButton.addEventListener(&#39;click&#39;, () =&gt; {
  if(cookieValue) {
    chrome.tabs.query({}, tabs =&gt; {
        tabs.forEach(tab =&gt; {
        chrome.tabs.sendMessage(tab.id, {cookie: cookieValue});
      });
    });
    alert(&#39;성공적으로 등록되었습니다. 새로고침을 진행해주세요.&#39;);
    window.close();
  } else {
    alert(&#39;쿠키가 존재하지 않습니다.&#39;);
    window.close();
  }
})
</code></pre>
<pre><code class="language-jsx">chrome.tabs.query({}, tabs =&gt; {
  tabs.forEach(tab =&gt; {
    chrome.tabs.sendMessage(tab.id, {cookie: cookieValue});
  });
});
// content_script에 메시지를 보내는 방식입니다.</code></pre>
<p>클릭을 할시 <code>content_script</code>를 향해 sendMessage를 해줍니다. 이때 날짜, 도메인에 변경된 <code>cookieValue</code>를 content_script에 보내줍니다.</p>
<h3 id="case-2-로그아웃버튼-클릭시">case-2. 로그아웃버튼 클릭시</h3>
<pre><code class="language-jsx">$logoutButton.addEventListener(&#39;click&#39;, () =&gt; {
  chrome.tabs.query({}, tabs =&gt; {
    tabs.forEach(tab =&gt; {
    const yesterday = new Date();
    yesterday.setDate(yesterday.getDate() - 1);
    const expiredCookieValue = `a=${originCookieValue.value};expires=${yesterday.toUTCString()};domain=.test.com`

    chrome.tabs.sendMessage(tab.id, {cookie: expiredCookieValue});
  });
  alert(&#39;성공적으로 로그아웃 되었습니다. 새로고침을 진행해주세요.&#39;);
  window.close();
});</code></pre>
<p>로그인버튼 클릭했을시와 비교하면 <code>content_script</code>에 cookieValue를 보내는것은 동일하지만 로그아웃시에는 어제의 날짜를 담아 보내, 바로 expire시키도록 설정합니다.</p>
<h2 id="✨-background">✨ background</h2>
<pre><code class="language-jsx">chrome.runtime.onMessage.addListener(function(message, sender, sendResponse){
  if(message.popupOpen) { 
    chrome.cookies.get({url: &#39;https://example.com&#39;, name: &quot;a&quot;}, (cookie) =&gt; {
      if(cookie) {
        chrome.storage.sync.set({ cookie: cookie });
      } else {
        chrome.storage.sync.set({cookie: null});
      }
    });
   }
});
</code></pre>
<p>popup에서 보낸것을 <code>chrome.runtime.onMessage.addListener</code>를 통하여 이벤트를 받을 수 있습니다.</p>
<p>message에서 popupOpen의 true값을 읽고 <code>chrome.cookies</code>를 이용하여 기존에는 가지고오지 못했던 <code>httpOnly</code>가 적용된 쿠키까지 가지고 올 수 있게 됩니다.</p>
<p>그런 후 다시 popup에서 cookie값을 이용하기위해 chrome의 storage에 저장합니다. (이는 chrome extension의 storage입니다.)</p>
<h2 id="✨-content_script">✨ content_script</h2>
<pre><code class="language-jsx">  chrome.runtime.onMessage.addListener(msgObj =&gt; {
    document.cookie = msgObj.cookie;
  });</code></pre>
<p>놀랍게도 이게 전부입니다.. content_script에서는 이제 우리가 옮겨줄 페이지 <code>*.test.com</code>에 쿠키를 심어주는 스크립트가 존재합니다.</p>
<p>background와 동일하게 <code>chrome.runtime.onMessage.addListener</code>로 받고 <code>document.cookie</code>를 해주시면 됩니다.</p>
<h1 id="🐳-배포">🐳 배포</h1>
<p>chrome extension을 배포하려면 인증도 받아야하고 이것저것 할게 많습니다..</p>
<p>하지만 우리는 이것을 크롬 웹스토어 올려서 이용할것이 아니라 <code>개발 생산성</code> 증진을 위해서 이용하는것이기에 이렇게 할 필요가 없습니다.</p>
<p>그러면 어떤 방법으로 할 수 있을까요?</p>
<h3 id="chrome-extension-developer-mode">chrome extension developer mode</h3>
<p>chrome extension에는 개발자 모드가 있습니다. 밑의 사진에 우측 상단을 보시면 확인할 수 있습니다. chrome에서 <strong>확장프로그램 관리</strong>를 클릭시 다음과 같이 들어갈 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/ed7e8099-a615-4d7d-9f69-109d6e708836/image.png" alt=""></p>
<p>그후에 <code>압축해제한 확장 프로그램을 로드합니다.</code>를 클릭하고 만들어 놓은 extension을 넣어주면 크롬 웹스토어에서 다운로드없이 사용을 할 수 있습니다.</p>
<p>이제는 이 익스텐션을 이용하고 싶은 모두에게 이용할 수 있도록 해야겠죠?</p>
<p>프로젝트를 tag로 관리하면 버전별로 source code를 zip파일로 받을 수 있기때문에 이를 압축해제하여 <strong>chrome extension developer mode</strong>로 이용할 수 있습니다.
<img src="https://velog.velcdn.com/images/baby_dev/post/c1fffba8-e29e-4635-8b8d-1f138e342c30/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/707f52f9-1931-4c8d-8e9f-02be3950dd27/image.png" alt=""></p>
<h2 id="결론">결론</h2>
<p>실제로 chrome extension에서 제공하는 여러 api를 통해서 개발생산성을 향상시킬 수 있는 여러가지 툴들을 만들 수 있습니다.</p>
<p>popup, background, content_script의 역할과 공식문서의 api만 잘 봐도 문제없이 개발이 가능하기에 한번쯤 만들어보시면 좋을것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React-Query] fetchQuery vs prefetchQuery]]></title>
            <link>https://velog.io/@baby_dev/React-Query-fetchQuery-vs-prefetchQuery</link>
            <guid>https://velog.io/@baby_dev/React-Query-fetchQuery-vs-prefetchQuery</guid>
            <pubDate>Mon, 23 Oct 2023 17:48:14 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/baby_dev/post/fc1663dd-9870-4266-92ab-e23833a5b8fe/image.png" alt=""></p>
<p>안녕하세요~ 오늘은 약간 가벼운 주제로 가져오게된 react-query의 <code>fetchQuery</code>와 <code>prefetchQuery</code>에 대한 내용입니다.</p>
<h2 id="prefetchquery">prefetchQuery</h2>
<p>보통 Next.js를 이용하여 ssr을 이용하실때 그냥 fetch만으로 이용하시는 경우도 있지만 경우에 따라서 <code>react-query</code>를 사용할 때가 있습니다.</p>
<p>그런 경우 react-query에서는 <code>prefetchQuery</code>라는 메서드를 이용하여 ssr을 사용할 수 있도록 제공합니다.</p>
<pre><code class="language-jsx">  const queryClient = getQueryClient();

  await queryClient.prefetchQuery({
    queryKey: [&#39;test&#39;],
    queryFn: getTest
  });
</code></pre>
<p>다음과 같이 prefetchQuery를 <code>Next.js 12</code>에서는 <code>getServerSideProps</code>, <code>getStaticSideProps</code>, <code>Next.js 13 (App router)</code> 에서는 server component에서 이용하시면 server에서 미리 데이터를 만들어 놓은 후 이 다음에 fetch하는것에는 이미 완성된 초기 데이터를 이용하는것이죠.</p>
<pre><code class="language-jsx">const {data} = useQuery([&#39;test&#39;], getTest);
...
</code></pre>
<p>실제 사용할때는 client단에서 <code>prefetchQuery</code>를 이용했던것과 동일한 query key를 설정하시면 됩니다.</p>
<p>이렇게 이용할시 기존에 data가 <strong>undefined (pending) -&gt; data (fulfilled)</strong> 되던 방식이 undefined가 initial data로 초기화 되있는 상태가 됩니다.</p>
<h2 id="fetchquery">fetchQuery</h2>
<p>이제는 <strong>fetchQuery</strong>에 대해서 얘기해봐야겠죠?</p>
<p>사실 대부분 react-query에서 ssr을 이용할때 <code>prefetchQuery</code>를 이용합니다. 
<a href="https://tanstack.com/query/v4/docs/react/guides/ssr">https://tanstack.com/query/v4/docs/react/guides/ssr</a></p>
<p>docs만 봐도 <strong>prefetchQuery</strong>로 하는 예시로만 되어있어 보통 이것만 이용하는 경우가 많습니다.</p>
<hr>
<p>하지만 prefetchQuery는 항상 성공한 쿼리만 <code>dehydrate</code>를 해주게 됩니다. 그리고 성공한 데이터에 대해서 결과값을 return을 하지 않습니다.</p>
<p>*<em>반면에 <code>fetchQuery</code>는 실패할 경우 에러를 던지며, 결과값에 대한 return을 할 수 있습니다.
*</em></p>
<p>보통 저는 2가지 케이스에서 이용을 합니다.</p>
<ul>
<li>server에서 받은 queryData를 통하여 jotai의 <code>hydrateAtom</code> 초기화</li>
<li>ssr에서의 error boundary를 이용할시</li>
</ul>
<p>⭐️ *<em>server에서 받은 queryData를 통하여 jotai의 <code>hydrateAtom</code> 초기화
*</em></p>
<pre><code class="language-jsx">  const SearchFilter = () =&gt; {
    const queryClient = getQueryClient();
      const searchFilter = await queryClient.fetchQuery({
        queryKey: [&#39;searchFilter&#39;, DEFAULT_SEARCH_FILTER],
        queryFn: () =&gt; getSearchFilter(params),
      });

    return (
      &lt;HydateAtomProvider atomKey=&quot;searchFilterInputAtom&quot; value={searchFilter}&gt;
        &lt;SearchFilterContents data={searchFilter} atomValue={atomValue} /&gt;
      &lt;/HydateAtomProvider&gt;
    );
  }
</code></pre>
<p>⭐️ <strong>ssr에서의 error boundary를 이용할시</strong></p>
<pre><code class="language-jsx">// HydratedList.tsx

export const HydratedList = async ({ params }: Params) =&gt; {
  const queryClient = getQueryClient();
  await queryClient.fetchInfiniteQuery({
    queryKey: [&#39;searchResult&#39;],
    queryFn: () =&gt; getList(params),
  });
  const dehydratedSearchResult = dehydrate(queryClient);

  return (
    &lt;Hydrate state={dehydratedSearchResult}&gt;
      &lt;List params={params} /&gt;
    &lt;/Hydrate&gt;
  );
};</code></pre>
<pre><code class="language-jsx">  // page.tsx
  &lt;ErrorBoundary FallbackComponent={NoSearchContents}&gt;
    &lt;HydratedCarList params={params} /&gt;
  &lt;/ErrorBoundary&gt;</code></pre>
<blockquote>
<p>여기서도 보셨듯이 fetchInfiniteQuery를 사용하시면 무한스크롤이나 페이지네이션에 이용하는 useInfiniteQuery에 대해서 ssr처리를 할 수 있습니다. (prefetchInfiniteQuery도 있습니다.)</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js 13] 이모저모]]></title>
            <link>https://velog.io/@baby_dev/Next.js-13</link>
            <guid>https://velog.io/@baby_dev/Next.js-13</guid>
            <pubDate>Mon, 16 Oct 2023 16:06:31 GMT</pubDate>
            <description><![CDATA[<h1 id="그냥-이모저모">그냥 이모저모..</h1>
<p>Next.js 13 으로 개발할때 알면 좋은것들을 적으려 했는데 하나하나 적기에는 짜쳐서(?) 그냥 이모저모 모아봤습니다.</p>
<ul>
<li>isr vs ssr</li>
<li>searchParams</li>
<li>error boundary</li>
<li>느린 api를 호출하는 ssr (streaming ssr)</li>
<li>서버컴포넌트가 너무 빨라요..</li>
<li>Next.js with Node.js</li>
<li>서버컴포넌트 zero-bundle 써먹기</li>
</ul>
<h2 id="isr-vs-ssr">isr vs ssr</h2>
<p>먼저 isr, ssr를 비교하는 것을 알아볼 예정입니다. 하지만 대부분 Next.js 13까지 도달하신 분들은 이 개념자체는 다 알고 계시겠죠?</p>
<p>그래서 간단하게만 설명하자면 isr은 일정 시간을 기준으로 api를 fetch하여 데이터를 갱신하는것이고 ssr은 사용자 요청때 마다 api를 서버에서 fetch하는 것입니다.</p>
<p>여기서 중요한것이 뭘까요?</p>
<p>*<em>바로 isr과 ssr의 요청 시점입니다.
*</em></p>
<p>ssr은 <code>사용자의 요청시점에</code>, isr은 <code>빌드 타임</code>에 생성이 됩니다.</p>
<p>이 때문에 자주 변하지 않는 데이터라도 isr을 사용하지 못할때가 존재합니다. 바로 사용자의 <code>request header</code>를 참조해야될때입니다.</p>
<hr>
<p>주로 request header에 존재하는 cookie, userAgent등을 참조하여 무언가 처리가 필요할때는 isr을 사용할 수 없는것이죠.</p>
<p>이럴때는 어쩔수 없지만 ssr을 사용하시면 됩니다. 🐳</p>
<p>+) 여담이지만 제 옛날 글에 Next.js 13.3기준으로 컴포넌트 별 isr이 동작하지 않는다라고 글을 작성한적이 있는데 Next.js 13.4 (app dir stable)이 되면서 컴포넌트 별 isr이 동작하더군요. 이용할 수 있는곳에 많이 이용하시는 것을 추천드립니다.</p>
<h2 id="searchparams">searchParams</h2>
<p>개발하다보면 url에 상태를 저장해야할때가 있습니다. 예를 들어서 내가 url을 친구에게 공유하였을때 내가 보는 화면과 친구가 공유받은 url을 열었을때 보는 화면이 동일해야 할때 이용할 수 있죠.</p>
<p>Next.js에서는 searchParams를 다루는 방법이 2가지 상황이 존재합니다.</p>
<p>먼저 <strong>서버 컴포넌트</strong>일때입니다. <code>page.tsx</code>에서 props로 searchParams를 받아서 사용할 수 있습니다.</p>
<pre><code class="language-jsx">export default function Main({searchParams}) {
    reutrn ...
}
</code></pre>
<p>다음은 <strong>클라이언트 컴포넌트</strong>입니다.</p>
<p>클라이언트 컴포넌트에서는 <code>useSearchParams</code>라는 훅을 이용하여 searchparams를 조회할 수 있습니다. 참고로 이 훅은 읽기 전용이라 이것을 통해서 queryString을 set하는 동작은 할 수 없습니다.</p>
<pre><code class="language-jsx">
export const ClientComponent = () =&gt; {
  const searchparams = useSearchparams();

  return ...
}
</code></pre>
<h2 id="error-boundary">error boundary</h2>
<p>다음은 에러 바운더리입니다. 사실은 <code>isr vs ssr</code> 파트에 있어도 될 내용이지만 따로 얘기를 하고 싶어 분리를 했습니다.</p>
<p>에러가 발생했을때 그 에러에 대한 fallback처리를 할 수 있는 것이 에러 바운더리인데 기존에는 <code>client</code>에서 fetch하는 컴포넌트에서만 이용했었습니다.</p>
<p>하지만 이제는 서버 컴포넌트의 등장으로 컴포넌트 단위의 ssr, isr이 가능해서 여기에도 에러 바운더리를 적용할 수 있게 되었습니다.</p>
<p>하지만 서버 컴포넌트에서의 에러바운더리는 <strong>ssr</strong>에서만 이용할 수 있습니다.</p>
<p>아까 <code>isr vs ssr</code>에서 얘기했듯이 <strong>isr</strong>은 빌드타임에 호출을 하고 에러도 빌드타임에 발생하기 때문에 에러바운더리에서 잡을 수가 없습니다.</p>
<p>하지만 약간의 꼼수로 isr에서도 에러바운더리같은 동작을 할 수 있도록 할 수 있습니다.</p>
<p>*<em>바로 에러를 메모리(코드 내 변수)에 저장하는것입니다.
*</em></p>
<p>빌드타임에서 발생한 에러를 변수에 저장을 한뒤 실제 런타임이 됬을때 그 변수의 상태 (ex. <code>error: true</code>)를 참조하여 fallback을 보여주도록 구현할 수 있습니다.</p>
<h2 id="느린-api를-호출하는-ssr">느린 api를 호출하는 ssr</h2>
<p>만약 서버 컴포넌트가 5개이고 각각 fetch를 하는 페이지가 있다고 가정해보겠습니다. 만약 <code>Link</code>태그를 이용하여 이 페이지에 오면 어떻게 될까요?</p>
<p>사용자는 바로 완성된 페이지를 보게됩니다. 이는 <code>Link</code>태그의 prefetch기능때문인데요.</p>
<p>하지만 여기서 5개 중에 하나의 컴포넌트가 10초가 걸리는 api가 있다고 가정해보겠습니다. 이런 경우에서는 어떻게 동작을 할까요?</p>
<p>사실 그 느린 api를 부르는 컴포넌트만 늦게 불러와지고 나머지는 이미 완성되어있는 페이지를 바로 보여주는게 베스트지만 실제로는 10초동안 멈춰있는듯이 있다가 10초 뒤에 완성된 페이지를 보게됩니다.</p>
<hr>
<p>이를 해결하기 위해서 Next.js 13부터는 <strong>Streaming Ssr</strong>이라는 것을 이용할 수 있습니다.</p>
<p>매우 좋은 기능에 비해서 사용방법도 간단합니다. 바로 느린 api를 부르는 서버컴포넌트에 <code>Suspense</code>태그를 감싸면 저절로 streaming ssr이 동작하게 됩니다.</p>
<p>이 경우에는 페이지가 바로 넘어가게 되며 느린 api를 부르는 서버 컴포넌트는 fallback (주로 스켈레톤 ui를 보여줍니다.)을 보여주며 나머지는 완성된 페이지를 볼 수 있습니다.</p>
<p>제가 Next.js 13에서 가장 애용하는 기능중 하나입니디.🎉🎉</p>
<h2 id="서버컴포넌트가-너무-빨라요">서버컴포넌트가 너무 빨라요..</h2>
<p>Next.js 13으로 개발을 하다보면 이러한 상황을 겪을 때가 있습니다. 서버 컴포넌트로 이루어져있는 페이지에서 클라이언트 fetch를 하는 컴포넌트가 존재하면 눈에 띄게 느려보이는 경우죠.</p>
<p>사실 이게 느리다기 보다는 정상속도인데 서버 컴포넌트에서 미리 보여주는 속도가 너무 빠른 탓이죠..🥲</p>
<p>저는 보통 이러한 상황에서 2가지 상황으로 해결을 합니다.</p>
<ul>
<li><strong>client 상태를 이용하는 fetch의 경우 react-query의 hydrate를 이용하여 prefetch를 진행</strong></li>
<li><strong>클라이언트 컴포넌트의 결과물과 동일한 ui의 스켈레톤 ui 만들기 (그냥 회색 스켈레톤 ui도 가능)</strong></li>
</ul>
<p><strong>실제로 client에서 fetch를 이용해야만 하는 경우는 client의 상태에 fetch가 의존성이 있을때 입니다.</strong> 예를 들어서 무한스크롤정도가 있을것 같네요.</p>
<p>이런 경우에는 <code>react-query</code>의 hydrate를 이용하여 prefetch를 진행한 후에 client compoenent에서 fetch를 진행하면 클라이언트가 다운로드 받을때까지 컴포넌트가 안보이는 부자연스러운 동작을 없앨 수 있습니다. </p>
<hr>
<p>두번째 방법은 많이 사용되지는 않지만 script태그등으로 ui를 불러와서 그려주는 경우에 이용을 할 수 있습니다.</p>
<p>script태그가 불러오는 동안 스켈레톤 ui를 서버 컴포넌트로 그려준뒤 script태그가 로드가 되면 서버 컴포넌트를 날려버리고 불러온 ui를 대체하여 보여주는 것이죠.</p>
<h2 id="nextjs-with-nodejs">Next.js with Node.js</h2>
<p>Next.js를 도커와 같은 컨테이너 환경으로 빌드를 할때 <strong>ci머신(jenkins, github action)</strong>에서 볼 수 있는 에러가 있습니다.</p>
<pre><code class="language-bash">&gt; Build error occurred
TypeError [ERR_INVALID_ARG_TYPE]: The &quot;id&quot; argument must be of type string. Received undefined
    at new NodeError (node:internal/errors:387:5)
    at validateString (node:internal/validators:162:11)
    at Module.require (node:internal/modules/cjs/loader:1093:3)
    at require (node:internal/modules/cjs/helpers:108:18)
    at Object.verifyTypeScriptSetup (/web/src/node_modules/.pnpm/next@13.4.19_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/lib/verifyTypeScriptSetup.js:115:42) {
  type: &#39;TypeError&#39;,
  code: &#39;ERR_INVALID_ARG_TYPE&#39;
}</code></pre>
<p>바로 다음과 같은 에러인데요. 이 에러는 노드 버전이 Next.js와 잘 맞지 않아서 발생하는 문제입니다.</p>
<p>이 경우 노드버전을 확인하신후 docker의 노드버전을 18.18(lts) 버전 이상으로 맞춰주세요.</p>
<h2 id="서버컴포넌트-zero-bundle-써먹기">서버컴포넌트 zero-bundle 써먹기</h2>
<p>서버컴포넌트의 큰 장점중 하나인 <code>zero-bundle</code>..!</p>
<p>사실 실 개발에서 크게 체감을 느끼면서 이용한 경우가 많이 없을것입니다.</p>
<p>data를 fetch해주는 매개체로 이용을 하는게 대부분이고 실제로 client상태가 안들어가는 컴포넌트는 매우 적기 때문에 서버컴포넌트로 인한 <code>zero-bundle</code>의 이점을 누리기는 쉽지가 않죠.</p>
<hr>
<p>*<em>하지만 data를 파싱할때 이 서버 컴포넌트의 <code>zero-bundle</code>이 유용하게 이용될 수 있습니다. *</em>사실 데이터의 파싱, 가공을 하는 것은 백엔드에서 해주는 것이 최고의 케이스이긴하지만 그렇게 이상적으로 모든 계산이 백엔드에서 되는 경우는 많이 없습니다.</p>
<p>특히 날짜에 대한 처리같은것을 프론트쪽에서 많이 해야될 경우가 있죠.</p>
<p>예를 들어 dayjs라는 날짜 파싱 라이브러리를 이용한다고 했을때 이를 기존처럼 클라이언트 컴포넌트에서 처리를 한다면 dayjs 패키지의 용량만큼 초기번들에 추가되게 됩니다.</p>
<p>하지만 서버 컴포넌트에서 dayjs를 이용하여 날짜파싱을 진행하게 된다면 dayjs의 용량은 초기번들에 포함되지 않게됩니다. 말그대로 서버에서 일어난 일이기 떄문이죠.</p>
<p>*<em>그래서 이러한 데이터를 가공을 하는 라이브러리를 이용을 할때 서버 컴포넌트에서 import하여 처리하고 클라이언트에 내려주게 된다면 초기 번들의 사이즈를 경량화할 수 있습니다.
*</em></p>
<h1 id="결론">결론</h1>
<p>Next.js 13은 파도파도 뭔가 나오는것 같네용..</p>
<p>Next.js 13 잡기술 모음집은 아마 이 글로 마무리 되지 않을까 싶습니다. 혹시 Next.js 13 개발을 하시면서 자신이 주로 이용하는 팁같은게 있으시면 댓글로 공유해주시면 감사하겠습니다. ㅎㅎ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js 13] 폴더 구조 + 이쁜 잡기술]]></title>
            <link>https://velog.io/@baby_dev/Next.js-13-%ED%8F%B4%EB%8D%94-%EA%B5%AC%EC%A1%B0-%EC%9D%B4%EC%81%9C-%EC%9E%A1%EA%B8%B0%EC%88%A0</link>
            <guid>https://velog.io/@baby_dev/Next.js-13-%ED%8F%B4%EB%8D%94-%EA%B5%AC%EC%A1%B0-%EC%9D%B4%EC%81%9C-%EC%9E%A1%EA%B8%B0%EC%88%A0</guid>
            <pubDate>Fri, 06 Oct 2023 10:13:46 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/baby_dev/post/16a8c842-51f3-4325-8cad-8b7923347248/image.png" alt=""></p>
<p>안녕하세용. 이번에 작성해볼 next.js 관련 게시물은 폴떠 구조에 대한 내용입니다.
사실 <code>react</code>나 <code>next.js 12</code>를 하시는 분들은 자신만의 폴더구조 설계 방식이 있을겁니다.</p>
<blockquote>
<p>components, utils, types, service, pages ... 이렇게 많이 존재하겠죠?</p>
</blockquote>
<p>하지만 이와 같은 방식으로 Next.js 13을 개발하게 된다면 갈수록 뭔가의 불편함을 느끼게 됩니다. Next.js 13에서 page를 만드는 새로운 방식떄문인데요.</p>
<p>그래서 Next.js에서는 어떻게 page들과 그 외의 폴더들을 관리하는지 그리고 약간의 잡기술을 하나 알려드릴까합니다.</p>
<p>*<em>docs -&gt; <a href="https://nextjs.org/docs/app/building-your-application/routing/colocation">https://nextjs.org/docs/app/building-your-application/routing/colocation</a>
*</em></p>
<h2 id="app-router에서-만날-수-있는-폴더구조-issue">App router에서 만날 수 있는 폴더구조 issue</h2>
<p>Next.js 13 (app router) 에서는 폴더내에 <code>page.js, page.tsx</code> 같은 네이밍을 작성했을때 그 폴더의 네이밍대로 routing이 생기게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/83c1b4f1-8e12-4432-a806-756ed8fb54d7/image.png" alt=""></p>
<p>하지만 이것때문에 나중에 개발에 혼동이 생기게됩니다. 한번 예시를 볼까요?</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/42bad04f-11cb-40ef-8200-19bab6192803/image.png" alt=""></p>
<p>저는 <code>material icon</code>이라는 vscode extension으로 비교적 구분하기 쉽지만 그래도 페이지들이 다른 폴더들과 똑같은 depth애서 위치하니까 뭐가 페이지인지 구별하기가 어려운 문제가 있습니다. 😥</p>
<hr>
<p>또한 <code>components/page.tsx</code> 이런식으로 작성을 하면 어떻게 될까요?? 그러면 app router는 <code>components</code>라는 네이밍을 가진 route를 생성합니다.</p>
<p>이 떄문에 <code>{url}/components</code> 라는 경로로 접속이 되는 이상한 일이 발생할 수 있죠.</p>
<h2 id="route-group--private-folder">Route group &amp; private folder</h2>
<p>그래서 Next.js 13에서는 Route group과 private folder 라는 것을 통하여 이 문제를 해결할 수 있도록 제공하였습니다.</p>
<h3 id="route-group">Route group</h3>
<p>Route group은 말그대로 route가 될 수 있는 페이지들을 하나의 그룹으로 묶을 수 있게 해줍니다.</p>
<p>문법은 간단하게 <code>(name)</code> 이런식으로 폴더 네이밍을 작성해주시면 됩니다. 예시를 한번 보도록 하죠.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/8f4dcce9-08fc-4790-8db2-0a200ad0bc54/image.png" alt=""></p>
<p>이렇게 route들을 다른 폴더들과 헷갈리지 않게 구분을 할 수 있고 각각의 도메인에 맞게 그룹을 나눌 수 도 있습니다. 이렇게만 나누더라도 확 구분이 잘되는 것을 볼 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/d10fb00c-2d91-4a34-9dec-2888cee228d8/image.png" alt=""></p>
<h3 id="private-folder">private folder</h3>
<p>자 이제는 <code>components/page.tsx</code> 만약 이런식으로 쓰더라도 route를 생성하지 않도록 설정해보죠. </p>
<p>이것도 역시 간단합니다. private folder를 이용하면 되는데요.</p>
<p>문법은 <code>_folder</code> 이런식으로 작성하면 이 폴더는 route로 생성되지 않습니다.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/454ba016-bbca-4511-85d7-5ab12f67b39f/image.png" alt=""></p>
<p>실제로 적용을 한번 해보도록 하죠. </p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/9942dee0-0a41-4061-afcb-eb508431ed47/image.png" alt=""></p>
<p>드디어 route group을 이용하여 route들끼리 묶고 그 외의 것들은 route를 생성하지 못하게 private처리까지 해주었습니다.</p>
<p>하지만 여기서 문제가 드러나네요.</p>
<p>*<em><code>material icon</code>으로 인해 알록달록하게 유지되던 폴더 색깔이 단번에 회색으로 덮여버렸습니다.
*</em></p>
<p>오히려 private을 적용하기보다 보기 어려워졌네요.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/1c6c1dcf-aba4-46fd-aaab-898f00f96bd7/image.png" alt=""></p>
<p>route 별로 components를 관리하게 된다면 더더욱 알아보기 힘드네용.. 🥲🥲</p>
<h2 id="잡기술-material-icon을-custom-하기">(잡기술) material icon을 custom 하기</h2>
<p>그러면 이제 칙칙한 회색으로 변해버린 폴더들을 다시 꾸며보도록 하겠습니다. <code>material icon</code>에서는 정말 다양한 폴더에 따른 icon image를 제공하는데요.</p>
<p>여기서 자신이 원하는 것으로 custom을 해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/d0678502-2408-4522-91cc-c0c4e33c00d4/image.png" alt=""></p>
<p>일단 vscode의 이러한 extension 설정을 건드릴 수 있는 파일이 어떤것인지 아시나요??</p>
<p>바로 <strong><code>settings.json</code></strong>입니다.</p>
<p>vscode editor에서 <code>ctrl (command)</code> + <code>shift</code> + <code>p</code> 를 누른 뒤에 settings.json을 검색하셔서 들어가보세요.</p>
<p>그러면 현재 자신의 vscode에 설정되어있는 extension들의 config를 볼 수 있습니다.</p>
<hr>
<p>그렇다면 여기서 material icon에 대한 설정을 건드린다면 icon이 알록달록하게 바뀌겠죠? </p>
<p>하지만 여기서 설정을 하게 된다면 오직 이용하는 자신만 알록달록하게 폴더를 이용할 수 있습니다. </p>
<p>*<em>그래서 editor의 <code>settings.json</code>을 수정하는것이 아닌 이 프로젝트를 clone한 모두에게 알록달록한 아이콘을 볼 수 있도록 설정해보겠습니다.
*</em></p>
<h3 id="vscode">.vscode</h3>
<p><code>.vscode</code> folder를 만든후 <code>settings.json</code>을 만들면 프로젝트의 모두가 동일한 extension 설정을 바라볼 수 있습니다. </p>
<p>*<em>다음과 같이 설정하면 자신이 설정한 폴더의 설정을 custom Icon으로 설정할 수 있습니다.
*</em></p>
<p>폴더 icon 외에도 다른것을 custom하게 만들고 싶다면 밑의 docs를 참조하시면 됩니다.
<a href="https://marketplace.visualstudio.com/items?itemName=PKief.material-icon-theme">material-icon-theme docs</a></p>
<pre><code class="language-js">// setting.json

{
  &quot;material-icon-theme.folders.associations&quot;: {
    &quot;_components&quot;: &quot;Components&quot;,
    &quot;_constants&quot;: &quot;Constant&quot;,
    &quot;_hooks&quot;: &quot;Hook&quot;,
    &quot;_service&quot;: &quot;Api&quot;,
    &quot;_store&quot;: &quot;Container&quot;,
    &quot;_types&quot;: &quot;Typescript&quot;,
    &quot;_utils&quot;: &quot;Utils&quot;,
    &quot;(route)&quot;: &quot;Routes&quot;,
  }
}
</code></pre>
<p>마지막으로 폴더 결과물을 보시죠.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/ab2fc428-22ea-4968-b5db-d4cdbda3aadb/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js 13] App router로 개발을 시작할때 알면 좋을것 🐳]]></title>
            <link>https://velog.io/@baby_dev/Next.js-13-App-router%EB%A1%9C-%EA%B0%9C%EB%B0%9C%EC%9D%84-%EC%8B%9C%EC%9E%91%ED%95%A0%EB%95%8C-%EC%95%8C%EB%A9%B4-%EC%A2%8B%EC%9D%84%EA%B2%83</link>
            <guid>https://velog.io/@baby_dev/Next.js-13-App-router%EB%A1%9C-%EA%B0%9C%EB%B0%9C%EC%9D%84-%EC%8B%9C%EC%9E%91%ED%95%A0%EB%95%8C-%EC%95%8C%EB%A9%B4-%EC%A2%8B%EC%9D%84%EA%B2%83</guid>
            <pubDate>Tue, 26 Sep 2023 15:22:22 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>제가 처음 Next.js 13을 사용할때만 되더라도 대부분 아직 13버전을 이용하지 않을때였는데 요즘은 대부분 13으로 시작을 많이 하시더라고요.</p>
<p>그래서 Next.js 13의 <code>App Router</code>를 이용했을때 알아야 할것, 알면 좋은 것들에 대해서 정리해볼까 합니다. (App Router가 아니더라도 Pages 에서도 유용할것 입니다.)</p>
<h2 id="왜-app-router를-쓰시나요">왜 App Router를 쓰시나요?</h2>
<p>일단 먼저 왜 <code>App Router</code> 를 쓰시는지 다시 생각해보세요. <code>App Router</code>에 익숙하지 않다면 서버 컴포넌트와 클라이언트 컴포넌트때문에 생산성이 저하된답니다. 그래서 빠르게 제품을 만들어야 될 경우에는 개발속도에 발을 잡을 수 있죠.</p>
<p>또한 서버 컴포넌트의 존재로 <code>Pages Router</code>보다 많은 메모리를 사용하게 됩니다.</p>
<p>그래서 <code>App Router</code>를 사용할때는 서버 컴포넌트의 장점인 <strong><code>컴포넌트 별로 다른 렌더링 전략</code></strong>을 가져가야만 하거나 <strong><code>App Router를 충분히 사용해본 경우</code></strong> 에 사용하는것을 추천합니다.</p>
<h2 id="nextjs-image-컴포넌트">Next.js Image 컴포넌트</h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/4f2a032a-7a75-4c83-b660-69dd774610b3/image.png" alt=""></p>
<p><a href="https://velog.io/@baby_dev/%EA%B7%B9%ED%95%9C%EC%9D%98-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%84%B1%EB%8A%A5%EC%B5%9C%EC%A0%81%ED%99%94-1%ED%8E%B8-Nextjs-13">Next.js13 극한의 성능 최적화</a> 예전에 이 글에서도 한번 다뤘던 내용입니다. </p>
<p>Next.js에서는 <code>Image</code> 컴포넌트가 존재하는데요. 굉장히 매력적인 기능이 많은 컴포넌트입니다. </p>
<p>하지만 그렇게 많은 기능이 있을수록 <code>Next.js</code>의 <strong>서버</strong>에서 해주는 일이라는것을 잊지 마세요. 사이트에 이미지가 많은 경우 유저가 한번 들어올때 그 많은 이미지들이 최적화하는 과정을 <code>Next.js</code> 서버에서 해주고 있는것입니다.</p>
<p><strong>서버컴포넌트 + 상황에 따른 <code>route handler</code> 여기에 이미지 최적화</strong>까지 하나의 서버에서 하면 <code>Next.js</code> 서버가 터지는건 일순간이겠죠.</p>
<p>페이지에 이미지가 거의 없거나 사용자가 그렇게 크지 않을것 같은 사이드 프로젝트정도에는 이용하셔도 괜찮습니다.</p>
<p>하지만 실제 운영에서 이용하실때는 <code>cdn</code>을 이용하여 이미지를 최적화하는것을 추천드립니다.</p>
<blockquote>
<p>이와 유사하게 Next.js내부에서 Optimizing을 해주는것은 그냥 아무비용없이 해주는것이 아닐겁니다. 수시로 모니터링을 하고 메모리 이슈가 있다면 한번 Optimizing부분을 의심해보세요.</p>
</blockquote>
<h2 id="nextjs-13의-버전-팔로우">Next.js 13의 버전 팔로우</h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/36d223a4-e89a-4a6c-8bdd-78ed80b56375/image.png" alt=""></p>
<blockquote>
<p>13.5 버전 업데이트</p>
</blockquote>
<p>최근에 <code>Next.js 13.5</code> 버전이 나온것을 알고 계신가요? <code>App router</code>의 고질적인 문제인 <code>메모리 이슈</code>를 잡아낸것을 확인할 수 있습니다. </p>
<p>실제로 <code>13.5</code>를 적용한뒤 AWS의 메모리 사용량이 50프로 가까이 줄어들은것을 확인할 수 있었습니다.
이외에도 기존 13.3까지는 컴포넌트 별 <code>isr</code>이 잘 동작하지 않았었는데 그 문제도 <code>13.4</code>에서  고쳐졌더라고요.</p>
<p>이와같이 <code>App router</code>는 아직 많은 개선이 이뤄지고 있습니다. 무려 438개의 개선을 이뤄냈다고 하네요. (13.4 -&gt; 13.5)</p>
<p>그래서 다른 라이브러리들은 안정적으로 버전을 관리하는것을 추천하지만 <code>App router</code>를 사용하실때는 주기적으로 버전 업데이트를 체크하신뒤 따라가시는것을 추천드립니다.</p>
<h2 id="style-framework">style framework</h2>
<table>
<thead>
<tr>
<th>sass</th>
<th>vanilla-extract</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/baby_dev/post/96597c08-d51e-481a-a700-747049c7f67b/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/baby_dev/post/fb382fc1-67ff-4bb1-b9e3-95430f21698a/image.png" alt=""></td>
</tr>
</tbody></table>
<p><code>App Router</code>를 선택한 순간 style framework에도 제약이 생깁니다. </p>
<p>물론 서버 컴포넌트를 <code>fetch</code>만 하고 client에 props만 넘기는 용도로만 사용하면 굳이 제약은 없을것 같지만요.</p>
<p>일단 서버 컴포넌트에 css를 입히려면 빌드타임에 css를 생성해야합니다. 그래서 기존에 이용하던 대부분의 <code>css-in-js</code>를 사용하기에는 어려울것입니다.</p>
<blockquote>
<p><a href="https://nextjs.org/docs/app/building-your-application/styling/css-in-js">https://nextjs.org/docs/app/building-your-application/styling/css-in-js</a></p>
</blockquote>
<p>그래서 <code>zero-runtime</code> 라이브러리인 <code>vanilla-extract</code>, <code>panda-css</code>등을 이용해야만 할것입니다. 하지만 이 라이브러리들도 완벽하게 서버 컴포넌트를 지원해줄지는 의문이죠.</p>
<p>vanilla-extract도 얼마전까지 서버 컴포넌트에서 이용하는것에 관련해서 이슈가 있었고, 서버 컴포넌트의 업데이트등에 의해 충분히 되던 코드가 안될 수 있기 때문입니다.</p>
<p>저는 그래서 <code>sass</code>와 <code>css module</code>을 이용하는것을 추천하는 편입니다.</p>
<p><code>classnames</code> 라이브러리와 함께 사용하면 css-in-js의 생산성에는 못미쳐도 나쁘지 않은 생산성이 나오기때문에 충분히 이용할만한것 같습니다.</p>
<h2 id="상황에-따른-렌더링-전략-채택">상황에 따른 렌더링 전략 채택</h2>
<p><code>App router</code>를 선택하셨다면 대부분 이 이유라고 생각됩니다. 사실 서버컴포넌트의 <code>zero-bundle</code>은 그렇게 크지 않은것 같습니다. 서버 컴포넌트는 data fetch의 용도로만 이용하고 실제로는 <code>client component</code>로 도배를 할 수 밖에 없기 때문이죠.</p>
<p>그렇다면 어떻게 렌더링 전략을 채택하면 될까요? 
사실 이것은 너무 당연하게 얘기되는 <code>자주 변하면 SSR</code>, <code>아니면 SSG, ISR</code> 이렇게 사용을 하면 됩니다. 하지만 조금 더 상세하게 알아보도록 하죠.</p>
<p><strong>일단 <code>ISR</code>을 최우선 선택으로 생각해보세요.</strong> 이 데이터는 ISR에 이용할 수 있을것인가?를 먼저 생각해보시면 됩니다. 예를 들어 어드민에서 받아오는 값이 있다고 가정해보겠습니다.</p>
<p>이런 경우에는 어드민이 업데이트 될시에만 데이터가 변경되기때문에 <code>On-demand revalidation</code>을 이용하면 어드민이 변경될때만 그 컴포넌트는 fetch를 하기때문에 불필요한 fetch를 하지 않아도 됩니다.</p>
<blockquote>
<p>추가적으로 블로그나 프론트 하드코딩으로 되어있는 컴포넌트가 아니면 <code>SSG</code>를 쓸수있는 경우는 많이 없을겁니다. </p>
</blockquote>
<hr>
<p><code>SSR</code>은 사용자가 fetch할때마다 서버에서 fetch를 하고 server component를 렌더하기 때문이죠. 이는 역시 <code>Next.js</code>의 메모리 이슈에 연계됩니다. 하지만 사실 <code>정적인 데이터</code>거나, <code>어드민 데이터</code>가 아니라면 SSR로 사용할 수 밖에 없습니다. 단지 <code>ISR</code>이 되는지 생각을 해보고 이용하셨으면 합니다.</p>
<h2 id="서버-로그">서버 로그</h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/9a2b1776-a166-42d6-a8ba-18554cc43b9e/image.png" alt=""></p>
<p><code>App router</code>로 개발을 할때 가장 중요한 요소중 하나라고 생각됩니다. </p>
<p>서버 컴포넌트에서 fetch를 할때는 크롬 개발자 도구의 <code>네트워크</code>, <code>콘솔</code>탭에 아무런 정보가 남지 않습니다. 사실 당연한거죠. 서버 컴포넌트는 서버니까요.</p>
<p>로컬에서 개발을 할 경우에는 터미널에 console.log가 보이기 때문에 큰 불편함이 없지만 이제 운영에 올라간 경우에는 브라우저에서 아무런 디버깅을 할 수 없습니다.</p>
<p>이것을 위해서는 프론트에서도 백엔드처럼 로깅작업이 필요합니다.</p>
<ul>
<li><strong>cloud 서비스</strong>: AWS cloudwatch, datadog등을 이용하여 로깅작업을 하면 됩니다.</li>
<li><strong>그 외의 물리서버</strong>: nodejs의 winston 라이브러리를 이용한뒤 컨테이너의 <code>/var/log</code> 폴더에 떨어트리면 됩니다.</li>
</ul>
<p>이 작업과 함께 client의 에러 로그를 위해 <code>Sentry</code>를 함께 설정하면 모든 로그를 대체할 수 있습니다.</p>
<h1 id="결론">결론</h1>
<p>다음과 같이 설정을 하면 Next.js 13을 시작할때 심신의 안정을 가져올 수 있습니다..👻</p>
<p>이 글을 시작으로 Next.js 13 관련 게시물들을 작성할 예정입니다. 많은 관심 부탁드립니당.🎉</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 개발의 미래는 어디로 향할까?]]></title>
            <link>https://velog.io/@baby_dev/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9D%98-%EB%AF%B8%EB%9E%98%EB%8A%94-%EC%96%B4%EB%94%94%EB%A1%9C-%ED%96%A5%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@baby_dev/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9D%98-%EB%AF%B8%EB%9E%98%EB%8A%94-%EC%96%B4%EB%94%94%EB%A1%9C-%ED%96%A5%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Sun, 24 Sep 2023 12:41:03 GMT</pubDate>
            <description><![CDATA[<p>&#39;프론트엔드는 6개월에 한번씩 트렌드가 바뀐다&#39; 라는 말이 있을 정도로 프론트엔드 생태계는 굉장히 변화가 잦은 편입니다. 이런것 때문에 트렌드를 따라가기 힘들어하는 경우도 있습니다.</p>
<p>하지만 어떤 흐름으로 흘러가는지는 미리 알아둔다면 어떤 공부를 해보면 좋을지 그리고 이 생태계가 무엇을 지향하는지 정도 알 수 있을겁니다.</p>
<h1 id="현재의-프론트엔드-트렌드">현재의 프론트엔드 트렌드?</h1>
<p>현재의 프론트엔드 트렌드를 종류별로 한번 구분해봅시다.</p>
<h3 id="spa-프레임워크">SPA 프레임워크</h3>
<ul>
<li><strong>react</strong></li>
<li>angular</li>
<li>vue</li>
</ul>
<p>가장 유명한 3개의 SPA 프레임워크입니다. 이중에서도 react가 가장 큰 지분을 차지하고 있으며, Nextjs, recoil, react-query 등등 유명한 프레임워크(라이브러리)들도 모두 react를 따라가고 있음을 볼 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/6542360b-eb6f-4a89-88a0-1faae0e3fefc/image.png" alt=""></p>
<h3 id="meta-framework">Meta framework</h3>
<ul>
<li>Nextjs</li>
<li>Nuxtjs</li>
<li>Remix</li>
</ul>
<p>SPA 프레임워크에게 개발의 편의성과 렌더링 전략을 제공하는 meta framework들입니다. react에서는 Nextjs, vue에서는 Nuxtjs, 그리고 떠오르는 meta framework인 remix정도가 있을것 같습니다. 
여기서도 역시 react를 따라 Nextjs가 가장 큰 지분을 차지 합니다.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/77029f64-573c-433a-a803-72e1a7518042/image.png" alt=""></p>
<h3 id="css-in-js">css-in-js</h3>
<ul>
<li>styled-components</li>
<li>emotion</li>
<li>styled-jsx</li>
</ul>
<p>js내에서 css를 다룰 수 있도록 하는 css-in-js들입니다. 아직까지는 runtime에 style을 넣는 styled-components, emotion등이 대세인것을 볼 수 있습니다. 
이 3개 모두 비슷한 수준의 지표를 보여주네요.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/0228998c-9ba9-4ede-886c-6dfeacbeaf5a/image.png" alt=""></p>
<h3 id="module-bundler">module bundler</h3>
<ul>
<li>webpack</li>
<li>vite</li>
<li>esbuild</li>
<li>rollup</li>
</ul>
<p>요즘 없어서는 안되는 빌드 도구들입니다. 역시 <code>webpack</code>이 압도적인 1위를 지키고 있고 그 뒤로 esbuild, rollup, vite가 오네요.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/bf5909db-d8ef-4e90-a69e-f995c592bdb6/image.png" alt=""></p>
<h3 id="기타-생산성-향상-도구">기타 생산성 향상 도구</h3>
<ul>
<li>typescript</li>
<li>eslint</li>
</ul>
<p>요즘 개발할때 거의 필수인 친구들이죠~</p>
<h1 id="⚛-react-언제까지-생태계-정점에-있을것인가">⚛ react 언제까지 생태계 정점에 있을것인가..</h1>
<p>아까도 봤듯이 SPA 프레임워크중 가장 압도적인 위치를 자랑하는 react, 과연 언제까지 이 react의 시대일까요? 
react를 대체하려는 시도는 없을까요?</p>
<p>과연 react에 어떤 문제가 있고 어떻게 개선한 라이브러리들이 나오고 있을까요?</p>
<h2 id="preact"><a href="https://preactjs.com/">preact</a></h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/fd8ce6d6-e714-4256-b18e-e88fffa1cb72/image.webp" alt=""></p>
<p>가장 먼저 볼 녀석은 <code>preact</code>입니다. </p>
<p>react와 동일하게 이용할수있지만 3kb정도로 매우 가볍습니다. 가볍기때문에 초기에 불러와지는 로드속도 향상 및 저사양기기에서도 매우 잘돌아갈것입니다.
또한 최근에는 useSignal이라는 새로운 패러다임을 내세우며 react와의 다른길을 갈 준비를 하고도 있죠.</p>
<p>하지만 react를 모방한 라이브러리이기때문에 완전히 react와 sync가 맞는것이 아닙니다.
react용으로 만들어진 여러 라이브러리들과 호환이 잘 안되는 경우도 있습니다.</p>
<p>예를들어 nextjs13의 서버 컴포넌트에서는 renderToPipableStream이라는것을 이용하는데 이 메서드가 preact에는 구현되어있지않기에 nextjs13과 preact를 혼합해서 쓰는것은 문제가있습니다.</p>
<blockquote>
<p>preact가 지향하는 바는 가벼운 react다.</p>
</blockquote>
<h2 id="millionjs"><a href="https://million.dev/">millionjs</a></h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/40cc2d11-a16e-4446-8129-adcb5228d664/image.webp" alt=""></p>
<p>이번에 볼것은 million.js입니다. 사이트를 들어가서 확인하니 react app을 67프로까지 빠르게 해준다고 하네요. 👻👻</p>
<p>millionjs는 react를 대체한다기보다는 react-dom을 대체하는 라이브러리입니다. 그렇기 때문에 기존에 사용하는 react를 변경할 필요가 없습니다.</p>
<p>millon.js는 react-dom의 <code>virtual-dom</code>의 방식을 버리고 <code>block</code> 방식이라는 것을 선택하였는데요. <code>block</code>에 대한 자세한 설명은 <a href="https://ktseo41.github.io/blog/log/virtual-dom-back-in-block.html">https://ktseo41.github.io/blog/log/virtual-dom-back-in-block.html</a> 게시물에 자세히 나와있습니다.</p>
<hr>
<p>이것도 preact와 비슷하게 3kb미만의 크기의 가벼움을 가지고 있습니다. 하지만 preact보다 나은점은 <code>react</code>자체를 변경한것이 아니기 때문에 react의 다양한 생태계를 함께 이용하기에는 <code>millionjs</code>가 훨씬 좋은 선택이라고 할 수 있죠.</p>
<p>그렇지만 이런 라이브러리는 항상 한계가 있었죠? 대놓고 공식문서에서도 한계를 적어주었습니다.</p>
<p>바로 <a href="https://million.dev/docs/rules">rules of block</a> 이죠. 앞서 설명을 한 <code>block</code>방식에 한계가 있다는것인데요. </p>
<ul>
<li>component에서 early return 안됨</li>
<li>spread 문법 이용불가</li>
<li>map대신에 <code>&lt;For /&gt;</code> 문을 이용해야함.</li>
</ul>
<p>이러한 개발방식의 변화가 <code>millon.js</code>의 한계라고 볼 수 있습니다.</p>
<blockquote>
<p>millonjs 역시 가벼운 react를 지향하고 있다.</p>
</blockquote>
<h2 id="solidjs"><a href="https://www.solidjs.com/">solidjs</a></h2>
<p>.<img src="https://velog.velcdn.com/images/baby_dev/post/e92290d1-0a6d-40a4-87af-0ee7d41a0c2f/image.png" alt=""></p>
<p>다음은 solidjs입니다. 개인적으로 react를 대체할 가능성이 그나마 높아보이는 라이브러리입니다.</p>
<p>역시 이녀석의 특징중 하나는 <code>가벼움</code>입니다. 위에 설명한것들과 동일하게 virtual-dom을 이용하지 않기때문에 가볍고 빠르죠.</p>
<p>그리고 위의 <code>preact</code> 에서도 언급한 <code>signal</code>을 이용하는 친구입니다. 간단하게 signal에 대해서 설명하자면 state의 <code>reactive(반응형)</code>이라고 생각하시면 됩니다.</p>
<p>state의 경우 상태가 변경되면 거기에 해당하는 컴포넌트 트리가 모두 변하게 되는데 <code>signal</code> 같은 경우에는 상태를 구독을 하는 형식이기에 오직 변한 상태들만 인식하여 리렌더링을 하기에 <code>state</code> 보다 훨씬 성능이 좋습니다. </p>
<p>react의 문법과 비슷하여 리액트를 하는 사람들에게는 큰 러닝커브가 존재하지 않을거 같고 유일한 흠은 아직까지 생태계가 리액트만큼 자리잡지 못했다정도 일것 같네요.</p>
<blockquote>
<p>solidjs는 가벼움과 state대신 signal을 지향한다.</p>
</blockquote>
<h2 id="qwik"><a href="https://qwik.builder.io/">qwik</a></h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/23e5d6bd-aac0-474d-bbbf-51bc7b4ffab3/image.png" alt=""></p>
<p>다음은 Qwik입니다. 한때 가장 빠른 자바스크립트 프레임워크라는것으로도 유명했었죠.</p>
<p>qwik의 가장 큰 특징은 <code>resumable (재개가능한)</code> 입니다. nextjs 같은 서버 렌더링 프레임워크에서 하는 방식인 <code>하이드레이션</code>을 버리고 선택한 방식이죠.</p>
<p>사실 <code>하이드레이션</code>은 react에서 처음불러오는 속도는 비약적으로 빨라지지만 <strong>TTI(유저상호작용시간)</strong> 은 리액트와 다를게 없습니다. 결국은 js bundle이 넘어와야 상호작용이 되기 때문이죠.</p>
<p>하지만 <code>resumable</code> 같은 경우에는 <strong>TTI</strong>자체를 개선을 하였습니다. 제로 하이드레이션을 하며 서버측 렌더링을 중지하고 browser로 언제든지 이동을 하며 js를 불러올 수 있으며 다시 서버 렌더링으로 재개가 가능합니다. 여기에 qwik은 실제 상호작용에만 필요한 js번들을 1kb미만으로 줄여 상호작용시간이 매우 빠르게 할 수 있도록 구현을 하였습니다.</p>
<p>또한 <code>preact</code>, <code>solidjs</code>와 같이 <code>signal</code>을 이용하는 프레임워크입니다. 역시 매우 좋은 프레임워크이며 react와 약간 상이한 문법, 생태계의 부족을 제외하면 충분히 미래에 사용될수있을 프레임워크입니다.</p>
<blockquote>
<p>qwik은 signal 그리고 nextjs와 비교될 zero-hydration을 지향한다.</p>
</blockquote>
<hr>
<h1 id="그래서-react는-어디로-향할것인가">그래서 react는 어디로 향할것인가</h1>
<p>위 프레임워크들의 공통점과 큰 특징을 한번 봅시다.</p>
<ul>
<li>react의 크기를 줄이려고 노력을 한다. -&gt; <strong>virtual-dom을 제거하고 새로운 방식으로 하는 방향</strong> (대부분 5kb 이하를 목표로 한다.)</li>
<li>react의 불필요한 컴포넌트 트리 리렌더링을 최소화한다. -&gt; <strong>state대신에 signal을 이용하는 방향</strong></li>
<li>jsx형식을 이용한다 -&gt; <strong>리액트에서와 유사하게 jsx와 컴포넌트를 이용하는 방향</strong></li>
<li>재개가능성 -&gt; **<code>zero-hydration</code>을 지향하는 방향 (근본적인 TTI문제를 해결)</li>
<li>*</li>
</ul>
<p>아마 다음과 같은 방향으로 흘러갈것 같으며 가깝게는 아직은 <code>react</code>생태계를 유지하며 개선하기위해 <code>preact</code>, <code>millionjs</code>가 다른 프레임워크보다는 실전에 이용될것 같으며 충분히 다른 js 프레임워크들의 생태계가 커진다면 <code>solidjs</code>, <code>qwik</code>으로도 향할 수 있을거라고 생각됩니다.</p>
<p>그 외에도 리액트정도의 안정성을 이 프레임워크들이 가져올 수 있을지, 가벼움을 얻고 <code>virtual-dom</code>을 포기했지만 매우 잦은 리렌더링이 발생하더라도 <code>virtual-dom</code>의 성능에 가깝게 구현을 할 수 있을지가 이 프레임워크들이 더 나아갈 방향성이라고 생각됩니다.</p>
<hr>
<h1 id="⚛-메타-프레임워크는-어떨까">⚛ 메타 프레임워크는 어떨까?</h1>
<p>메타 프레임워크라고 하면 크게 <code>Nextjs</code>, <code>Nuxtjs</code>, <code>Remix</code>정도 많이 이용을 하고 있습니다. 한번 요즘 떠오르는 메타 프레임워크들을 알아보죠.</p>
<h2 id="nextjs13">Nextjs13</h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/5a284d8c-d6b2-4243-be8a-a92a4c397dad/image.png" alt=""></p>
<p>nextjs는 현재 가장 많이 사용하고 있는 프레임워크입니다. 그중에서도 최근에 나온 Nextjs13은 파격적인 <code>서버컴포넌트</code>라는 것을 공개했습니다.</p>
<p>서버 컴포넌트를 이용하면 client의 bundle size를 줄일 수 있으며 기존 <code>ssr</code>, <code>ssg</code>, <code>isr</code>들이 페이지 단위로 처리되던게 컴포넌트 단위로 처리할 수가 있어집니다.</p>
<p>그리고 또한 <code>streming ssr</code>을 지원하기 때문에 <code>Suspense</code>만 이용한다면 <code>streaming ssr</code>도 이용할 수 있죠.</p>
<p>하지만 아직 서버 컴포넌트에 대한 안정성이 확실하게 검증되지 않았다는 점을 제외하면 매우 혁신적인 기술입니다.</p>
<h2 id="astro">Astro</h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/ff250385-57e8-4051-99a9-207646d79237/image.png" alt=""></p>
<p>다음은 <code>astro</code>입니다. <code>Nextjs</code>에 비해서 갑자기 네임밸류가 내려가버렸는데요..^^</p>
<p>그래도 제가 생각하는 Nextjs를 대체할만한 다음 메타 프레임워크라고 생각됩니다. 이 astro의 가장 큰 특징이라고 하면 바로 <code>아일랜드 아키텍처</code>를 이용하고 있는 프레임워크입니다.</p>
<p><code>아일랜드 아키텍처</code>는 페이지가 하나의 거대한 사이트가 아닌, 대체 가능하며 독립적인 덩어리(component)로 분리된 하나의 거대한 바다라는 개념입니다.</p>
<p>이것은 마치 <code>섬</code>처럼 각각의 컴포넌트들이 독립적으로 불러오고 js를 유저의 상호작용에 따라 지연해서 로드하게도 할 수 있습니다. 즉 각 컴포넌트가 하나의 페이지처럼 동작한다고 생각하면 좋습니다. 하나의 <code>페이지</code>가 에러가 나더라도 다른 페이지는 문제없듯이 여기서도 동일하게 한 컴포넌트에서 에러가 나더라도 다른곳에는 영향을 끼치지 않습니다.</p>
<p>저는 직접 이용을 해보지는 않았지만 <a href="https://gmyankee.tistory.com/377">https://gmyankee.tistory.com/377</a> 이 포스팅에서 볼수있듯이 아직은 많은 문제가 있는것으로 보입니다. </p>
<p>하지만 아직 2버전이니 추후에 기능 업데이트 및 안정화가 된다면 충분히 자리잡을 수 있는 프레임워크라고 생각됩니다.</p>
<h1 id="그래서-메타-프레임워크는-어디로-향할것인가">그래서 메타 프레임워크는 어디로 향할것인가</h1>
<ul>
<li>server-component를 이용 -&gt; 번들크기를 줄이며 isr, ssr, ssg를 컴포넌트 단위로 결정하는 방향</li>
<li>streaming ssr -&gt; 페이지 단위에서 ssr을 할때 하나의 api라도 실패하면 페이지 자체가 정지하는 문제를 해결하는 방향</li>
<li>island architecture -&gt; 각 컴포넌트를 독립적으로 운영할 수 있도록하며 각각의 js파일을 줄이며 유저의 상호작용에 따라서 불러오는 방향</li>
</ul>
<p>일단 server-component를 더 발전시키는 방향으로 nextjs의 진화가 될것 같으며 <code>island-architecture</code>는 astro외에도 fresh, marko같은 프레임워크에서도 채택하고 있으며 다음세대의 렌더링 전략으로 떠오를것 같습니다.</p>
<hr>
<h1 id="🍀-css-in-js는-어떨까">🍀 css-in-js는 어떨까?</h1>
<p>css-in-js에도 다양한 라이브러리들이 나오고 있습니다. <code>styled-component</code>, <code>emotion</code>를 메인으로 많이 이용을 하고 있는데요. 동적으로 prop을 넣어 관리하기에 매우 편리하지만 runtime때 style을 삽입해주는 형식이기에 복잡한 스타일 계산이 있을때는 runtime overhead문제가 발생하곤 합니다.</p>
<p>그래서 이 뒤에 다룰 라이브러리들은 이 runtime overhead를 해결하기 위해서 나온 기술들입니다.</p>
<h2 id="linaria">linaria</h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/214ce457-c0b8-4775-9870-908bab65c105/image.png" alt=""></p>
<p>linaria css는 <code>zero-runtime</code>을 지향하고 만들어진 <code>styled-component</code>, <code>emotion</code>의 다음세대 라이브러리입니다. </p>
<p><code>zero-runtime</code>은 빌드 타임에 babel-plugin을 통하여 <code>critical-css</code>를 추출해냅니다. 형태를 보면 예전 React의 React.createElement와 닮은 형태로 파싱을 합니다. 이렇게 초기 로드에 필요한 critical css에 대해서는 빌드타임에 생성읋 하고 초기로드에 필요없는 css들은 <code>collect</code>라는 메서드를 통하여 동적으로 불러올 수 있게 구성되어있습니다.</p>
<p><a href="https://github.com/callstack/linaria/blob/master/docs/CRITICAL_CSS.md">https://github.com/callstack/linaria/blob/master/docs/CRITICAL_CSS.md</a>
<a href="https://github.com/callstack/linaria/blob/master/docs/HOW_IT_WORKS.md">https://github.com/callstack/linaria/blob/master/docs/HOW_IT_WORKS.md</a></p>
<p>linaria는 이것 외에도 생산성에 도움이 되는 다른 여러가지 기능을 제공하며 오늘 소개해드릴 다른 라이브러리 중에서도 가장 인기있게 사용중입니다.</p>
<p>한계라고 하면 webpack,babel같은 도구의 추가설정이 필요하기 때문에 약간의 어려움이 있을 수 있다정도 일것 같습니다.</p>
<h2 id="stitches">stitches</h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/be353704-cd8e-4ffd-94e8-e0ab8df2090f/image.png" alt=""></p>
<p>stitches는 zero-runtime이 아니라 <code>near-zero-runtime</code>을 철학으로 만들어진 라이브러리입니다. 말그대로 zero-runtime은 아니지만 그에 가깝게 빠른 라이브러리입니다.</p>
<p>여기서는 <code>prop interpolation 최적화</code>를 통하여 <code>near-zero-runtime</code>을 구현하였습니다. styled-components, emotion과 같은 runtime라이브러리와 다르게 사전에 정의된 <code>variant</code>를 통해서만 props를 넘기도록 제약을 주어 이 부분을 최적화를 했습니다.</p>
<p>한계라고 한다면 최근에 활발하게 발전하는 <code>vanilla-extract</code>와 다르게 <code>stitches</code>를 관리하는 팀이 많이 존재하지 않고 변화해가는 react (react18 동시성, server component)를 따라갈지도 의문이기 때문에 미래를 본다면 선택하기 어려울 수 도 있을것 같습니다.</p>
<h2 id="vanilla-extract">vanilla-extract</h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/50abdafc-8062-4966-9d8b-e7c3514f64dc/image.png" alt=""></p>
<p>vanilla-extract는 앞서 설명한 linaria와 동일하게 <code>zero-runtime</code> 라이브러리입니다. 최근에 국내 기업에서는 vanilla-extract를 많이 선택하는 경향을 보여주고 있는것 같습니다. 그렇다면 어떠한 이유때문에 vanilla-extract가 선택되는 걸까요?</p>
<p>제가 생각하는 가장 큰 장점은 <code>zero-runtime</code>의 성능을 가져감과 동시에 <code>typescript</code>를 전처리로 사용하는 라이브러리이기 때문에 타입추론이 된다는것과 간단한 번들러 설정으로 이용할 수 있어서가 아닐까라는 생각이 듭니다.</p>
<p>또한 위에서 언급했듯이 <code>vanilla-extract</code>는 활발하게 최신의 기술들과 호환을 맞추기 위해 contributor들이 기여를 하고 있어 미래에도 충분히 유지될만한 라이브러리이기 때문에 선택할만하지 않을까라고 생각됩니다.</p>
<h1 id="그래서-css-in-js는-어디로-향할것인가">그래서 css-in-js는 어디로 향할것인가</h1>
<p>음.. 사실 css-in-js는 아직 styled-component, emotion이 점령하고 있기에 크게 이 시장이 변화할것이라는 생각은 들지 않습니다만, runtime overhead가 직접적으로 느껴지는 서비스라면 위에서 언급한 라이브러리를 이용하는것도 좋다고 생각됩니다.</p>
<p>그 중에서는 <strong><code>vanilla-extract</code></strong>가 현재 많은 기업, 사람들이 이용하고 있고 최신 기술에도 맞춰져있기때문에 미래에 좀 더 떠오를거라고 생각됩니다.</p>
<p>미래에는 변화하는 기술에 대해 호환을 잘해주는 라이브러리가 무조건 우세에 있지 않을까 생각됩니다. 예를들어 최근에 나온 서버 컴포넌트를 지원하는 라이브러리가 아직 많이 없는데 서버 컴포넌트를 지원해주는 <strong><code>panda-css</code></strong>가 떠오르는것 처럼 말이죠. </p>
<p>물론 이것은 추측하는 <code>css-in-js</code>의 방향이고 저는 개인적으로 <code>scss</code>, <code>module css</code>를 이용하는 방향으로 돌아가야 되지 않나 생각이 듭니다..</p>
<p><code>css-in-js</code> 자체가 가지는 runtime overhead도 존재하지 않고 무엇보다 새로 업데이트될 기술에 대해서 scss는 동작이 안될 가능성이 거의 없기때문입니다. </p>
<p>이번에 서버 컴포넌트로 프로젝트를 진행하며 <code>scss</code>, <code>module css</code>를 이용하여 진행하였는데 <code>css-in-js</code>에 비해서는 약간의 생산성이 떨어진다고는 느꼇지만 서버 컴포넌트를 이용하며 크게 신경을 써야될 부분이 없어서 사용 경험이 나쁘지않았습니다.</p>
<p>그래서 <strong>튜닝의 끝은 순정이다</strong> 라는 말이 있듯 언젠가는 scss로 돌아오지 않을까라는 생각이 듭니다. 🐳</p>
<hr>
<h1 id="📦-모듈-번들러는-어떨까">📦 모듈 번들러는 어떨까?</h1>
<p>원래는 <code>webpack</code>의 독점 시장이었던 모듈번들러 시장이 확실히 변하고 있다는게 느껴집니다. 한번 어떤 모듈번들러들이 webpack을 위협하는지 알아봅시다.</p>
<h2 id="vite">vite</h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/7a192cdf-d31c-4e3f-bc6e-60f10c013295/image.png" alt=""></p>
<p>저는 보통 일반적인 개발환경을 구축할때는 <strong><code>webpack</code></strong>, 라이브러리 개발을 할때는 원활한 <code>tree-shaking</code>을 위하여 <strong><code>rollup</code></strong> 이렇게 2가지 도구를 크게 이용하고 있었습니다. </p>
<p>하지만 현재는 그 2개의 상황을 <code>vite</code>라는 도구 하나가 모두 해결해줄 수 있습니다. </p>
<p>당시에는 부족했던 플러그인이 현재는 webpack을 대체할 정도로 충분한 플러그인이 나오게 되었으며, <code>rollup plugin</code>을 이용할 수 있는 번들러이기때문에 <code>treeshaking</code> 처리까지 가능하게 되었습니다.</p>
<p>아마 미래에 가장 많이 사용할 모듈 번들러에 가장 근접한 녀석이 아닌가 생각됩니다. 😀</p>
<h2 id="esbuild">esbuild</h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/da60a1f0-eea8-4afc-b38f-23c1aa2a83cd/image.png" alt=""></p>
<p>go로 만들어져 있어서 webpack보다 100배 빠르다고 하여 유명했던 <code>esbuild</code>입니다.</p>
<p>하지만 아직은 다른 도구에 비하면 없는 기능들이 많고 1.0 버전이 나오지 않은 번들러 이기때문에 <code>esbuild-loader</code>만 떼서 다른 모듈번들러에서 <strong>컴파일러</strong>로만 이용하는 경우가 많습니다.</p>
<p>하지만 1.0버전이 나오고 기능들이 많이 보완된 <code>esbuild</code>는 어떨지 또 모르겠네요..🙃</p>
<h2 id="turbopack">turbopack</h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/1a03fbec-dae9-433e-b1b0-0753629d1036/image.png" alt=""></p>
<p>vercel에서 만든 <code>rust</code>기반의 모듈 번들러입니다. 기존 vercel에서 이용하던 swc 컴파일러를 진화시켜 <code>turbopack</code>이라는 모듈번들러까지 완성시킨것 같네요.</p>
<p>무려 webpack보다 700배가 빠르며, 그 vite보다도 10배나 빠르다는 벤치마킹으로 유명해졌습니다. (물론 실제로 10배까지는 아니라는 반박이 있었습니다.)
그리고 <strong><code>webpack</code></strong>의 후속작이기 때문에 <code>vue</code>를 목적으로 만들어진 <code>vite</code>보다 좀 더 react 개발환경에 잘 맞으며 마이그레이션에 더 용이하지 않을까라는 점만으로도 많은 흥미를 끌 수 있었습니다.</p>
<p>아직 기능은 크게 많지 않기 때문에 속도면만 보자면 incremental build, caching을 이용하여 빠르고 vercel의 원격캐싱을 이용하여 팀원들이 공유하는 캐싱을 이용할 수 있다고 합니다. </p>
<p>마치 vercel에서 만든 <code>turborepo</code>를 보는것 같습니다. (incremental build, caching)
<img src="https://velog.velcdn.com/images/baby_dev/post/c755abd4-12f8-4285-8721-4f22b12f78ab/image.png" alt=""></p>
<p>현재 Nextjs13.4 버전기준으로 실험적으로 turbopack(beta)로 개발서버를 이용할 수 있도록 해놓은 상태여서 turbopack을 이용해볼 수 있습니다.</p>
<p>하지만 아직은 안정적인 모듈번들러가 되기에는 약간의 시간이 더 필요할 것 같습니다. <a href="https://turbo.build/pack/docs">https://turbo.build/pack/docs</a></p>
<h1 id="그래서-모듈번들러는-어디로-향할것인가">그래서 모듈번들러는 어디로 향할것인가?</h1>
<p>근래에는 vite가 꽤 우세를 보여줄것 같습니다. <code>create-react-app</code>, <code>storybook</code>에 마저 기본 번들러를 vite로 지원해줄 정도로 이미 webpack의 자리를 vite가 많이 가져가고 있습니다.</p>
<p>빠르게 생태계를 확립한 만큼 다른 도구들보다도 많은 플러그인, 기능들로 시장을 선점할것같습니다.</p>
<p>하지만 좀 더 미래를 보면 turbopack이 많이 사용되지 않을까 생각됩니다. <strong>vite보다 빠른속도, webpack의 후속 모듈번들러, turborepo에서 이용하던 vercel원격 캐싱</strong>까지 존재하기에 안정화만 된다면 사용하지 않을 이유가 없을것 같습니다.</p>
<p>또한 vercel에서 만든 모듈번들러이기때문에 현재 많이 이용하는 <code>nextjs</code>와의 지원도 잘 될수밖에 없기때문이죠.🎉</p>
<h1 id="🛠-나머지-기타도구들">🛠 나머지 기타도구들..</h1>
<p>간단하게만 알아볼 예정입니다. 주로 javascript로 compile하는 도구들을 rust와 같이 저레벨언어로 포팅하는 라이브러리들입니다.</p>
<p>앞에 <code>typescript</code>와 <code>eslint</code>에 대해서 적었는데 이부분은 요즘 어떤 흐름으로 흘러가는지만 알아보도록 하겠습니다.</p>
<h2 id="stc">stc</h2>
<p>typescript는 자체적으로 대체할 수 있지 않을것 같지 않습니다. 하지만 typescript를 빌드하는 tsc는 다르죠.</p>
<p>현재 swc를 개발하신 분이 <a href="https://github.com/dudykr/stc">https://github.com/dudykr/stc</a> <code>speedy typescript type checker</code>라는 stc 컴파일러를 오픈소스로 개발중입니다.</p>
<p>아직은 개발단계이기 때문에 tsc를 대체하기까지는 한참걸리겠지만.. 이용할 수 있을 수준이 되면 stc가 떠오르지 않을까 생각됩니다.</p>
<hr>
<h2 id="rome">rome</h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/abff8fa7-6a81-4961-92d3-4389dd00931e/image.png" alt=""></p>
<p>rome은 간단하게 말하자면 javascript개발에 필요한 <code>compile</code> 부분을 Rust로 포맷팅하며 all-in-one으로 넣은 패키지입니다.</p>
<p>현재는 <code>eslint</code>, <code>prettier</code>부분 정도만 rust로 지원을 하고 있는것 같은데 추후에는 <code>bundler</code>, <code>jest</code> 까지 rome으로 모두 합칠 계획인것 같습니다.</p>
<p>이것은 현재 시점으로는 <code>eslint</code>, <code>prettier</code>에 적용해볼만 하지 않을까 생각이 듭니다. (vscode extension도 존재합니다.)</p>
<h1 id="결론">결론</h1>
<p>글을 약 7월쯤에 작성하다가 일이 바빠 잠시 두고 있었는데 그새 또 기술들이 변했습니다.. (Next13.5, svelte5가 나오고 특히 bun 1.0이 나왔더라고요.)</p>
<p>글 초반에 6개월만에 기술이 변한다라고 작성했는데 6개월도 길다라는 생각이 드네요. </p>
<p>전체적으로 보면 <strong>실제 운영에 쓰일 라이브러리</strong>들은 생산성은 충분히 다 갖춰서 현재 가장 주로 쓰이는 라이브러리의 <strong>코드 스타일을 따라가며 성능을 향상</strong>시키는 방향으로 많이 가는것 같습니다. <strong>(react의 jsx를 유지하며 용량은 줄이려는 웹 프레임워크들, css-in-js 방식을 유지하며 zero-runtime 쪽으로 가는 css 프레임워크)</strong> 물론 <code>game changer</code>급이 나오기도 하지만요. (Nextjs Server Component, Island Architechure)</p>
<p>그리고 <strong>개발에 도움될 도구</strong>들은 기존 javascript로 되어있는 compiler를 rust로 전환하는 추세인것 같습니다. <strong>(webpack -&gt; turbopack, eslint -&gt; rome, tsc -&gt; stc...)</strong></p>
<p>그렇기 때문에 실제 기술이 변경되어도 크게 우리의 코드짜는 방식이 뒤엎어지거나 그런일은 크게 없을것 같습니다. 굳이 여기 존재하는 기술들을 쫓아가기 위해서 공부하기 보다는 현재 하는 기술에 집중하면 좋을것 같습니다. (react에서 solidjs로 변경되더라도 코딩 스타일은 비슷할거기 때문이죠.)</p>
<blockquote>
<p>매우 주관적인 생각으로 작성한 글입니다. 또한 작성한 모든 기술을 전부 직접 이용해본것은 아니기 때문에 약간의 오류가 있을 수 있습니당.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js 13] Next.js 13 with react query]]></title>
            <link>https://velog.io/@baby_dev/Next13-with-react-query</link>
            <guid>https://velog.io/@baby_dev/Next13-with-react-query</guid>
            <pubDate>Mon, 26 Jun 2023 16:59:27 GMT</pubDate>
            <description><![CDATA[<h1 id="react-query와-서버-컴포넌트">react-query와 서버 컴포넌트</h1>
<p>제목을 nextjs13과 react-query라고 하였으나 사실 nextjs12에서도 이용하던 방법입니다. </p>
<p><code>initialData</code>, <code>hydrate</code>를 이용한 방법이죠. react-query (현 @tanstack/react-query)의 공식문서에도 있는 react query와 nextjs13의 app router 사용을 함께 보시죠.</p>
<h2 id="react-query를-사용할-일이">react-query를 사용할 일이..?</h2>
<p>nextjs13의 공식문서를 보면 data fetching은 server component, interativge한 작업은 client component를 이용하라고 친절하게 ✅, ❌ 로 구분도 해주었습니다.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/8bd84d66-2957-4320-9143-4ab9aa53ab46/image.png" alt=""></p>
<p>그렇지만 실제 개발을 하다보면 서버 컴포넌트에서만 data fetching을 하는것은 이상적인 상황에 불가합니다. </p>
<p>client 상태를 기반으로 fetch를 해야할때는 client component에서 fetch를 해주어야하죠. 대표적인 예시로 무한스크롤이 있습니다. </p>
<p>page가 늘어날때마다 api의 page param을 하나씩 늘려가면서 호출을 해야하는데 server component에서는 page라는 상태를 참조할 수 없기때문에 client에서 fetch를 하는것이 강제됩니다.</p>
<h2 id="react-query의-hydrate를-사용하는-이유">react-query의 hydrate를 사용하는 이유</h2>
<p>그러면 그냥 client side에서 fetch하면 되는거 아닌가요? 맞습니다. </p>
<p>하지만 나머지 부분이 server component로 구현하고 무한 스크롤 부분만 client component에서 렌더링을 하게된다면 나머지는 모두 고정되어있지만 혼자 매우 깜빡이며 매우 늦게 불러오는것 처럼 보이게 됩니다.</p>
<p>사실은 다른 요소들이 너무 빠른것 뿐인데 말이죠.</p>
<p>그래서 client side에서 fetch를 하는데 첫 데이터에 대해서 이미 그려진 html처럼 보여지게 하기위해서 (ssr) react-query에서는 <code>prefetch</code>와 <code>hydrate</code>라는것을 제공해주었습니다.</p>
<h2 id="react-query-with-nextjs13">react-query with nextjs13</h2>
<p>이제 react-query와 nextjs13이 어떻게 같이 사용되는지 볼 것입니다.</p>
<p>먼저 queryClient를 초기화해주는 함수를 만듭니다.</p>
<pre><code class="language-jsx">// app/getQueryClient.jsx
import { QueryClient } from &#39;@tanstack/react-query&#39;;
import { cache } from &#39;react&#39;;

export const getQueryClient = cache(() =&gt; new QueryClient());</code></pre>
<p>server component에서는 useQueryClient 같은 훅을 이용할 수 없으니 다음과 같이 custom하게 만들어줘야 합니다.</p>
<pre><code class="language-jsx">// server component
export const HydratedReservationItemList = async () =&gt; {
  const queryClient = getQueryClient();
  await queryClient.prefetchInfiniteQuery(
    [&#39;reservation&#39;, {sortType: D}],
    () =&gt; getReservationList(&#39;R&#39;, false),
  );

  const dehydratedState = dehydrate(queryClient);

  return (
    &lt;Hydrate state={dehydratedState}&gt;
      &lt;ReservationItemList /&gt;
    &lt;/Hydrate&gt;
  );
};</code></pre>
<p>이렇게 server component에 <code>queryClient</code>를 초기화 시킨후 <code>prefetchQuery</code> or <code>prefetchInfiniteQuery(페이지네이션 or 무한스크롤)</code>을 이용합니다.</p>
<p>그후 <code>dehydrate</code>, <code>Hydrate component</code>를 이용하면 <strong><code>[&#39;reservation&#39;, {sortType: D}]</code></strong> 이 key의 초기값이 캐싱이 됩니다.</p>
<p>그래서 보통 api call을 하면 loading할때 undefined상태였다가 api가 성공하면 data를 가지고 있는 문제를 처음부터 undefined 상태가 없이 data를 초기화시킨 상태로 이용할 수 있습니다.</p>
<h2 id="약간의-한계">약간의 한계..?</h2>
<p>react-query를 이용할때 대부분 상태가 변경될때마다 refetch를 위하여 query key를 이용하는데요. </p>
<p>하지만 <code>Hydrate</code>는 queryKey가 일치할떄만 동작을 합니다. 말그대로 그 query key에 cache를 해두는것이죠.</p>
<p>그렇기때문에 이러한 queryKey를 맞춰주기위하여 처음값을 하드코딩으로 설정할 필요가 있습니다. <code>[&#39;reservation&#39;, {sortType: D, type: &#39;all&#39;}]</code> 이런 방식으로 초기값을 설정을 해야 합니다.</p>
<p>그렇지만 만약 초기에 들어갈 수 있는 잠재적인 값이 여러개라면 어떨까요? 예를들어, 현재 <code>type: &#39;all&#39;</code>로 되어있는것에 10개의 타입으로 입장을 할수있다면? <code>ex) https://example.com?category=all, https://example.com?category=hotel...</code></p>
<p>이런경우에는 queryString을 통해서 여러가지 상태로 진입을 할 수 있는데 처음에 <code>type: all</code>로만 prefetch를 해둔다면 <code>type: hotel</code>, <code>type: air</code> 이런 값으로 들어올때는 prefetch가 되어있지 않기때문에 동일하게 loading을 하다가 ui가 보이게 될것입니다.</p>
<p>물론 2,3개 정도의 작은 진입점이라면 그것들도 모두 prefetch해주면 됩니다. 간단하게 prefetchQuery를 2번, 3번 이용하면 되는것이죠.
하지만 10개, 20개 이렇게 된다면 물론 prefetchQuery를 10번, 20번으로 <code>가능</code>은 하겠지만 그렇다면 사용자가 새로고침할때마다 불필요한 api호출이 10개, 20개나 더 발생한다는것이죠.</p>
<p>이는 사용자가 많아지면 많아질수록 nextjs server에 더 많은 부담을 줄것이며 오히려 csr을 이용하는것이 나은 속도가 나올수도 있습니다.</p>
<hr>
<p>이부분은 특정 진입점들은 사용자들이 많이 접근하겠다 하는것과 그 진입점의 갯수에 대해서 어디까지 prefetch를 이용할까는 스스로 고민하여 이용하시면 될것 같습니다. </p>
<p>최적화라는것에는 항상 트레이드오프가 따라온다는것만 명심하고 이용하면 큰 문제없이 이용할 수 있을것 같습니다.</p>
<h2 id="결론">결론</h2>
<p>사실 사용하는 방법을 제외하면 next13이전 버전에도 동일하게 해당하는 내용들입니다. ssr인데 react-query를 이용하실분들은 다음과 같은 방법을 해보시고, prefetch를 어디까지 할지는 직접 고민하여 설정하시는 것을 추천합니다.</p>
<p>이것외에도 react-query를 이용하신다면 <code>optimistic update</code>라는 기술도 알아보시는 것을 추천합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js 13] eslint-plugin-server-component-rules 개발기]]></title>
            <link>https://velog.io/@baby_dev/eslint-plugin-server-component-rules-%EA%B0%9C%EB%B0%9C%EA%B8%B0</link>
            <guid>https://velog.io/@baby_dev/eslint-plugin-server-component-rules-%EA%B0%9C%EB%B0%9C%EA%B8%B0</guid>
            <pubDate>Sun, 25 Jun 2023 12:37:15 GMT</pubDate>
            <description><![CDATA[<h1 id="server-component-개발을-하며">server-component 개발을 하며..</h1>
<p>저는 현재 Nextjs13의 server component를 활용하여 개발을 하고 있습니다.</p>
<p>그러던 중 하나의 불편함을 느꼇습니다. <code>server component</code> 에서 여러가지 제약이 있는데 그 제약을 확인하기가 약간의 불편함이 있었습니다.</p>
<p>개발을 하며 <code>빌드</code>를 해야 그 제약을 확인할 수 있었고 제약중 하나인 <code>client component에서 server component를 import하지 못하는것</code>은 확인하지 못하였습니다.</p>
<p>그래서 이러한 제약을 개발하며 <code>에디터 (vscode)</code> 단에서 확인하면서 개발하면 좋지 않을까? 라는 생각으로 <code>eslint-plugin</code>을 이용했습니다.</p>
<h2 id="eslint-custom-rule">eslint-custom-rule</h2>
<p><a href="https://eslint.org/docs/latest/extend/plugins">https://eslint.org/docs/latest/extend/plugins</a></p>
<p>eslint의 공식문서에는 custom rule을 만들때 굉장히 잘 나와있습니다.</p>
<p>일단 eslint는 코드를 ast tree 형태로 변경을 하여 파일, 함수, 변수, jsx, 토큰 단위들로 확인을 할 수 있습니다.</p>
<p><a href="https://astexplorer.net/">https://astexplorer.net/</a> 이 사이트를 사용하면 코드를 ast tree로 변경했을때 어떠한 결과값이 나오는지를 확인할 수 있습니다.</p>
<hr>
<p>eslint의 custom rule을 만들게 될때의 지켜야 될것이 있습니다. </p>
<ul>
<li>package이름은 무조건 eslint-plugin으로 시작을 해야합니다.</li>
<li>사용하는 측에서는 eslint-plugin-example 이렇게 설치한다면 eslintrc.js 부분의 plugins 부분에 <code>[&#39;example&#39;]</code> 식으로 추가하면 됩니디.</li>
<li>배포한 rules들을 사용하는측의 rules에서 이용하게 되며 여기서는 <code>{plugin}/{rules}</code> 형식으로 들어가게 됩니다.<pre><code class="language-js">plugins: [&#39;example&#39;],
rules: {
  &quot;example/test-rule&quot;, [&quot;warn&quot;] // warn, error, off
},</code></pre>
</li>
</ul>
<hr>
<p><code>eslint-custom-rule</code>을 만들때는 다음과 같은 형태로 만들어 줘야합니다. rule에는 <code>meta정보</code>를 담는 <code>meta</code> 프로퍼티와 룰을 만들 수 있는 <code>create</code> 프로퍼티를 이용하여 커스텀 룰을 만들수 있습니다.</p>
<pre><code class="language-jsx">export const customRule = {
  meta: {
    type: &#39;problem&#39;, 
    fixable: true,
    docs: {
      url: &#39;...&#39;,
    },
  },
  create: (context) =&gt; {
    return {
      Program: function (node) {
         context.report({
          node,
          message: `에러에러`,
        });
      }
    }
  }</code></pre>
<p>그리고 module.exports의 rules에 <code>test-rule</code>을 key로 하면 위의 예시처럼 <code>example/test-rule</code> 로 사용할 수 있습니다.</p>
<pre><code class="language-jsx">module.exports = {
  rules: {
    &#39;test-rule&#39;: {...customRule}
  },
};</code></pre>
<hr>
<p><code>create</code>안에서는 <code>https://astexplorer.net/</code> 에서 나오는 파란색 부분을 <code>key</code>로 이용할 수 있습니다. 예를 들어서 <code>Program</code>, <code>JSXElement</code>, <code>ExportNamedDeclaration</code> 같은것 들이죠.</p>
<p>이것들은 eslint에서 selector이라고 부르며 단순 참조하는것뿐만 아니라 내부에서 로직적인 처리, <code>onCodePathStart</code>같은 이벤트핸들러를 key로 둘수도 있습니다.</p>
<p><a href="https://eslint.org/docs/latest/extend/selectors">https://eslint.org/docs/latest/extend/selectors</a></p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/74a6ab26-8b7a-4d65-8391-13134e9aa7d9/image.png" alt=""></p>
<h2 id="context-node-그리고-key">context, node 그리고 key</h2>
<p>context객체에는 source코드를 가져오는 기능, 파일네임을 가져오는 기능, 실제 에러를 보여주도록 하는 기능등 custom rule을 만들때 필요한 유틸적인 요소들을 포함한 객체입니다.</p>
<p>context객체는 처음의 <code>create</code> 의 매개변수 에서 가져올 수 있습니다.</p>
<p><a href="https://eslint.org/docs/latest/extend/custom-rules#the-context-object">https://eslint.org/docs/latest/extend/custom-rules#the-context-object</a>
<a href="https://eslint.org/docs/latest/extend/code-path-analysis">https://eslint.org/docs/latest/extend/code-path-analysis</a></p>
<hr>
<p>*<em>node객체는 context객체와는 다르게 <code>create</code>의 매개변수가 아닌 그 하위인 <code>Program</code>, <code>JSXElement</code>등의 selector들의 매개변수로 참조를 할수있습니다.
*</em>
이 node객체에는 그 selector에 해당하는 정보가 담기게 됩니다. </p>
<p>예를 들어 <code>Program</code>같은 경우 하나의 파일단위로 탐색을 하는것이기때문에 <code>context.getSourceCode(node)</code>를 하게되면 그 파일의 소스코드가 모두 나오게됩니다.</p>
<p>추가적으로 <code>JSXElement</code>의 node객체를 이용하여 <code>context.getSourceCode(node)</code>를 하게되면 <code>&lt;div&gt;테스트&lt;/div&gt;</code> 같은 Jsx요소를 결과로 받을 수 있습니다.</p>
<h2 id="server-components-rule만들기">server components rule만들기</h2>
<p>이제 이러한 커스텀 룰을 이용하여 <code>server component rule</code> 을 만들때입니다. </p>
<h3 id="서버-컴포넌트-판별하기">서버 컴포넌트 판별하기</h3>
<pre><code class="language-jsx">    create: function (node) {
      let isServerComponent = true;

      Program: function (node) {
        const sourceCode = context.getSourceCode().getText(node);
        const extension = filename.substring(filename.lastIndexOf(&#39;.&#39;) + 1);

        if (extension === &#39;tsx&#39; || extension === &#39;jsx&#39;) {
          if (sourceCode.includes(&#39;use client&#39;)) {
            isServerComponent = false;
          }
        } else {
          isServerComponent = false;
        }
      }
    }</code></pre>
<p>먼저 <code>let isServerComponent = true</code> 를 선언하여 <code>Program</code> selector에서 서버 컴포넌트를 판별합니다.</p>
<p>확장자가 <code>tsx</code>, <code>jsx</code>인것중에 파일에 <code>&#39;use client&#39;</code>가 포함되어있는것을 client component로 판별을 하도록 했습니다.</p>
<h3 id="custom-hook-판별하기">custom hook 판별하기</h3>
<pre><code class="language-jsx">create: function (node) {
   ExportNamedDeclaration: function (node) {
      if (node.declaration?.type === &#39;VariableDeclaration&#39;) {
        if (node.declaration.declarations[0].id.name.match(/^use[A-Z]/)) {
          isCustomHook = true;
        }
      }

      if (node.declaration?.type === &#39;FunctionDeclaration&#39;) {
         if (node.declaration.id.name.match(/^use[A-Z]/)) {
          isCustomHook = true;
         }
      }
  },

  ExportDefaultDeclaration: function (node) {
      if (node.declaration?.type === &#39;Identifier&#39;) {
          if (node.declaration.name.match(/^use[A-Z]/)) {
            isCustomHook = true;
          }
      }

      if (node.declaration?.type === &#39;FunctionDeclaration&#39;) {
          if (node.declaration.id.name.match(/^use[A-Z]/)) {
            isCustomHook = true;
          }
      }
   },
}</code></pre>
<p><code>ExportNamedDeclaration</code>, <code>ExportDefaultDeclaration</code>를 이용하여 혹시 <code>export named</code>, <code>export default</code>로 내보내는 이름을 파악한 다음에 그 이름이 <code>use</code>로 시작한다면 customHook으로 판별하도록 했습니다.</p>
<p>이 2개의 조합으로 custom hook은 아니면서 server component인것들을 판별하여 custom rule을 적용시킬 예정입니다.</p>
<h2 id="⭐️-server-component-rulesfile-name">⭐️ server-component-rules/file-name</h2>
<pre><code class="language-jsx">Program: function (node) {
  const sourceCode = context.getSourceCode().getText(node);
  const filename = context.getFilename();
  const extension = filename.substring(filename.lastIndexOf(&#39;.&#39;) + 1);

  // 다른곳에서 판별한 server component처리 로직
  if (isServerComponent &amp;&amp; !isCustomHook) {
    const fileName = context.getFilename();
    const { options } = context;
    const option = options.find((opt) =&gt; &#39;middle&#39; in opt);
    const middle = option.middle;

    if (fileName.includes(&#39;tsx&#39;) &amp;&amp; !fileName.endsWith(`index.${middle}.tsx`)) {
      const suggestedFileName = fileName.replace(/\.tsx$/, `.${middle}.tsx`);

      context.report({
        node,
        message: `server component&#39;s file name should be &#39;${suggestedFileName}&#39;`,
      });
    }
  }
},</code></pre>
<p>server component에 대한 file name을 제한하는 룰입니다. server component에 file name convention을 지정하는룰입니다. </p>
<p><code>index.${middle}.tsx</code>를 convention으로 하여서 client component와의 확실한 구분을 하도록 하였고, 이는 나중에 <code>no-import-use-client</code> rule에 이용과 함께 이용이 됩니다.</p>
<pre><code class="language-js">  plugins: [&#39;server-component-rules&#39;],
  rules: {
    &quot;server-component-rules/file-name&quot;, [&quot;error&quot;, {middle: &#39;server&#39;}] // warn, error, off
  },</code></pre>
<p>또한 사용하는 측에서 배열의 두번째 요소의 옵션에 <code>{middle: {name}}</code> 과 같은 식으로 이용을 하면 됩니다. 위의 예시로는 서버컴포넌트의 컨벤션은 <code>index.server.tsx</code>이 됩니다.</p>
<h2 id="⭐️-server-component-rulesno-import-use-client">⭐️ server-component-rules/no-import-use-client</h2>
<pre><code class="language-jsx">ImportDeclaration: function (node) {
  if (isServerComponent || isRouteHandler) {
    return;
  }

  if (isCustomHook) {
    return;
  }

  const importedComponent = node.source.value;
  const importSourceCode = context.getSourceCode().getText(node);

  if (!importSourceCode.includes(&#39;/&#39;) || !importSourceCode.includes(&#39;from&#39;)) {
    // 외부 라이브러리들 import를 제외하는것입니다. ex) import React from &#39;react&#39;;
    return;
  }

  const { options } = context;
  const option = options.find((opt) =&gt; &#39;middle&#39; in opt);
  const middle = option.middle;

  if (importSourceCode.split(&#39;from&#39;)[1].split(&#39;/&#39;).at(-1).includes(middle)) {
    context.report({
      node,
      message: `Can&#39;t import server component in client component (${importedComponent})`,
    });
  }
},</code></pre>
<p><strong>client component에서는 server component를 import하지 못한다라는 룰</strong>입니다. </p>
<p><code>ImportDeclaration</code> selector를 이용하여 모든 import 선언문을 가져오고 그 import 선언문에서 <code>from</code> 뒤에 있는 확장자를 가져와서 <code>server component</code> 인지 판별하고 에러를 뱉게 하는 기능입니다.</p>
<p>현재로서는 import한 컴포넌트가 서버 컴포넌트인지 판단하는 방법이 file naming 밖에 없어서 위의 <code>server-component-rules/file-name</code> 룰과 함께 이용을 하여 제어할 수 있습니다.</p>
<h2 id="⭐️-server-component-rulesno-use-event-handler">⭐️ server-component-rules/no-use-event-handler</h2>
<pre><code class="language-jsx">JSXAttribute: function (node) {
    const attributeName = node.name.name;

    if (attributeName.startsWith(&#39;on&#39;) &amp;&amp; isServerComponent &amp;&amp; !isCustomHook) {
      context.report({
        node,
        message: `Can&#39;t use ${attributeName} in server component`,
      });
    }
  },
};</code></pre>
<p>다음은 <strong>server component에서 이벤트 핸들러를 사용하지못하는 룰</strong>입니다. </p>
<p><code>JSXAttribute</code>를 이용하면 JSXElement에 있는 attribute들을 참조할 수 있습니다. 여기서 <code>node.name.name</code>을 참조하면 그 attribute의 key값을 가져올 수 있는데요.</p>
<p>이렇게 그 attribute를 가져온 후 앞에 <code>on</code>으로 시작하는 속성이면 에러를 뱉도록 처리하였습니다.</p>
<h2 id="⭐️-server-component-rulesno-use-browser-api">⭐️ server-component-rules/no-use-browser-api</h2>
<pre><code class="language-jsx">Identifier: function (node) {
  const { name } = node;

  if (!isServerComponent) {
    return;
  }

  if (isCustomHook) {
    return;
  }

  if (name === &#39;document&#39; || name === &#39;window&#39;) {
    context.report({
      node,
      message: `Do not use browser APIs such as &#39;${name}&#39; in server component`,
    });
  }
},</code></pre>
<p>다음은 <strong>서버 컴포넌트에서 window나 document같은 브라우저의 객체를 참조하지 못하는 룰</strong>입니다.</p>
<p><code>Identifier</code> selector를 이용하여 모든 식별자들을 받아오고 그 식별자의 이름이 document, window인 것에 에러를 뱉도록 처리하였습니다.</p>
<p>하지만 window객체같은 경우 내부 메서드들을 바로 참조해도 문제가 없는데요.
현재 이 window객체의 내부 메서드까지 판단할 수는 없는 문제가 있습니다.</p>
<h2 id="⭐️-server-component-rulesno-use-custom-hook">⭐️ server-component-rules/no-use-custom-hook</h2>
<pre><code class="language-jsx">CallExpression: function (node) {
  if (node.callee.type === &#39;Identifier&#39;) {
    const { name } = node.callee;

    if (name.match(/^use[A-Z]/) &amp;&amp; isServerComponent &amp;&amp; !isCustomHook) {
      context.report({
        node,
        message: `Do not use ${name} hook inside JSX files in server component`,
      });
    }
  }
},</code></pre>
<p>마지막으로 <strong>서버 컴포넌트에서 hook을 사용하지 못하는 룰</strong>입니다.</p>
<p><code>CallExpression</code> selector를 이용하면 호출한 함수들을 다 받아올 수 있습니다. custom hook도 함수이기 때문에 여기에 포함이 됩니다.</p>
<p>그런 다음 그 식별자(함수)의 이름을 가져온뒤 시작이 use로 시작하며 그 다음에 대문자가 오는 camel케이스인지 파악을 하도록 했습니다.</p>
<blockquote>
<p>startsWith(&#39;use&#39;)로 처리를 하니 user... 이라는 함수도 가져와지는 문제가 있더라고요. 🌧</p>
</blockquote>
<h2 id="ending">Ending</h2>
<p>이걸 만들고 난후 server component에서 가끔씩 하는 실수들을 코드를 작성하면서 바로 처리하여 생산성이 올라가고 기존 잡을 수 없는 <code>client component에서 server component를 Import 못하는 룰</code>을 린트시에 에러가 나올수 있도록 하여 예상치 못한 런타임 문제를 해결하였습니다.</p>
<p>이러한 rule 제한 외에도 팀에서 특정 컨벤션을 문서화로만 관리하지말고 이렇게 커스텀 룰을 만들어서 제한하는 방법도 좋다고 생각합니다. </p>
<hr>
<p><a href="https://github.com/hwangstar156/eslint-plugin-server-component-rules">https://github.com/hwangstar156/eslint-plugin-server-component-rules</a></p>
<p>실제 소스코드는 여기서 확인할 수 있습니다.<img src="https://velog.velcdn.com/images/baby_dev/post/7486a104-42ed-4abe-ad43-51021beac4af/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Server Component 실전에서 써먹기 (2편 - 서버컴포넌트 with 에러바운더리)]]></title>
            <link>https://velog.io/@baby_dev/Server-Component-%EC%8B%A4%EC%A0%84%EC%97%90%EC%84%9C-%EC%8D%A8%EB%A8%B9%EA%B8%B0-2%ED%8E%B8</link>
            <guid>https://velog.io/@baby_dev/Server-Component-%EC%8B%A4%EC%A0%84%EC%97%90%EC%84%9C-%EC%8D%A8%EB%A8%B9%EA%B8%B0-2%ED%8E%B8</guid>
            <pubDate>Wed, 07 Jun 2023 11:16:23 GMT</pubDate>
            <description><![CDATA[<p>1편에서 다 작성하기에는 Server Component의 에러 처리부분이 길어 질것 같아 2편으로 옮겼습니다.</p>
<p>에러처리에서는 단순히 어떻게 하는지 최종 과정만이 있는것이 아니라 저의 삽질 과정이 포함되어있는 글입니다 ㅎㅎ</p>
<h1 id="서버-컴포넌트-에러-처리">서버 컴포넌트 에러 처리</h1>
<h2 id="서버-컴포넌트에서-에러가-난다면">서버 컴포넌트에서 에러가 난다면?</h2>
<p>만약 서버 컴포넌트에서 에러가 발생하면 어떻게 동작할까요? 과연 우리는 에러를 잡을 수 있을까요? 아쉽지만 저희는 서버 컴포넌트에서 발생하는 에러를 잡아서 클라이언트에서의 특정 동작을 할 수 없습니다.</p>
<hr>
<p>말그대로 서버에서 발생하기 때문에 에러가 발생하면 아예 빌드가 터져버릴것입니다.</p>
<p>그렇기 때문에 서버 컴포넌트를 부르는 부분에서 <code>try/catch</code>를 통하여 잡아주는것이 필요합니다.</p>
<pre><code class="language-jsx">export const getListData = async () =&gt; {
  try {
    const response = await fetch(`${HOME_API.API_GET_LIST}`);
    const serverData = await response.json();

    return serverData;
  } catch (e) {
    if (e instanceof Error) {
      return [];
    }
  }
};</code></pre>
<p>이렇게 작성을 하면 서버에서 에러가 나오면 빈 배열을 return을 하여 실제 화면에는 이 list가 아무것도 안나오는 것 처럼 보이게 되겠죠.</p>
<p>하지만 <code>ErrorBoundary</code> 에서 특정 client fallback을 보여주고 싶을 때 서버 컴포넌트에서 처리할 수 있을까요? 당연히 할 수 없습니다. 저희가 할 수 있는 최선은 에러가 터지지 않게 <code>빈 배열</code>로 내보내는것이 다 이기 때문이죠.</p>
<h2 id="첫번째-도전---빈배열인지-확인하여-처리">첫번째 도전 - 빈배열인지 확인하여 처리</h2>
<p>처음에는 야매로 도전을 했습니다. server component의 데이터를 받아와서 읽어오는 <code>ul tag</code>의 자식요소 갯수를 세어서 0개이면 그것을 에러로 판단하여 그것을 기준으로 <code>fallback</code>을 보여주도록 하였습니다.</p>
<pre><code class="language-jsx">  const [isActive, setIsActive] = useState(false);

  useEffect(() =&gt; {
      const element = document.querySelector(`${listClassName}`) as HTMLUListElement;
      const childCount = element.childElementCount;
      if (childCount === 0) {
        setIsActive(true);
      }
  }), []);</code></pre>
<p>이렇게 할시 동작은 당연히 되었습니다. 하지만 배열로 오는 response만 처리할 수 있다는 점과 전혀 확장성이 없는 로직이죠. </p>
<p>당장의 급한불은 끌 수 있지만 이대로 이러한 로직을 작성할시 다음 서버컴포넌트를 작성할떄는 또 그에 맞도록 로직을 변경해야 될지 모르기 때문에 방법을 변경하기로 합니다.</p>
<h2 id="두번째-도전---zustand를-이용한-서버---클라이언트-컴포넌트-연결">두번째 도전 - zustand를 이용한 서버 &lt;-&gt; 클라이언트 컴포넌트 연결</h2>
<p>두번째 시도했던것은 <code>zustand</code> 를 이용하여 서버 컴포넌트와 클라이언트 컴포넌트를 연결하는것입니다. </p>
<p>서버 컴포넌트에서는 <code>use...</code>과 같은 훅과 react의 모든 문법들은 이용하지 못합니다. 그렇기 때문에 react에서 사용가능한 (<strong>React.createContext로 만들어진</strong>) 상태관리 라이브러리들은 서버 컴포넌트에서 이용할 수 없습니다. 예를 들어 recoil, jotai등은 이용할 수가 없죠.</p>
<p>그렇다면 <code>redux</code>, <code>zustand</code> 같은 vanilla js에서도 돌아가는 상태관리 라이브러리들은 server component에서도 이용이 가능하였습니다.</p>
<p>번들크기를 고려하여 <code>zustand</code>를 선택하였습니다. </p>
<pre><code class="language-tsx">  export const useStore = create(() =&gt; ({
    isError: false
  }))</code></pre>
<p>이렇게 <code>useStore</code>에 isError: false로 초기화를 해줍니다.</p>
<pre><code class="language-tsx">export const getListData = async () =&gt; {
  try {
    const response = await fetch(`${HOME_API.API_GET_LIST}`);
    const serverData = await response.json();

    return serverData;
  } catch (e) {
    if (e instanceof Error) {
      useStore.setState({isError: true});
      return [];
    }
  }
};
</code></pre>
<p>그런 후에 아까의 api 호출하는 부분에서 catch에 걸리면 <code>isError</code> 를 true로 바꾸도록 설정해줍니다. zustand이기때문에 서버에서도 이러한 것이 가능한거죠.</p>
<pre><code class="language-tsx">&#39;use client&#39;

export const ServerErrorBoundary = ({children, fallback}: {children: React.ReactNode, fallback: React.ReactNode}) =&gt; {
  const isError = useStore.getState();

  if(isError) {
    return &lt;&gt;{fallback}&lt;/&gt;
  }

  return &lt;&gt;{children}&lt;/&gt;
}</code></pre>
<p>그 후에 <code>ServerErrorBoundary</code>라는것을 만들어서 아까만든 상태를 받아서 isError일 경우에는 <code>fallback</code>, 아니면 <code>children</code> 을 내보내도록 말이죠.</p>
<p>이것은 <code>client component</code>이기 때문에 fallback에도 자유자재로 <code>client component</code>를 이용할 수 있습니다.</p>
<hr>
<p>하지만 서버와 클라이언트 컴포넌트끼리 이러한 상태관리 툴로 나누는것이 맞을까요..? 이는 예상치 못한 동작을 야기시킬수도 있을것 같았습니다. 예를 들어 타이밍 문제 같은것도 있을 수 있고요..</p>
<p>저는 서버와 클라이언트는 <code>통신</code>을 통하여 데이터를 전달받는게 맞다고 생각하여 그 다음 과정인 <code>SSE</code>로 넘어가게 됩니다.</p>
<h2 id="세번째-도전---server-sent-event">세번째 도전 - Server Sent Event</h2>
<p>벌써 세번째 도전중입니다.. 이번에 시도해본것은 Server Sent Event인데요. 여러가지 통신의 방법중에서 왜 <code>Server Sent Event</code>를 선택하였을까요?</p>
<p>클라이언트의 트리거로 인하여 발생하는 http요청, 서버 &lt;-&gt; 클라이언트 양방향 실시간 통신의 websocket는 현재 상황과 맞지 않아서 Server Sent Event를 선택했습니다.</p>
<p>구현은 <code>nextjs api route</code>를 통하여 <code>sse</code>서버를 만들었습니다.</p>
<h3 id="sse-server">SSE Server</h3>
<pre><code class="language-jsx">import { NextRequest } from &#39;next/server&#39;;

import { eventEmitterObj, Ssekeys } from &#39;@/server/sse&#39;;

export const dynamic = &#39;force-dynamic&#39;;

export async function GET(
  req: NextRequest,
  { params }: { params: { type: (typeof Ssekeys)[number] } },
) {
  const { type } = params;

  const eventEmitterInstance = eventEmitterObj[type];

  try {
    const eventListener = (data: any) =&gt; {
      const eventData = `data: ${JSON.stringify(data)}\n\n`;
      eventEmitterInstance.data = eventData;
    };

    if (eventEmitterInstance.eventEmitter.listeners(`event-${type}`)[0]) {
      eventEmitterInstance.eventEmitter.removeListener(
        `event-${type}`,
        eventEmitterInstance.eventEmitter.listeners(`event-${type}`)[0] as (...args: any) =&gt; any[],
      );
    }

    eventEmitterInstance.eventEmitter.on(`event-${type}`, eventListener);

    const realResponse = new Response(eventEmitterInstance.data, {
      headers: {
        &#39;Content-Type&#39;: &#39;text/event-stream&#39;,
        Connection: &#39;keep-alive&#39;,
        &#39;Cache-Control&#39;: &#39;no-cache, no-transform&#39;,
      },
    });

    return realResponse;
  } catch (e) {
    if (e instanceof Error) {
      console.log(e);
    }
  }
}
</code></pre>
<h3 id="event-source받는-server-errorboundary">event source받는 Server ErrorBoundary</h3>
<pre><code class="language-jsx">&#39;use client&#39;;

import { useEffect, useState } from &#39;react&#39;;
import { sseActiveNames, sseErrorNames, Ssekeys } from &#39;../sse&#39;;

export const ServerErrorBoundary = ({
  targetName,
  children,
  fallback,
}: {
  targetName: (typeof Ssekeys)[number];
  children?: React.ReactNode;
  fallback?: React.ReactNode;
}) =&gt; {
  const [hasError, setHasError] = useState(false);

  useEffect(() =&gt; {
    const eventSource = new EventSource(`/home/api/sse/${targetName}`);

    eventSource.onmessage = async (event) =&gt; {
      if (event.data) {
        const data = JSON.parse(event.data);
        if (sseErrorNames[targetName] in data) {
          setHasError(true);
        }
      }

      eventSource.close();
    };
  }, [targetName]);

  if (hasError) {
    return &lt;&gt;{fallback}&lt;/&gt;;
  }

  return &lt;&gt;{children}&lt;/&gt;;
};
</code></pre>
<h3 id="event-관리자">event 관리자</h3>
<pre><code class="language-jsx">import { EventEmitter } from &#39;events&#39;;

export const Ssekeys = [
  &#39;test&#39;,
  &#39;dddd&#39;
] as const;

const entries = Ssekeys.map((key) =&gt; [
  key,
  { data: `data: ${JSON.stringify({})}\n\n`, eventEmitter: new EventEmitter() },
]);

export const eventEmitterObj = Object.fromEntries(entries) as Record&lt;
  (typeof Ssekeys)[number],
  { data: string; eventEmitter: EventEmitter }
&gt;;

export const sseEventNames = Object.fromEntries(
  Ssekeys.map((key) =&gt; [key, `event-${key}`]),
) as Record&lt;(typeof Ssekeys)[number], `event-${(typeof Ssekeys)[number]}`&gt;;

export const sseErrorNames = Object.fromEntries(
  Ssekeys.map((key) =&gt; [key, `error-${key}`]),
) as Record&lt;(typeof Ssekeys)[number], `error-${(typeof Ssekeys)[number]}`&gt;;

export const sseActiveNames = Object.fromEntries(
  Ssekeys.map((key) =&gt; [key, `isActive-${key}`]),
) as Record&lt;(typeof Ssekeys)[number], `isActive-${(typeof Ssekeys)[number]}`&gt;;


// 이벤트 발생 함수 -&gt; catch문 내부에서 이를 호출하면 됨.
export const emitErrorEvent = (targetName: (typeof Ssekeys)[number], e: Error) =&gt; {
  eventEmitterObj[targetName].eventEmitter.emit(sseEventNames[targetName], {
    [sseErrorNames[targetName]]: e,
  });
};
</code></pre>
<p>제가 생각한 과정은 다음과 같습니다. </p>
<p>에러가 발생하면 <code>catch</code>문에서 <code>emitErrorEvent</code>이벤트를 통해서 에러의 상태를 바꿉니다.</p>
<p>이제 SSE server에서 그 에러상태를 client에 내보냅니다.</p>
<p><code>client (Server ErrorBoundary)</code>에서 서버가 던져준 에러정보를 받아서 이를 통하여 <code>fallback</code>을 보여주도록 합니다.</p>
<hr>
<p>하지만 사실 SSE 서버가 서버 컴포넌트보다 늦게 초기화되기때문에 첫 빌드를 할때 동작을 하지 않습니다. </p>
<p>또한 현재 memory에 존재하는 값을 통하여 sse에게 에러 상태를 전달하고 있습니다. 사실 server component에서 발생하는 에러는 <code>빌드 타임</code>에서 발생하는것입니다. <code>window</code>객체같은 공유할 수 있는 객체에 넣어도 절대 참조를 할 수 없습니다.</p>
<p>*<em>빌드 타임에 나오는 에러에 대한것을 <code>sse server</code>가 던질 수 있도록 하려면 반드시 <code>memory (코드의 상수)</code>에서 참조해야되기 때문이죠. (위의 코드에서 <code>eventEmitterObj</code>)
*</em></p>
<hr>
<p>처음에 <code>SSE</code>로 하면 딱이겠다. 라는 생각으로 하다가 메모리를 참조하는 방법밖에 없다보니 실제로는 <code>SSE</code>를 사용할 필요가 없는 코드가 되었습니다. 오히려 괜히 <code>eventStream</code>으로 불필요한 네트워크 payload를 사용하게 되는 셈이죠.</p>
<h2 id="마지막-도전---그냥-memoryㅎ">마지막 도전 - 그냥 memory..ㅎ</h2>
<p><strong><code>SSE</code>코드에서 이제 <code>SSE</code>부분만 제거를 하였습니다.</strong> 실제로 이제는 key와 그 key에 해당하는 데이터만 존재하는 객체만 있을 뿐이죠.</p>
<p>동작 하는 로직은 다음과 같습니다.</p>
<ul>
<li>처음 default값은 <code>isError: false</code>로 되어있음</li>
<li>Server Component(ISR,SSG) 에서 빌드타임에 fetch후 컴포넌트를 그림</li>
<li>fetch에서 에러가 나와서 catch문으로 가게됨.</li>
<li>catch문에서 isError값을 true로 바꿔줌 -&gt; (isError는 그냥 코드상에 존재하는 객체이다.)</li>
<li>이제 javascript를 다운로드 완료하고 ErrorBoundary부분이 동작 (ErrorBoundary에서 isError면 fallback 보여줌)</li>
</ul>
<p>*<em>이렇게 될시 만약 <code>isr</code> 을 이용하여 5분마다 revalidate를 해주게 된다면 fetch를 하여서 에러가 발생할 시 isError값이 true가 되고 이 true값은 다음 revalidate값까지 계속 유지가 됩니다. (빌드타임때 변경이 되었기 때문에)
*</em>
그러면 어짜피 5분동안은 갱신이 되지않기때문에 계속 일관된 fallback을 보여줄 수 있는것이죠.</p>
<p><code>ssr</code> 을 이용할때는 새로고침할때마다 isError가 false, true인지 체크해서 보여주도록 합니다. </p>
<p>이렇게 될시 정말 간단하게 객체 하나를 이용해서 server component의 에러상태를 관리하고 그로인한 client component fallback이 가능하게 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Server Component 실전에서 써먹기 (1편 - 서버컴포넌트 잡기술)]]></title>
            <link>https://velog.io/@baby_dev/Server-Component-%EC%9A%B0%EC%95%84%ED%95%98%EA%B2%8C-%EB%8B%A4%EB%A3%A8%EA%B8%B0-1%ED%8E%B8</link>
            <guid>https://velog.io/@baby_dev/Server-Component-%EC%9A%B0%EC%95%84%ED%95%98%EA%B2%8C-%EB%8B%A4%EB%A3%A8%EA%B8%B0-1%ED%8E%B8</guid>
            <pubDate>Tue, 06 Jun 2023 15:50:11 GMT</pubDate>
            <description><![CDATA[<h1 id="😎😎-인기만점-서버-컴포넌트-😎😎">😎😎 인기만점 서버 컴포넌트 😎😎</h1>
<p>요즘 프론트엔드 부분에서 가장 큰 이슈 중 하나인 <code>서버 컴포넌트</code>는 대부분 들어보셨을겁니다. 하지만 실제로 <code>서버 컴포넌트</code>를 <strong>production</strong>에서 운영중인 회사도 많이 없을 뿐더러 규모가 큰 프로젝트에서 <code>서버 컴포넌트</code>를 사용한 사례가 크게 없는듯 하여 이번에 실제로 <code>서버 컴포넌트</code>를 운영까지 반영하며 겪었던 이슈와 그를 어떤 방식으로 해결하였고 올라간 복잡도를 어떻게 간편화 할 것 인지에 대해서 한번 알아보겠습니다.</p>
<h2 id="🚗-server-component로-인한-코드-작성법-변경---관심사의-분리">🚗 server component로 인한 코드 작성법 변경 - 관심사의 분리</h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/8c76349b-4d08-4e5a-929f-a48b48fc71bb/image.png" alt=""></p>
<p>가장 먼저 알아볼 것은 server component로 인해 <code>nextjs</code> 코드 작성의 변경점입니다.</p>
<p>서버 컴포넌트의 등장으로 인해서 <code>nextjs</code>를 작성할때 복잡도가 올라갔다고 많이 얘기를 합니다.</p>
<p>실제로 사용을 해보면 <code>서버 컴포넌트</code>로 이용하려는데 몇가지 요인때문에 이용을 못하거나 server component에서는 <code>useState</code>, <code>useEffect</code>, 그리고 <code>browser api</code> 등을 이용하지 못하 client component내부에 server component를 import못하는 문제등으로 인해서 굉장히 코드를 작성하기 힘들어집니다.</p>
<p>이에 대해서는 <code>서버 컴포넌트</code>의 <strong>단점</strong>과 <strong>고려사항</strong>만을 모은 추가 게시물을 작성할 예정입니다.</p>
<hr>
<p>하지만 다른관점으로 바라보면 저는 이 <code>서버 컴포넌트</code>의 존재가 관심사의 분리로서의 어느정도 강제성을 주는 이점이 존재한다고 생각합니다. </p>
<p><code>react-query</code>가 나온 원인 중 하나인 <code>redux-saga</code>, <code>redux-thunk</code> 같이 redux라는 하나의 스토어에서 클라이언트, 서버 데이터를 모두 다루는 것을 <code>서버와 클라이언트 데이터</code>에 대한 관심사를 분리하기 위해 나왔던 것 처럼 말이죠.</p>
<p><strong>이제 react-query가 맡던 서버 상태에 대한 관리는 <code>서버 컴포넌트</code>에서 모두 처리하며 그 데이터가 그리는 것 마저 <code>서버</code>에서 처리하기 떄문에 <code>client component</code>는 정말 서버 상태가 존재하지 않는 순수 <code>client</code> 에 대한 것만 처리하게 됩니다.</strong></p>
<hr>
<p>저는 다음과 같은 폴더 관리 방법으로 <code>서버 컴포넌트</code>와 <code>클라이언트 컴포넌트</code>를 분리하였습니다.</p>
<p>먼저 <code>도메인</code> 별로 관리할 폴더를 나눈뒤 그 내에서 <code>server</code>, <code>client</code>를 분리하여 컴포넌트를 관리하도록 하였습니다.</p>
<pre><code>ex)

- shop
  - server
    - shopList (server component)
  - client
    - shopItem (client component)</code></pre><h2 id="🌧-서버-컴포넌트에-외부-클라이언트-로직-부여하기">🌧 서버 컴포넌트에 외부 클라이언트 로직 부여하기</h2>
<p>다음 알아볼 방법은 <code>서버 컴포넌트에 외부 클라이언트 로직을 부여하기</code> 입니다.</p>
<p>과연 이게 어떤 상황일까요?</p>
<p>예를 들어 특정 <code>list</code>를 <strong>server component</strong>로 그렸다고 가정하겠습니다. 그런데 그 <code>list</code>를 swiper로 적용하고 싶다면?, 혹은 intersection observer를 통하여 <code>무한 스크롤</code> 혹은 <code>특정 지점에 감지</code>되었을때 동작을 하고 싶다면?</p>
<p>서버 컴포넌트에서는 이러한 클라이언트 로직을 이용할 수 없기때문에 개발에 지장을 줄 수 있습니다. </p>
<p>그래서 제가 생각한 방법은 간단합니다. 기존 클라이언트 로직을 부모 컴포넌트에 넘기는 것이죠.</p>
<p>마치 <code>Suspense</code>와 <code>ErrorBoundary</code>를 이용할때 <strong>로딩과 에러</strong>에 대한 로직을 부모 컴포넌트에 위임하는것으로 관심사를 분리한것처럼 말이죠.</p>
<pre><code class="language-jsx">// server component

export async function List() {
  const listData = await getListData();

  return (
      &lt;nav className=&quot;categoryMenu&quot;&gt;
        &lt;ul className=&quot;list&quot;&gt;
          &lt;GnbFallback /&gt;
          {gnbData?.map((datum, idx) =&gt; (
            &lt;GnbItem key={idx} {...datum} /&gt;
          ))}
          &lt;GnbMoreButton /&gt;
        &lt;/ul&gt;
        &lt;GnbCloseButton /&gt;
      &lt;/nav&gt;
  );
}</code></pre>
<p>여기서 <code>ListLogicWrapper</code> 이라는 <code>client component</code> 를 하나 만든 후 서버 컴포넌트에 감싸주면 됩니다. </p>
<p>그러면 <code>ListLogicWrapper</code>의 내부를 볼까요?</p>
<pre><code class="language-jsx">const ListLogicWrapper = ({ children }: { children: React.ReactNode }) =&gt; {
  const [isExpanded, _] = useGnbExpanded(); // list가 펴질지 안펴질지 처리하는 훅
  useFixedGnb(); // list가 특정 위치에 도달할시 intersection observer를 통하여 fixed 처리하는 훅
  useGnbDimmed(&#39;.list&#39;); // list가 펼쳐질 경우 외부화면을 dimmed 처리하는 로직

  // gnb 펼칠지, 안펼칠지 확인
  useEffect(() =&gt; {
    const target = document.querySelector(&#39;.categoryMenu&#39;);

    if (isExpanded) {
      target?.classList.add(&#39;active&#39;);
      return;
    }

    target?.classList.remove(&#39;active&#39;);
  }, [isExpanded]);

  return &lt;&gt;{children}&lt;/&gt;;
};</code></pre>
<p>일단 로직별로 하나하나 Wrapper를 만들면 오히려 <code>jsx</code>를 읽는데 불편하다고 생각되었기때문에 단순히 이 컴포넌트의 <code>로직</code>을 처리한다는 의미로 <code>ListLogicWrapper</code>와 내부에서 <code>custom hook</code>을 이용하여 로직에 대한 관심사를 분리하여 처리하였습니다.</p>
<p>그래서 하나의 wrapper에 <code>list가 펴질지 안펴질지 처리</code>, <code>list가 특정 위치에 도달할시 intersection observer를 통하여 fixed 처리</code>, <code>list가 펼쳐질 경우 외부화면을 dimmed 처리</code>등의 로직이 존재하는 것이죠.</p>
<h3 id="문제점">문제점</h3>
<p>여기서 제가 생각하는 하나의 문제점이 있습니다. client component와 server component간의 <code>ref</code>를 이용하여 서로 전달받을 수 없다는것. 그렇기 때문에 <code>react</code>에서 안티패턴중 하나인 <code>real-dom</code>에 직접 접근을 하여 처리를 해야합니다.</p>
<p>그리고 현재는 회사에서 퍼블리셔팀이 따로 존재하기에 <code>pure css</code>를 이용하고 있는데 이것이 후에 <code>css-in-js</code>로 변경되었을때 어떻게 관리하면 좋을지에 대해서도 따로 알아봐야 될 내용입니다.</p>
<h2 id="⛵️-여러개의-서버컴포넌트-이용">⛵️ 여러개의 서버컴포넌트 이용</h2>
<p>다음은 하나의 클라이언트 컴포넌트에 여러개의 서버컴포넌트를 부르고 싶을 때 입니다.</p>
<p>사실 이건 간단한데요. </p>
<p>따로 nextjs docs에도 없고 여러분들이 typescript를 이용하실때 보통 사용하는 <code>children</code> 타입을 쓰면 <strong>type error</strong>가 나오기 때문에 몰랐을 가능성이 있습니다.</p>
<pre><code class="language-tsx">{children}: {children: React.ReactNode}</code></pre>
<p><strong>사실 children은 배열형태로 오기때문에 서버컴포넌트를 이용할떄는 <code>children[0]</code>, <code>children[1]</code>과 같이 직접 요소에 접근하여서 사용하시면 됩니다.</strong></p>
<pre><code class="language-jsx">&lt;ClientComponent&gt;
  &lt;FirstServerComponent /&gt;
  &lt;SecondServerComponent /&gt;
&lt;/ClientComponent&gt;
</code></pre>
<pre><code class="language-tsx">// ClientComponent.tsx

export const ClientComponent = ({children}: {children: React.ReactNode[]}) =&gt; {
  return (
    &lt;div&gt;
      {children[0]} // FirstServerComponent
      {children[1]} // SecondServerComponent
    &lt;/div&gt;
  )
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 ci/cd 속도 최적화 (feat. docker multi staging)]]></title>
            <link>https://velog.io/@baby_dev/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-cicd-%EB%B9%8C%EB%93%9C-%EC%86%8D%EB%8F%84-%EC%B5%9C%EC%A0%81%ED%99%94-feat.-docker-multi-staging</link>
            <guid>https://velog.io/@baby_dev/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-cicd-%EB%B9%8C%EB%93%9C-%EC%86%8D%EB%8F%84-%EC%B5%9C%EC%A0%81%ED%99%94-feat.-docker-multi-staging</guid>
            <pubDate>Sun, 04 Jun 2023 16:23:05 GMT</pubDate>
            <description><![CDATA[<h1 id="돌아온-빌드-속도-최적화">돌아온 빌드 속도 최적화</h1>
<p>기존에 github action에서 빌드속도 최적화, webpack에서의 빌드속도 최적화 부분을 작성한것이 있었는데 이번에 또 빌드 속도 최적화라는 글로 다시 작성하게 되네요.</p>
<p><a href="https://velog.io/@baby_dev/github-action-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-%EB%B9%8C%EB%93%9C%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0%EA%B8%B0">github action 최적화</a></p>
<p>이 글을 작성하는 이유는 지난번에 작성한 최적화는 실제 현업에서 빌드를 할떄의 환경과 가볍게 ci만 최적화하던것이 달랐기 때문입니다.</p>
<p>기존에는 <code>docker</code>도 이용하지 않고 <code>package manager</code>를 yarn에 고정하고 다른것을 고려하지 않았었습니다. 또한 이번에는 nextjs를 이용하기 때문이죠.</p>
<p>먼저 최종 결과부터 한번 같이 보시죠. jenkins를 이용하였으며 좌측에는 최적화 전, 우측이 최적화 후입니다. <strong>(우측의 상단이 캐싱 x, 하단이 캐싱이 적용된 사진입니다.)</strong></p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/b0f229a0-7691-43ac-82d4-c77664dc3301/image.png" alt=""> | <img src="https://velog.velcdn.com/images/baby_dev/post/13dfafaa-aa44-4183-ae2e-16f3ec66f39b/image.png" alt="">
| --- | --- | </p>
<ul>
<li><strong>원인 분석</strong></li>
<li><strong>docker 이미지 줄이기</strong><ul>
<li>docker multi staging</li>
</ul>
</li>
<li><strong>package manager 선택하기</strong><ul>
<li>yarn berry</li>
<li>yarn berry를 포기한 이유</li>
<li>번외) docker layer cache</li>
<li>번외) 중복된 의존성 제거(dedupe)</li>
</ul>
</li>
<li><strong>swcMinify를 통한 빌드속도 최적화</strong></li>
<li><strong>결론</strong></li>
</ul>
<p>다음과 같은 목차로 알아볼것입니다.</p>
<h2 id="원인-분석">원인 분석</h2>
<p>현재 jenkins는 크게 다음과 같은 3개의 과정으로 돌아가고 있었습니다.</p>
<ul>
<li>yarn install (in docker build)</li>
<li>yarn build (in docker build)</li>
<li>docker push</li>
</ul>
<p>이 각각을 분석했을때 docker push가 가장 큰시간을, 그 다음 yarn install, 다음 yarn build순으로 시간을 많이 잡아먹었습니다.</p>
<h3 id="docker🐳-push가-느려요">docker🐳 push가 느려요</h3>
<p>3개의 원인중에서도 docker push가 압도적으로 느리게 동작을 했었는데 그 원인은 바로 docker image의 크기였습니다.</p>
<p>docker build를 했을때 나오는 docker image가 4.07gb였기때문이죠.</p>
<p>docker push가 빠르게 돌아가면 오히려 이상할 정도의 사이즈죠. 이는 물론 서버 메모리에도 굉장히 악영향을 줍니다.</p>
<h3 id="yarn-install이-느려요">yarn install이 느려요</h3>
<p>기존에 사용하는 yarn (v1) 을 이용하여 install할때 속도가 꽤 느렸습니다. 3~4 분정도는 이녀석이 잡아먹었기 때문이죠.</p>
<p>yarn install에 대해 제가 내린 문제상황은 3개였습니다.</p>
<ul>
<li>패키지 매니저의 한계 (yarn v1 -&gt; yarn berry, pnpm)</li>
<li>캐시가 적용되지않음 </li>
<li>불필요한 패키지가 존재하지않는가</li>
</ul>
<h3 id="build-속도가-느려요">build 속도가 느려요.</h3>
<p>사실 프로젝트를 빌드하는속도는 크게 느리지않았습니다. 위의 2개의 비해서는 말이죠.</p>
<p>그야 대부분의 것은 nextjs가 최적화해주기때문에 정말 불필요한 패키지가 많이들어있지 않는이상 큰 문제는 없었습니다.</p>
<p>오늘은 미약하게라도 최적화를 시킬방법을 알아볼겁니다.</p>
<h2 id="🐳-docker-이미지-줄이기">🐳 docker 이미지 줄이기</h2>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/95970ddc-155d-4a10-87ff-b706a75128e5/image.png" alt=""></p>
<p>아까 docker push의 속도가 느린 docker image를 줄이는 방법에 대해서 알아보겠습니다.</p>
<p>먼저 불필요한 layer를 제거하는 과정이 필요합니다. 예를 들어 동일한 command를 연달아 이용하는 레이어가 있다면 그것을 하나의 커맨드와 줄띄움(<code>\</code>)과 이용할 수 있습니다.</p>
<p>그 다음 경량화된 패키지를 이용하는 것이 좋습니다. 여기서 말하는 패키지란 무엇일까요? javascript를 돌릴때는 node, python을 돌릴때는 pip를 이용하는것처럼 처음에 이러한 패키지를 dockerfile에서 받아주는데요.</p>
<p>일단 프론트엔드에서는 javscript를 이용하니 node를 이용할것입니다.</p>
<p>기존에 이용하던 node16의 크기는 ... 입니다. 하지만 node16의 경량화 버전인 node16-alpine을 이용한다면 ...까지 줄어들게 됩니다.</p>
<p>그리고 마지막으로 제목에도 적혀있듯이 가장 중요한 docker multi staging 입니다.</p>
<p><code>docker multi staging</code>이란 실제 docker를 run시킬때 필요한 파일들만 마지막에 남길 수 있도록 하는 것입니다.
**
실제로 yarn install, yarn build 등등 여러가지 동작을 하지만 실제로 운영에 필요한것은 <code>.next</code>, <code>yarn.lock</code>, <code>node_modules</code>등등의 정적파일만 존재하면 됩니다.**</p>
<p>그것을 위해 실제 필요한것만 처리하는 stage들을 나누는 전략이 <code>docker multi staging</code>입니다. </p>
<hr>
<h3 id="🐳-docker-multi-staging">🐳 docker multi staging</h3>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/b2fc26dc-2434-446e-bada-46cf17f5c387/image.jpeg" alt=""></p>
<p>staging을 나눌때 저는 크게 <code>의존성 설치하는 deps</code>, <code>build를 하는 builder</code>, <code>실제 실행하는 runner</code> 이렇게 3개로 나누어서 이용합니다.</p>
<p>docker multi staging을 이용할때는 <strong>From ... As <code>stage</code></strong> 문법과 <strong>--from={stage}</strong> 만 기억하시면 됩니다.</p>
<p>첫번째의 <strong>From ... As <code>stage</code></strong>는 stage를 정의하는 것이며, 각각의 스테이지는 완전히 독립적인 환경을 지닙니다. 예를 들어서 <code>deps stage</code>에서 온갖 파일을 만들어낸다하더라도 <code>builder stage</code>에는 기존처럼 초기화되어있는 것이죠.</p>
<p>두번째의 <strong>--from={stage}</strong>는 초기화된 환경에서 지난 stage의 정적파일만을 가져올 수 있도록 합니다. 예를 들어 deps stage에서 yarn install을 하면 그때서야 <code>node_modules</code>가 생길것입니다. 그러면 builder stage에서는 yarn install을 따로 하지않고 node_modules만 가져올 수 있는것이죠.</p>
<p>다음은 docker multi staging을 적용한 간단한 예시입니다.</p>
<pre><code class="language-jsx">
FROM node:16-alpine AS deps // deps stage -&gt; 의존성을 설치
ARG PROFILE
ENV PROFILE=${PROFILE}
ARG VERSION=0.0.1
WORKDIR /web/src

COPY [&quot;yarn.lock&quot;, &quot;package.json&quot;, &quot;./&quot;]
COPY [&quot;.dockerignore&quot;, &quot;lerna.json&quot;, &quot;.gitignore&quot;, &quot;./&quot;]
COPY ./package.json ./package.json

RUN yarn install

FROM node:16-alpine As builder // build stage
WORKDIR /web/src
ARG PROFILE
ENV PROFILE=${PROFILE}
COPY --from=deps /web/src/package.json ./ //deps stage에서 필요한 파일을 가져온다.
COPY --from=deps /web/src/lerna.json ./lerna.json
COPY --from=deps /web/src/yarn.lock ./yarn.lock
COPY --from=deps /web/src/node_modules ./node_modules
COPY ./web.src ./

RUN yarn run build:${PROFILE}

FROM node:16-alpine As runner // docker run stage
WORKDIR /web/src
COPY --from=builder /web/src/lerna.json ./lerna.json // --from=builder에서 필요한 파일을 가져온다.
COPY --from=builder /web/src/yarn.lock ./yarn.lock
COPY --from=builder web/src/.next ./.next
COPY --from=builder web/src/public ./public
COPY --from=builder web/src/package.json ./package.json
COPY --from=builder web/src/node_modules ./node_modules

COPY  ./packages/docker/init.sh ./

CMD [&quot;yarn&quot;, &quot;start&quot;]</code></pre>
<p>최종 결과로 4.1GB였던 docker image가 660MB로 줄어들었고 docker push 시간 역기 30초 내외로 되었습니다.</p>
<h2 id="🔑-package-manager-선택하기">🔑 package manager 선택하기</h2>
<p>yarn install, 즉 의존성을 설치할때 최적화하는 방법중 하나인 package manager에 대해서 알아보겠습니다. </p>
<p>사실 package manager의 <code>npm</code>, <code>yarn classic</code>, <code>yarn berry</code>, <code>pnpm</code> 등을 모두 다루는 것은 이미 다른 게시물에도 많이 존재하기 때문에 본 게시물에서는 다루지 않겠습니다.</p>
<p>이번에는 <code>yarn berry</code>의 <code>zero-install</code>을 이용하여 빌드속도를 최적화하였는데요. </p>
<p>간단하게 <code>yarn berry</code>에 대해 설명입니다.</p>
<h3 id="yarn-berry">yarn berry</h3>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/ff22228d-563e-40a8-bb07-8a8b9673fe65/image.webp" alt=""></p>
<blockquote>
<p>yarn v1을 yarn classic, yarn v2이상을 yarn berry라고 한다. 현재 yarn에서는 yarn classic에 대한 업데이트는 멈춘 상태이며 yarn berry에 대한 업데이트만을 진행합니다. (현재 yarn 3.3.1)
기존 yarn에서 yarn berry로 변경하려면 <code>yarn set version berry</code> 명령어를 이용하면 된다. 그런뒤 yarn install을 진행하면 node_modules대신에 <code>pnp.cjs, pnp.loader.cjs, .yarn</code> 이 생기게 됩니다.</p>
</blockquote>
<blockquote>
<p>yarn berry는 node_modules가 아닌 zip으로 패키지를 저장하기 때문에 훨씬 가볍고 install을 하는데 시간을 소비하지 않습니다.
그리고 yarn berry부터는 <code>.yarnrc.yml</code> 파일이 필수로 필요하기 때문에 로컬에서 이를 만들어준뒤 내부에 config를 설정해야만 합니다.</p>
</blockquote>
<ul>
<li>.yarnrc.yml<pre><code class="language-yaml">nodeLinker: pnp
</code></pre>
</li>
</ul>
<p>packageExtensions:
  next@<em>:
    dependencies:
      react: &quot;</em>&quot;
      react-dom: &quot;*&quot;</p>
<p>yarnPath: .yarn/releases/yarn-3.3.1.cjs</p>
<pre><code>다음은 yarn berry를 이용하기 위한 `.yarnrc.yml` 파일입니다. 가장 중요한 3개의 옵션을 따로 뽑아 왔습니다.

**`nodeLinker`**는 node_modules, pnp, pnpm 이렇게 3개의 모드가 존재하며 `node_modules`는 기존 yarn classic과 같으며, `pnp`는 `zero install`을 지원하며 node_modules대신 **.yarn과 pnp.cjs**를 통하여 의존성을 확인합니다., `pnpm`은 pnpm패키지 매니저와 동일하게 동작을 합니다.

여기서 zero-install을 이용하기 위해 저는 `pnp`모드를 이용했습니다.

**`packageExtensions`**는 yarn berry에게 의존성을 알려주는 역할입니다. 만약 `yarn install`을 했을때 `A require … provides B` 에러가 나온다면 packageExtensions에 다음과 같은 형식으로 넣어 주어야 합니다.


### yarn berry를 포기한 이유

이렇게 yarn berry를 통하여 zero-install을 이용하려 했지만 `yarn berry`는 다양한 문제가 있었습니다. 사실 대부분 yarn berry를 이용하는 곳에서 어느정도 한계를 느끼고 `pnpm`으로 전환을 많이 하시는것 같았습니다.

ex) https://engineering.ab180.co/stories/yarn-to-pnpm

저 역시 특정 라이브러리를 이용하려할때 yarn berry와의 호환이 잘 맞지 않은 문제가 존재하였고 pnp모드를 이용하면서 생기는 `.yarn/cache`폴더의 비대함 문제와 의존성이 변경되면 `.yarn/cache`내부의 `zip`파일들이 모두 **file changes**로 되어 **코드리뷰가 힘들어 진다는 점**에서 `yarn berry`를 포기하였습니다.

![](https://velog.velcdn.com/images/baby_dev/post/6d0f3b88-0bd5-4253-b47c-4eb113bcc8f9/image.svg)

최종적으로는 `pnpm`을 이용하고 있으며 node_modules를 동일하게 이용하지만 `yarn classic` 사용할때 보다 빌드 시간이 1분가량 줄어든것을 확인할 수 있었습니다.


### 번외) 🐳 docker layer cache

install속도를 최적화한다기보다 `docker build`를 할때 의존성의 변경이 없으면 따로 install을 하지 않도록 하는 docker layer cache에 대해서 알아보겠습니다.

실제로 docker는 이전 빌드결과물과 내용이 동일한것을 COPY하면 cache를 이용하여 그 과정을 진행하지 않습니다. 

그래서 위의 `Dockerfile`을 보시면 `COPY ./web/src ./` 이렇게 통째로 복사하면 될것을 `COPY ./web/src/package.json ./package.json` 와 같이 따로 쪼개서 COPY를 하고 있습니다.

그 이유는 `COPY ./web/src ./` 와 같이 통째로 COPY를 하면 무조건 안의 내용이 변하기 때문에 docker layer cache가 동작하지 않습니다.

하지만 `COPY ./web/src/package.json ./package.json` 로 쪼개서 이용을 하면 package.json만의 변경을 확인하기때문에 의존성에 대한 docker layer cache를 이용할 수 있습니다.

### 번외) 중복된 의존성 제거하기 (dedupe)

yarn berry의 `yarn dedupe`, pnpm의 `pnpm dedupe`등 최신 패키지 매니저에는 `dedupe`라는 명령어로 중복으로 설치된 불필요한 패키지들을 제거해줍니다. 

의존성 자체가 가벼워지기 때문에 `의존성 설치` 뿐만 아니라 후의 `build`를 하는데에도 최적화 효과를 볼 수 있습니다.

## 🛠 swcMinify를 통한 빌드속도 최적화 (Next.js)

![](https://velog.velcdn.com/images/baby_dev/post/5a64919d-28d2-449a-a577-577812411871/image.jpeg)


사실 이 부분은 아까도 말씀드렸듯이 별것 없습니다. Nextjs에서 대부분 최적화를 진행해주기 때문이죠. 그래서 `next.config.js`의 옵션을 이용하여 약간이나마 최적화가 가능합니다.

`Nextjs 12.3.0` 버전부터 정식 지원하는 swc 컴파일러를 이용하면 되는데요. 이 옵션은 기존 `prodution` 에 정적파일을 내보낼때는 번들사이즈를 줄이기 위해 `terser`라는 것으로 `minify`를 진행하는데요. 이 `terser`는 javascript로 작성되어있기 때문에 컴파일 속도가 느립니다.

하지만 `swc`라는 컴파일러를 이용할 경우에는 low level의 rust언어를 이용하기 때문에 javascript보다 훨씬 빠른 컴파일 속도를 자랑합니다.

설정하는법은 매우 간편한데요. 단지 `next.config.js`에 **`swcMinify: true`**로 설정하시면 됩니다.

---

만약 Nextjs가 아니라 직접 webpack을 이용해서 구성하신다면 다음 링크를 보시면 됩니다.

[webpack 빌드속도 최적화 - 1](https://velog.io/@baby_dev/%EC%9B%B9%ED%8C%A9-%EB%B9%8C%EB%93%9C%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0%EA%B8%B0-feat.-%EC%86%8C%EC%8A%A4%EB%A7%B5-runtimeChunks-ts-loader)
[webpack 빌드속도 최적화 - 2](https://velog.io/@baby_dev/%EC%9B%B9%ED%8C%A9-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-%EB%B9%8C%EB%93%9C-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0%EA%B8%B0-2)

# 결론

간단하게 `빌드속도` 최적화 하는 방법에 대해서 알아봤습니다. 물론 이 부분은 현재 작업하고 있는 환경에 따라 최적화 방법이 달라질텐데요. 

오늘은 `Nextjs`, `pnpm`, `docker`, `jenkins` 를 이용하는 프로젝트 설정일 경우에 최적화 할 수 있는 방법에 대해서 알아봤습니다.

만약 github action을 이용한다면 `docker layer cache`가 작동하지 않기 때문에 `docker/build-push-action`을 이용해야 되는등의 추가옵션이 존재할 수 있습니다.

&gt; 마지막으로 기술스택에 제약받지 않는 프론트엔드에서의 빌드속도 최적화 방법을 원하신다면 다음과 같은 흐름으로 찾아보시면 수월합니다.

- 일단 느린 문제원인을 정확히 파악하자
- 빌드속도 최적화의 가장 핵심중 하나는 `캐시`다. 굳이 다시 해도되지 않을 과정일것 같다면 캐시가 가능할지 부터 생각해보자.
- js로 처리하여 느린것들에 대한것들은 `swc`로 대체되어있는 방법이 있나 알아보자. `terser -&gt; swcMinify`, `ts-jest -&gt; @swc/jest` 생각보다 swc로 최적화 되어있는 패키지들이 존재한다.
- webpack을 사용하고 있다면 다른 모듈 번들러 고민도 해보자. 최근에는 **vite, esbuild, turbopack(실험)**등등 많은 것들이 나오고 있다.
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[극한의 프론트엔드 성능 최적화 3편 (외부 스크립트 최적화 feat. partytown🎉)]]></title>
            <link>https://velog.io/@baby_dev/%EA%B7%B9%ED%95%9C%EC%9D%98-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-3%ED%8E%B8-%EC%99%B8%EB%B6%80-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%B5%9C%EC%A0%81%ED%99%94-feat.-partytown</link>
            <guid>https://velog.io/@baby_dev/%EA%B7%B9%ED%95%9C%EC%9D%98-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-3%ED%8E%B8-%EC%99%B8%EB%B6%80-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%B5%9C%EC%A0%81%ED%99%94-feat.-partytown</guid>
            <pubDate>Thu, 18 May 2023 15:14:23 GMT</pubDate>
            <description><![CDATA[<h1 id="외부-스크립트-최적화하기">외부 스크립트 최적화하기</h1>
<p>이번에 얘기해볼 주제는 <strong>외부 라이브러리</strong>를 최적화하는 방법입니다.</p>
<p>작은 프로젝트에서는 이러한 외부스크립트를 사용할일이 많이 존재하지않지만 어느정도 커지게 된다면 <strong>ga, gtm등 분석하는 도구를 외부 스크립트</strong>를 이용할일이 생기며 회사의 레거시때문에 어쩔수없이 외부 스크립트를 이용할때가 있을겁니다.</p>
<p>물론 오늘 얘기할 내용은 <strong>외부 스크립트가 아니더라도 비용이 큰 스크립트라면 이 방법으로 최적화</strong> 시킬수있습니다.</p>
<h2 id="메인스레드가-블로킹-되지-않게-하기---🎉partytown🎉">메인스레드가 블로킹 되지 않게 하기 - 🎉partytown🎉</h2>
<p>요즘 가장 빠른 자바스크립트 프레임워크로 뜨고있는 <strong>qwik</strong>을 만든 builder.io에서 만든 <a href="https://github.com/BuilderIO/partytown">라이브러리</a>입니다.</p>
<p>이 라이브러리의 핵심은 바로 <strong><code>web worker</code></strong>입니다. <code>web worker</code> 는 비용이 오래 걸리는 코드가 메인 스레드를 블로킹하는것을 막기위해서 비용이 오래걸리는 코드만 다른 스레드에서 처리할수 있는 하나의 서비스 워커입니다.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/d2b4bbbd-1286-4835-9ef1-1db522d70a11/image.png" alt=""></p>
<p><a href="https://developer.mozilla.org/ko/docs/Web/API/Web_Workers_API/Using_web_workers">Web Worker API</a></p>
<hr>
<p>예를 들어 프로젝트에 for문을 1억번 돌리는것이 있을경우 그 for문이 끝날때까지 그 다음 코드로 넘어가지 않습니다. </p>
<p>이것을 해결하기위해 나온것이 <code>web worker</code> 입니다. 그렇다면 <code>web worker api</code> 자체로 스크립트를 다른 스레드에서 작업하게하면 되는게 아니냐라고 생각할수있지만 web worker에서 할수없는 기능들을 partytown 에서 가능하게 하기때문에 이용합니다.</p>
<blockquote>
<p>*<em>기존 web worker에서는 window객체, dom에 접근을 할수없습니다.
*</em>
하지만 요즘 외부라이브러리들은 window객체를 통하여 상태를 저장시키거나 dom에 직접 접근을 하는 경우가 있습니다. partytown은 web worker, proxy, custom event등의 문법의 조합으로 web worker환경에서도 window, dom에 접근이 가능하도록 하였습니다.</p>
</blockquote>
<p><code>밑에는 간단하게 partytown의 동작에 대한 공식 사진입니다.</code></p>
<h4 id="partytown의-동작-과정">partytown의 동작 과정</h4>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/76a5d763-54cd-4dd1-9168-a5ee37337560/image.jpeg" alt=""></p>
<h4 id="partytown의-동작원리">partytown의 동작원리</h4>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/a59300e9-72b3-45ec-ae4d-6d6bd6feb9e8/image.jpeg" alt=""> </p>
<p>webworker, proxy 그리고 event를 통하여 web worker의 한계를 극복한 partytown의 구현체입니다.</p>
<h3 id="그래서-어떻게-사용">그래서 어떻게 사용..?</h3>
<p>이번에 이용한 프로젝트가 nextjs이기 때문에 nextjs 기준으로 설명드리겠습니다. 그 외의것들도 <a href="https://partytown.builder.io/">partytown의 공식문서 </a>에 잘나와있으니 보면서 하면 큰 문제가 없을겁니다.</p>
<pre><code>// layout.tsx

    &lt;Head&gt;
      &lt;Partytown forward={[&#39;dataLayer.push&#39;]} /&gt;
      &lt;script type=&quot;text/partytown&quot; src=&quot;https://www.googletagmanager.com/gtm.js&quot; /&gt;
    &lt;/Head&gt;</code></pre><p>먼저 <code>next/head</code> 부분에 Partytown이라는 태그를 넣어주면 webworker를 사용할 준비는 마친겁니다.</p>
<p>여기서 forward란 gtm처럼 <code>window.dataLayer</code>에 접근하는 코드가 있을 경우에는 forward에 저렇게 넣어줘야 <code>dataLayer.push</code> 라는것을 인지하고 에러를 뱉지 않습니다.</p>
<pre><code>        &lt;script
          type=&quot;text/partytown&quot;
          dangerouslySetInnerHTML={{
            __html: `
              document.getElementById(&#39;output-script&#39;).textContent = &#39;passed&#39;;
              document.body.classList.add(&#39;completed&#39;);
            `,
          }}
        /&gt;</code></pre><p>이렇게 partytown 라이브러리를 이용해서 사용할 수 있을뿐만 아니라 nextjs에서 제공하는 <code>Script</code>태그에도 partytown을 내장하고 있는 문법이 있습니다.</p>
<p>바로 <code>strategy attribute</code>인데요.</p>
<p><a href="https://nextjs.org/docs/pages/building-your-application/optimizing/scripts#offloading-scripts-to-a-web-worker-experimental">nextjs Script docs</a></p>
<p>별도의 설정없이 Script태그에 <code>strategy=&quot;worker&quot;</code>을 주면 이용이 가능합니다.</p>
<pre><code class="language-jsx">&lt;Script src=&quot;https://example.com&quot; strategy=&quot;worker&quot; /&gt;</code></pre>
<p>하지만 이것은 <code>app</code> directory와 마찬가지로 실험적인 기능이기 때문에 next.config.js의 experimental옵션에 다음과 같은 설정을 해주어야 합니다.</p>
<pre><code class="language-js">module.exports = {
  experimental: {
    nextScriptWorkers: true,
  },
};</code></pre>
<h3 id="한계">한계</h3>
<p>*<em>하지만 partytown의 현재 버전은 0.8.0 베타버전이기도 하며 모든 외부스크립트에 적용이 가능한것은 아닙니다. 
*</em>
특히 내부에 setInterval로직이 있는 그런 스크립트같은 경우 partytown이용시 web worker를 똑바로 못받아오는 문제가 존재합니다.</p>
<p>이 부분만 주의하시면 비용이 큰 스크립트를 따로 처리하는데는 큰 문제가 없을것 같습니다.</p>
<h2 id="🏃🏻미리-받아오기---preload-preconnect">🏃🏻&#39;미리&#39; 받아오기 - preload, preconnect</h2>
<p>중요도가 높은 스크립트들을 빨리 받아올때 무엇보다 필요한 옵션들입니다.</p>
<p>예를 들어 최대한 빨리 보여져야하는 폰트, 혹은 가장 처음 보이는 뷰에 영향을 끼치는 스크립트라면 최대한 빨리 받아오는 것이 좋겠죠.</p>
<p>그것을 위해서 <code>preconnect</code>와 <code>preload</code>를 이용할수 있습니다.</p>
<p><a href="https://beomy.github.io/tech/browser/preload-preconnect-prefetch/">https://beomy.github.io/tech/browser/preload-preconnect-prefetch/</a></p>
<h3 id="🏃-preconnect---미리-도메인에-연결하기">🏃 preconnect - 미리 도메인에 연결하기</h3>
<p>preconnect는 외부 도메인에서 특정 스크립트및 css를 받아올때 용이한 옵션입니다.</p>
<p>이 옵션을 설정하면 실제 외부 도메인과 연결할때 미리 필요한 소켓을 연결하기에 실제로 요청을 할때는 dns, tcp시간을 절약할 수있습니다.</p>
<p>예를 들어 <a href="https://example.com%EC%9D%84">https://example.com을</a> 미리 preconnect 설정해놓으면 이곳으로 보내는 요청에 대한것은 미리 소켓을 연결한 상태로 처리하기 때문에 조금 더 빠르게 응답이 이루어질것입니다.</p>
<pre><code class="language-jsx">&lt;link rel=&quot;preconnect&quot; href=&quot;https://example.com&quot; /&gt;</code></pre>
<h3 id="🏃-preload---미리-다운로드-받아버리기">🏃 preload - 미리 다운로드 받아버리기</h3>
<p>현재 페이지에 필요한 리소스의 우선순위를 높여서 가장 먼저 받아오게 하는 기법입니다.</p>
<p>아까 말했듯이 폰트나 가장 먼저 보이는 뷰에 영향을 끼치는 스크립트에 적용을 하면 좋습니다.</p>
<pre><code class="language-jsx">&lt;link rel=&quot;preload&quot; src=&quot;https://fontfont.com/font&quot; as=&quot;font&quot; /&gt;</code></pre>
<p>참고로 as에 불러오는 리소스의 속성을 제대로 명시해줘야 리소스를 이용할 수있습니다.</p>
<h3 id="알아두면-좋을것---prefetch-prerender">알아두면 좋을것 - prefetch, prerender</h3>
<p>이번 프로젝트에 사용한것은 preload, preconnect정도이지만 <strong>prefetch, prerender</strong>도 최적화하는데 많은 도움이 될수있습니다.</p>
<p>*<em><code>prefetch</code>는 <code>pre</code>가 받아져있지만 오히려 우선순위를 높인다기 보다는 우선순위를 늦춘다고 생각하시면 됩니다.
*</em>
미래에 사용할 컴포넌트를 미리 받아오고 후에 캐시에 저장하고 컴포넌트를 받아올시 캐시에 있는 값을 보내주는 역할입니다. 즉, 미래의 사용할 컴포넌트에 이용하시면 됩니다.</p>
<pre><code class="language-jsx">&lt;link rel=&quot;prefetch&quot; href=&quot;about.html&quot;&gt;</code></pre>
<p>이렇게 <code>link</code>태그를 이용하여 자원을 미리 받아올 수 있지만 또 webpack에서 <strong>import의 형식 주석으로도 표현할 수 있습니다.</strong></p>
<pre><code class="language-jsx">import(/* webpackPrefetch: true */ &#39;./prefetch.jsx&#39;);</code></pre>
<hr>
<p><strong><code>prerender</code>는 다음에 이동할 페이지에 대해서 미리 렌더링을 한뒤 그 페이지로 이동할 경우 캐시된 페이지를 보여주는 것입니다.</strong></p>
<pre><code>&lt;link rel=&quot;prerender&quot; href=&quot;https://future.com&quot;&gt;</code></pre><p>사용할일이 있으면 적극적으로 활용하는것도 좋지만 두 전략모두 미리 받아와서 캐시에 저장하는것이기때문에 불필요한것을 받아오지 않고 너무 많은 리소스를 미리 받아오지 않도록 조심해야합니다.</p>
<p><strong>캐시라는것도 자원을 사용하는것이기 때문이죠 👀</strong></p>
<h3 id="⚠️-주의해야할점-⚠️">⚠️ 주의해야할점 ⚠️</h3>
<p>*<em>하지만 모든 최적화기술에는 어느정도 한계 및 약점이 있기 마련이죠.
*</em>
<code>preconnect</code> 같은 경우 새 연결을 연다는것 자체가 cpu 리소스를 사용하는 일이기때문에 과도하게 사용하면 좋지 않습니다.</p>
<p><code>preload</code> 같은 경우에는 필요하지 않는것은 불러오지 않도록 주의해야합니다.</p>
<p><code>prefetch</code>, <code>prerender</code>는 앞에서 말했듯이 미리 불러놓고 캐시에 저장하는것이기 때문에 불필요한것에 모두 이용하게 된다면 불필요하게 리소스가 많이 사용될것입니다. </p>
<h2 id="👻-비동기로-스크립트-받아오기---async-defer">👻 비동기로 스크립트 받아오기 - async, defer</h2>
<p><code>async</code>, <code>defer</code> 은 비교적 많이 들어봤을거라고 생각됩니다. 스크립트를 비동기로 받아올때 주로 사용합니다.</p>
<p>동기적으로 받아온다면 용량이 큰 스크립트를 받아올때는 뒤에 받아올 리소스들이 블로킹 되기 때문이죠.</p>
<p>그래서 실제 보이는 화면에 영향을 주는 스크립트같은 경우 미리 로드하고 DomContentLoad이벤트가 발생하면 그때 실행하는 <strong>defer</strong>, 그게 아닐시 화면과 상관없이 다운로드를 마치면 실행하는 async를 사용하면 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/d6943116-a275-4275-9476-521b0d73b23a/image.png" alt=""></p>
<p>이러한 스크립트말고도 css에 대해서도 비동기로 불러올 수 있는데요.</p>
<p>ssr을 이용할시 js같은 경우 늦게 로드가 되더라도 상호작용 시간이 느려질뿐 실제 화면은 보이게 되는데요. 하지만 css파일이 느려질경우에는 렌더트리를 그려지는것자체가 느려지기때문에 첫 html파일자체가 늦게 보이게되어 ssr의 장점이 없어지게됩니다.</p>
<h3 id="css-비동기로-받아오기">css 비동기로 받아오기</h3>
<pre><code class="language-jsx">useEffect(() =&gt; {
    const link = document.createElement(&#39;link&#39;);
    const handleLoadEvent = function () {
      this.media = &#39;all&#39;;
    };
    handleLoadEvent.bind(link);
    link.rel = &#39;stylesheet&#39;;
    link.href = &#39;https://www.example.com/reset.css&#39;;
    link.media = &#39;print&#39;;
    link.onload = handleLoadEvent;

    document.head.appendChild(link);
}, [])
</code></pre>
<p>css를 비동기로 받아오려면 <code>media</code>속성을 활용해야합니다.</p>
<p><code>media</code>를 print로 하면 css를 다운로드는 하지만 실행을 하지 않습니다. 그런후 <code>media</code>를 all로 하면 다운로드가 완료된 후 실행을 시작합니다!</p>
<p>그런데 react에서 왜 저렇게 링크를 만들고 head에 삽입하는식으로 이용했을까요? 바로 <code>onLoad</code> 이벤트 때문입니다.
실제로 link태그에 onLoad이벤트가 존재한다고 typescript는 알려주지만 실제로 <code>lib.d.ts</code>를 읽어보면 onLoad는 오직 <code>이미지</code>에서만 동작합니다.</p>
<p>그래서 실제 html의 <code>onload</code>이벤트를 이용하기 위해서 다음과 같이 실제 element를 만든뒤 처리하였습니다.</p>
<h2 id="마무리">마무리</h2>
<p>이번에는 스크립트 로드 최적화에 대해서 알아봤는데요. 이것도 역시 무작정 사용하는것은 지양하는것이 좋습니다.</p>
<ul>
<li>partytown 같은 경우에는 아직 안되는 스크립트가 조금 존재하는 편입니다.</li>
<li>prefetch, preload등은 미리 불러오고 캐시에 저장해놓는 방식이기 때문에 너무 많은 요청을 미리 불러오는것은 메모리에 부담을 줄 수 있습니다.</li>
<li>async같은 경우 페이지에 독립적인 스크립트에는 사용해도 괜찮지만 페이지의 돔에 종속적인 경우 에러를 뱉을 수도 있습니다. 종속적인 경우 defer를 이용하는게 좋습니다.</li>
<li>비동기로 css를 받아오는 방법은 react의 onLoad이벤트를 이용할수 없기에 real dom의 <code>onload</code> 이벤트를 이용해야 합니다.</li>
<li>비동기로 css를 받아올때는 초반 ui에 영향이 없는 css여야 합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[극한의 프론트엔드 성능최적화 2편 (Image 최적화)]]></title>
            <link>https://velog.io/@baby_dev/%EA%B7%B9%ED%95%9C%EC%9D%98-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%84%B1%EB%8A%A5%EC%B5%9C%EC%A0%81%ED%99%94-2%ED%8E%B8-Image-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@baby_dev/%EA%B7%B9%ED%95%9C%EC%9D%98-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%84%B1%EB%8A%A5%EC%B5%9C%EC%A0%81%ED%99%94-2%ED%8E%B8-Image-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Tue, 02 May 2023 12:14:10 GMT</pubDate>
            <description><![CDATA[<p>이번에 알아 볼 최적화 과정은 image 최적화입니다. 🌄 🌄 🌄</p>
<p>1편에서 말했듯이 간단하게 next/image를 사용하지 않고 cdn과 html의 picture, source 태그와 함께 어떻게 최적화 했는지 알아볼 예정입니다.</p>
<h2 id="cloudinary를-이용한-이미지-최적화">cloudinary를 이용한 이미지 최적화</h2>
<p>사실 next/image에서 이용하는 최적화를 cdn을 통해서 다 처리할 수 있습니다. 이미지 리사이징 ,포맷 변경, lqip이미지 생성, responsive 이미지 생성등 모든 것을 처리할 수 있습니다.</p>
<p>사실 clodinary 사용법이여서 이미지 최적화가 어떤 종류로 할 수 있는지만 보시면 좋을것같습니다.</p>
<h3 id="이미지-리사이징">이미지 리사이징</h3>
<p>이미지 최적화의 가장 중요한 부분이라고 생각하는것이 이미지 리사이징입니당.</p>
<p>말그대로 기존 원본이미지를 렌더링할때의 이미지에 맞춰서 잘라서 받아오는 것입니다. 이미지 용량에서 가장 큰 비중을 차지 하는 것이 이미지의 크기이기 때문에 이 이미지의 사이즈를 줄이는 것이 가장 중요합니다.</p>
<pre><code class="language-jsx">https://&lt;clodinary url&gt;/w_600,h_400/...</code></pre>
<p>이렇게 하면 width 600, height 400의 이미지가 생성됩니다.</p>
<h3 id="포맷-최적화">포맷 최적화</h3>
<p>이미지에는 다양한 포맷이 존재합니다. jpg, png, svg, webp, avif등이 있죠. 최적화되어있지 않은 대부분의 웹사이트는 png, jpg로 되어있을겁니다. </p>
<p>이 이미지 포맷이라는것도 이미지 압축률에 큰 영향을 주기에 차세대 이미지 형식인 webp, avif를 사용하는것이 좋습니다.</p>
<p>하지만 최신 기술이라고 하면 항상 가로막는 장벽이 존재하죠. 바로 <code>브라우저 지원 여부</code> 입니다. webp, avif가 아무리 좋아도 지원을 안하는 브라우저에서는 이용을 할 수 없죠.</p>
<p>그래서 cloudinary에서는 <code>f_auto</code> 옵션을 줄 시 브라우저에 알맞는 format으로 변경해서 줍니다. 최신버전의 chrome 같은 경우 <code>avif</code> 이런것들을 지원하지 않는 safari 브라우저같은 경우에는 <code>jpeg 2</code>로 이미지를 서빙하여 줍니다.</p>
<h3 id="퀄리티-낮추기">퀄리티 낮추기</h3>
<p>다음은 이미지의 퀄리티를 낮추는 방법입니다. 이미지의 퀄리티를 줄이는 것도 이미지의 사이즈를 줄이는것입니다. 사실 이미지의 퀄리티를 낮춘다고 하면 굉장히 찝찝하지만 실제로 퀄리티를 75프로까지 줄여도 사람 눈에는 큰 차이를 느끼지 못한다고 합니다. </p>
<p>개인적으로 추천하는 퀄리티는 <code>80</code> ~ <code>90</code> 정도 입니다.</p>
<h3 id="lqip생성">lqip생성</h3>
<p>다음 최적화 방법은 lqip입니다. 앞의 내용에 비해서 비교적 생소한 방법일 것 같은데요. 바로 <code>low quality image placeholder</code>의 약자입니다.</p>
<p>image의 Skeleton을 단순히 회색으로 보여주는것이 아니라 그 이미지에 블러처리가 들어가고 퀄리티가 매우낮은 이미지를 만들어 내는겁니다. </p>
<p>평균적으로 10배이상 원본이미지 크기보다 줄어들며 원본 이미지를 보여주기 전에 이러한 low quality 이미지를 불러옴으로써 사용자가 빈 이미지를 보는것으로 방지할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/0054a802-0fb7-4fad-b6b9-97f9ed7fb134/image.gif" alt=""></p>
<blockquote>
<p>캐시를 끄고 slow 3g 환경에서 테스트한 영상입니다.</p>
</blockquote>
<hr>
<p>이렇게 최적화한 전후 이미지를 비교해보면 <strong>408kb -&gt; 14.4kb</strong>까지 줄어든것을 볼 수 있습니다. 여기에서 lqip까지 적용된다면 처음에 불러오는 이미지는 오직 <strong>700b</strong>정도 밖에 되지 않습니다. </p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/ecac5340-8161-4360-8ec2-3507887849b1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/baby_dev/post/582e5263-4d8f-478d-8cfa-9a8a97836a13/image.png" alt=""></p>
<h2 id="react에서의-이미지-최적화">react에서의 이미지 최적화</h2>
<p>react에서의 이미지 최적화라고 적었지만 사실 모든 라이브러리에 해당하는 내용일겁니다. html, js만 사용한다면 구현할 수 있는것이기 때문이죠.</p>
<h3 id="lazy-loading">lazy loading</h3>
<p>lazy loading은 이미지 자체를 최적화한다기 보다는 이미지를 불러오는 웹사이트를 최적화하는 기술입니다.</p>
<p>예를 들어 swiper를 통해서 carousel을 구현했을때 20개의 슬라이드에 있는 이미지를 모두 받아올 필요가 있을까요? 
그렇게 된다면 웹사이트가 초기에 다운로드 받는 이미지가 많아지며 그만큼 블로킹이 발생할겁니다.</p>
<p>이것을 막기 위해 현재 보여지는것만 다운로드 받고 그 외의 것들을 나중에 다운로드받는 <code>lazy loading</code>이 필요합니다.</p>
<hr>
<p>lazy loading을 구현하기 위해서 여러가지 방법을 이용할 수 있는데요.</p>
<ul>
<li>lazySizes 같은 외부 라이브러리 이용</li>
<li>browser의 loading=lazy 옵션 이용</li>
<li>intersection observer를 이용한 구현</li>
</ul>
<blockquote>
<p><strong>lazySizes</strong>같은 경우 모든 브라우저에서 지원하게 하며 반응형 이미지도 쉽게 lazy loading을 할 수 있도록 하는 라이브러리입니다. 
<a href="https://github.com/aFarkas/lazysizes">https://github.com/aFarkas/lazysizes</a></p>
</blockquote>
<blockquote>
<p>browser의 <strong>loading=&#39;lazy&#39;</strong> 옵션은 img 태그에 <code>&lt;img loading=&quot;lazy&quot; /&gt;</code> 라고 작성을 하면 브라우저가 저절로 뷰포트에 도달할때 이미지를 다운로드 받습니다.
구현은 편하나 모든 브라우저의 지원을 하지 않기에 polyfill이용이 필요합니다.</p>
</blockquote>
<blockquote>
<p><strong>intersection observer</strong>를 이용하는 방법은 data-src, src를 이용하여 초기에는 스켈레톤 이미지, 뷰포트에 도달할시 data-src에 있는 것을 src로 옮겨 화면에 보여주도록 하는데 체감상 loading=&#39;lazy&#39; 보다 자연스럽지 못합니다.
역시 intersection observer를 지원하지 않는 경우가 있기 때문에 polyfill이 필요합니다.</p>
</blockquote>
<h4 id="주의할점">주의할점</h4>
<p>하지만 주의해야될 부분도 존재하는데요. 바로 초반에 보여야할 이미지에 lazy loading을 걸면 안된다는것입니다. 불필요하게 늦게 로딩이 될수 있기 때문입니다.</p>
<h3 id="fetchpriority">fetchPriority</h3>
<p>lazy loading의 반대되는 개념입니다. 초반에 빨리 보여져야되는 이미지 태그에 사용시 다른 이미지들 보다 다운로드 받는 우선순위를 높여 보다 빠르게 화면에 그려지게 됩니다.</p>
<pre><code>&lt;img fetchPriority=&quot;high&quot; /&gt;</code></pre><p>간편하게 다음과 같이 이용하면 됩니다.</p>
<p>하지만 fetchPriority 같은 경우 typescript에 아직 존재하지 않기에 </p>
<pre><code class="language-tsx">import { AriaAttributes, DOMAttributes } from &#39;react&#39;;

declare module &#39;react&#39; {
  interface HTMLAttributes&lt;T&gt; extends AriaAttributes, DOMAttributes&lt;T&gt; {
    fetchPriority?: &#39;high&#39; | &#39;low&#39; | &#39;auto&#39;;
  }
}
</code></pre>
<p>이렇게 커스텀 <code>d.ts</code> 를 만들어서 typescript를 지원시켜야 합니다.</p>
<h3 id="responsive-image">responsive image</h3>
<p>다음은 responsive image입니다. 주로 웺사이트를 만들때 반응형으로 만들게 될텐데 이럴때 모바일에서도 데스크탑 이미지 정도의 크기를 이용한다면 불필요한 용량을 가지게 됩니다.</p>
<p>그래서 모바일에는 모바일 사이즈에 맞는 이미지, 데스크탑에서는 데스크탑 사이즈에 맞는 이미지를 보여줘야합니다.</p>
<p>이것은 html의 <code>picture</code>, <code>source</code> 태그를 통하여 구현할 수 있습니다.</p>
<pre><code class="language-jsx">  &lt;picture&gt;
    &lt;source media=&quot;(max-width: 760px)&quot; srcSet={mobileUrl} /&gt;
    &lt;source media=&quot;(min-width: 761px)&quot; srcSet={desktopUrl} /&gt;
  &lt;/picture&gt;
</code></pre>
<p>이렇게 이용할 경우에 760px 이하일때는 mobileUrl을 보여주고 761px 이상일때는 desktopUrl을 보여주죠.</p>
<hr>
<p>하지만 이렇게 picture tag와 source tag만을 이용하면 lazy loading이나 fetchPriority는 어떻게 쓰나라고 고민할 수도 있는데요. 그럴때는 밑의 코드 처럼 작성해주시면 됩니다.</p>
<pre><code class="language-jsx">  &lt;picture&gt;
    &lt;source media=&quot;(max-width: 760px)&quot; srcSet={mobileUrl} /&gt;
    &lt;source media=&quot;(min-width: 761px)&quot; srcSet={desktopUrl} /&gt;
      &lt;img alt=&quot;title&quot; loading=&quot;lazy&quot; /&gt;
  &lt;/picture&gt;</code></pre>
<h3 id="lqip-이용하기">lqip 이용하기</h3>
<p>앞에서  lqip image를 만드는 방법을 봤었는데 이번에는 그 lqip 이미지를 실제로 어떻게 이용할지를 알아볼것입니다.</p>
<p>사실 간단하게 img태그를 감싸는 부모태그의 inline style의 background-image에 lqip이미지를 넣어주시면 됩니다.</p>
<pre><code class="language-jsx">    &lt;div
      style={{
        marginRight: &#39;20px&#39;,
        backgroundImage: `url(${blurImageUrl})`, // 여기!
        backgroundSize: `cover`,
        backgroundPosition: &#39;center&#39;,
        backgroundRpeat: &#39;no-repeat&#39;,
      }}
    &gt;
    &lt;/div&gt;</code></pre>
]]></description>
        </item>
    </channel>
</rss>