<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ssoon-m.log</title>
        <link>https://velog.io/</link>
        <description>개발을 재밌게!</description>
        <lastBuildDate>Thu, 04 Jan 2024 13:21:38 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ssoon-m.log</title>
            <url>https://images.velog.io/images/ssoon-m/profile/537a390d-e368-4fc2-8c99-b3772d3edcf3/KakaoTalk_Photo_2022-01-27-07-58-19.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ssoon-m.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ssoon-m" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Next.js app directory의 서버 컴포넌트에서 msw 사용하기]]></title>
            <link>https://velog.io/@ssoon-m/Next.js-app-directory%EC%9D%98-%EC%84%9C%EB%B2%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%97%90%EC%84%9C-msw-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ssoon-m/Next.js-app-directory%EC%9D%98-%EC%84%9C%EB%B2%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%97%90%EC%84%9C-msw-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 04 Jan 2024 13:21:38 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글에선 MSW에 대한 구체적인 설명이나 기본적인 설정 방법에 대해서는 다루지 않습니다.</p>
</blockquote>
<h2 id="시작하며">시작하며</h2>
<p>아직까지(2024.01.04) Next.js 13에서 새롭게 추가된 <strong>App Directory</strong> 기능을 MSW가 지원하지 않고 있는 상황입니다.(<a href="https://github.com/mswjs/msw/issues/1644">깃헙 이슈</a>)
그 이유는 Node.js에서 MSW가 작동하기 위해서는 전역 Node 모듈(http, https 등)을 패치해야 하는데, Next.js의 현재 프로세스 구조상 전역적인 곳에서 패치를 하는것이 아니라 개별 레이아웃마다 패치하기 때문에 어렵다고 합니다.</p>
<p>그럼 <strong>App Directory</strong>에서는 node환경에서 msw를 사용하지 못하는 걸까요?
아닙니다. next프로젝트와 상관이 없는 독립적인 서버를 구축하고 next의 서버사이드측에서 api요청을 보낼때 middleware를 사용하여 요청을 가로채면 위 문제가 해결이 됩니다.</p>
<p><strong>물론 임시적인 해결책이고 Next.js에서 정상적으로 지원을 해주면 방식을 바꿔야합니다.</strong></p>
<h2 id="문제의-코드">문제의 코드</h2>
<p>보통 node와 browser환경에서 msw설정을 하기 위해서 다음과 같은 코드를 작성했을 것입니다.</p>
<pre><code class="language-ts">export async function init() {
  if(typeof window === &#39;undefined&#39;) {
    const { server } = await import(&#39;./server&#39;);
    server.listen();
  } else {
    const { worker } = await import(&#39;./browser&#39;);
    worker.start();
  }
}</code></pre>
<p>위와 같은 방식을 <code>server component</code>에서 사용하게 되면, <code>data fetching</code>이 msw가 활성화되는 시점보다 먼저 발생해서 msw가 동작하지 않습니다.</p>
<h2 id="서버사이드에서-정상적으로-msw동작시키기">서버사이드에서 정상적으로 msw동작시키기</h2>
<p>독립적인 서버(express)를 띄워서 서버사이드에서 일어나는 데이터 패칭 로직을 가로채는 과정을 살펴보겠습니다.</p>
<h3 id="express-서버-구축">express 서버 구축</h3>
<p><code>@mswjs/http-middleware</code>와 <code>express</code> 를 의존성에 추가를 해주고 코드를 다음과 같이 작성해줍니다.</p>
<pre><code class="language-ts">// src/mocks/http.ts
import express from &#39;express&#39;
import { createMiddleware } from &#39;@mswjs/http-middleware&#39;
import { handlers } from &#39;./handlers&#39;

const app = express()
const port = 9090

app.use(express.json())
app.use(createMiddleware(...handlers))

app.listen(port, () =&gt; console.log(`Mock server is running on port: ${port}`))</code></pre>
<blockquote>
<p>참고자료 : <a href="https://github.com/mswjs/msw/issues/1644#issuecomment-1750722052">https://github.com/mswjs/msw/issues/1644#issuecomment-1750722052</a></p>
</blockquote>
<p><code>pacakge.json</code>에 express server실행을 위한 script를 다음과 같이 정의를 해줍니다. </p>
<pre><code>&quot;scripts&quot;: {
    &quot;mock&quot;: &quot;npx tsx watch ./src/mocks/http.ts&quot;,
    &quot;dev&quot;: &quot;next dev&quot;
}</code></pre><p>next의 dev서버를 실행시킬때 mock server실행을 위한 mock script도 같이 실행을 시켜주면 됩니다.</p>
<p>위 코드 구성을 완료했으면 msw에서 정의한 handler를 server component에서 사용할 수 있게 됩니다.</p>
<h3 id="동작과정">동작과정</h3>
<pre><code class="language-tsx">// server component 예시
async function getUser() {
  const response = await fetch(&#39;http://localhost:9090/user&#39;)
  const json = await response.json()

  return json
}

const UserPage = async () =&gt; {
  const user = await getUser()
  console.log(&#39;user&#39;, user)
  return &lt;div&gt;&lt;/div&gt;
}

export default UserPage
</code></pre>
<ol>
<li>위의 server component에서 http요청을 생성하여 <code>http://localhost:9090/user</code>에 보냅니다.</li>
<li>express서버에서 <code>http://localhost:9090</code>에 대한 요청을 수신합니다.</li>
<li><code>@mswjs/http-middleware</code> 라이브러리로 정의된 msw 핸들러가 이 요청을 가로채고 처리합니다. msw는 설정된 핸들러를 통해 요청을 확인하고 mock 데이터를 사용하여 응답을 생성합니다.</li>
<li>msw는 설정된 핸들러(handlers)에서 정의된 mock 데이터 또는 응답을 사용하여 요청에 대한 가짜 응답을 생성합니다. 이 mock 응답이 express 서버로 반환됩니다.</li>
<li>express 서버는 msw로부터 받은 mock 응답을 클라이언트에 반환합니다.</li>
</ol>
<h3 id="baseurl의-문제점">baseURL의 문제점</h3>
<p>하지만, 실제 코드에 적용하기엔 약간 부족한 점이 보입니다.
보통 프론트 개발서버에서는 개발 api서버(<code>https://dev.api/user</code>)를 호출하고 있을 것이고 baseUrl로 설정이 되어있을 것입니다.</p>
<p>next 개발서버를 실행시킬때 mock api서버를 띄워야하는지 판단해주는 환경변수를 주입해서 baseUrl을 localhost로 설정할지에 대한 판단을 해주면 됩니다.</p>
<p>매번 next dev server서버와 express서버를 실행시키기는 귀찮으므로 concurrently라는 라이브러리를 활용해서 next dev server와 express서버를 한 번에 실행을 시키고 next프로젝트에 <code>NEXT_PUBLIC_API_MOCKING</code>이라는 환경변수를 주입해줍니다.</p>
<pre><code>&quot;scripts&quot;: {
    &quot;dev:mock&quot;: &quot;concurrently --kill-others \&quot;NEXT_PUBLIC_API_MOCKING=enable next dev\&quot; \&quot;npx tsx watch ./src/mocks/http.ts\&quot;&quot;,
    //...
}</code></pre><p>그리고 프로젝트에선 다음과 같이 설정해주면 됩니다.</p>
<pre><code class="language-ts">const baseUrl = process.env.NEXT_PUBLIC_API_MOCKING === &#39;enable&#39; ? &#39;http://localhost:9090&#39; : 환경별baseURL판단로직</code></pre>
<h2 id="마치며">마치며</h2>
<p>생각보다 간편한 방법으로 <strong>App Directory</strong>에서 msw를 사용할 수 있지만, express서버를 별도로 띄워야 하므로 불편한 느낌이 조금은 있습니다.
msw측에서 next의 <a href="https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation">instrumentation</a>을 이용하여 해결하는 방법을 제시했는데 빨리 지원이 됐으면 좋겠습니다. 
(관련 pr 입니다. <a href="https://github.com/mswjs/examples-new/pull/7">https://github.com/mswjs/examples-new/pull/7</a>)
혹시 다른 좋은 방법 있으시면 의견 부탁드립니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js를 aws에 서버리스로 배포하는 방법 with sst]]></title>
            <link>https://velog.io/@ssoon-m/Next.js%EB%A5%BC-aws%EC%97%90-%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4%EB%A1%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-with-sst</link>
            <guid>https://velog.io/@ssoon-m/Next.js%EB%A5%BC-aws%EC%97%90-%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4%EB%A1%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-with-sst</guid>
            <pubDate>Tue, 02 Jan 2024 11:01:25 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>next11에서 <a href="https://github.com/serverless-nextjs/serverless-next.js">serverless-next</a>를 이용하여 배포하던 방식을(serverless-next는 현재 관리가 되지 않는 오픈소스여서 next13이후 배포를 지원하지 않습니다.) next14로 버전업을 함에 따라 <a href="https://github.com/sst/sst">SST</a>로 변경을 했습니다.
SST를 이용하여 next.js를 배포하는 방법에 대해 공유하고자 합니다.</p>
</blockquote>
<h2 id="sst란">SST란?</h2>
<p>SST는 <code>Next.js</code>,<code>Svelte</code>,<code>Remix</code>,<code>Astro</code>,<code>Solid</code>와 같은 프레임워크의 배포를 손 쉽게 도와줍니다.</p>
<p>사용되는 AWS의 서비스는 기본적으로 다음과 같습니다.</p>
<ul>
<li>S3 : 클라이언트 자산이 위치합니다.</li>
<li>Cloudfront : 빠른 컨텐츠 전송을 위해 사용이 됩니다.</li>
<li>Lambda : app server와 api기능이 배포 됩니다. (edge플래그가 활성화된 경우 Lambda@Edge에 배포가 됩니다.)</li>
</ul>
<h2 id="sst로-배포하는-방법">SST로 배포하는 방법</h2>
<h3 id="프로젝트에-sst-세팅">프로젝트에 sst 세팅</h3>
<p><code>npx create-sst@latest</code>를 실행후 <code>sst.config.ts</code>파일이 생성이 됐는지 확인을 해줍니다.(drop-in mode)
<img src="https://velog.velcdn.com/images/ssoon-m/post/b7b70a8c-a5da-4d38-ba81-d4f3dadf7188/image.png" alt="">
위 사진처럼 파일이 생성되게 됩니다. <code>npm install</code>을 해주고 sst모듈을 받아주시면 됩니다.(빨간줄도 사라지게 됩니다.)</p>
<pre><code class="language-json">//package.json
&quot;scrips&quot;:{
  &quot;dev&quot;: &quot;sst bind next dev&quot;
  //...
}</code></pre>
<p>dev 명령어가 <code>next dev</code> 에서 <code>sst bind next dev</code> 로 바뀐것도 인지를 해줍니다.
만약 로컬 환경에서 sst가 필요 없다면 <code>next dev</code>로 돌려주시면 됩니다.
(제 생각엔 로컬에서 lambda함수를 디버깅이나 테스트를 할 필요가 없다면 <strong>로컬 환경마다 credential 설정을 해야 하므로</strong> next dev로 개발을 하는게 좋을거 같습니다.)</p>
<h3 id="로컬에서-sst를-실행시키기-위한-iam-credentials-설정">로컬에서 SST를 실행시키기 위한 IAM Credentials 설정</h3>
<p>aws에 배포하기 때문에 SST가 사용할 <code>IAM Credentials</code> 설정을 해줘야 합니다.</p>
<blockquote>
<p>저의 경우 로컬에서는 SST를 이용하지 않아서 로컬에 credential을 설정하는 과정은 생략했습니다.</p>
</blockquote>
<h4 id="로컬에-credential-설정">로컬에 Credential 설정</h4>
<p>일반적으로 credential 파일이 존재한다면 다음 경로에 위치합니다.</p>
<ul>
<li>macOS : <code>~/.aws/credentials</code></li>
<li>windows : <code>C:\Users\USER_NAME\.aws\credentials</code></li>
</ul>
<p>내 컴퓨터에 credential파일이 없는 경우 다음 두가지 과정을 진행해줘야 합니다.</p>
<ol>
<li><a href="https://sst.dev/chapters/create-an-iam-user.html">IAM 사용자 생성 가이드</a>를 이용하여 key 생성후</li>
<li><a href="https://sst.dev/chapters/configure-the-aws-cli.html">AWS CLI를 이용하여 컴퓨터에 credential구성</a></li>
</ol>
<p>위 과정을 마쳤으면 다음과 같은 파일이 생성되어야 합니다.</p>
<pre><code>[default]
aws_access_key_id = &lt;YOUR_ACCESS_KEY_ID&gt;
aws_secret_access_key = &lt;YOUR_SECRET_ACCESS_KEY&gt;</code></pre><p>여러 자격 증명을 구성한 경우</p>
<pre><code>[default]
aws_access_key_id = &lt;DEFAULT_ACCESS_KEY_ID&gt;
aws_secret_access_key = &lt;DEFAULT_SECRET_ACCESS_KEY&gt;

[staging]
aws_access_key_id = &lt;STAGING_ACCESS_KEY_ID&gt;
aws_secret_access_key = &lt;STAGING_SECRET_ACCESS_KEY&gt;

[production]
aws_access_key_id = &lt;PRODUCTION_ACCESS_KEY_ID&gt;
aws_secret_access_key = &lt;PRODUCTION_SECRET_ACCESS_KEY&gt;</code></pre><p>기본적으로 SST는 [defalut] 프로필에 대한 credential을 사용합니다.
위의 staging이나 production중 하나를 사용하려면 <code>AWS_PROFILE</code> 환경 변수를 설정하면 됩니다.</p>
<pre><code class="language-bash">$ AWS_PROFILE=staging npx sst deploy</code></pre>
<p>이제 <code>sst bind next dev</code> 나 <code>npx sst deploy</code>와 같은 명령어를 실행시키면 정상적으로 동작하는걸 확인할 수 있습니다.</p>
<h3 id="github-action으로-배포하기-위한-방법">GitHub Action으로 배포하기 위한 방법</h3>
<p><a href="https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services">OpenID Connect</a>를 통해 AWS에 인증처리를 해줍니다.</p>
<h4 id="1-자격-증명-공급자-추가">1. 자격 증명 공급자 추가</h4>
<p><img src="https://velog.velcdn.com/images/ssoon-m/post/277c036a-9089-457f-a1a1-761f6c1a3829/image.png" alt=""></p>
<p>공급자 추가를 눌러줍니다.</p>
<p><img src="https://velog.velcdn.com/images/ssoon-m/post/ac13f6f4-7a1c-4bed-80ad-27346e04c8d2/image.png" alt=""></p>
<ul>
<li>Provider URL: <code>https://token.actions.githubusercontent.com</code></li>
<li>Audience: <code>sts.amazonaws.com</code></li>
</ul>
<p>위의 설정값을 넣어주고 공급자 추가를 해주면 완료입니다.</p>
<h4 id="2-iam-role-설정">2. IAM Role 설정</h4>
<p><img src="https://velog.velcdn.com/images/ssoon-m/post/8dfb6aed-deae-4354-a9e9-259a374c8cb3/image.png" alt=""></p>
<p>공급자 상세로 들어가 줍니다.</p>
<p><img src="https://velog.velcdn.com/images/ssoon-m/post/b221c23b-efed-4519-9af3-0155233403cf/image.png" alt=""></p>
<p>해당 화면에서 <strong>역할 할당</strong>을 눌러줍니다.</p>
<p><img src="https://velog.velcdn.com/images/ssoon-m/post/673eb4e0-97e9-4778-b1c6-1a9baec281a1/image.png" alt=""></p>
<ul>
<li>Trusted entity type: Web identity</li>
<li>Identity provider: token.actions.githubusercontent.com</li>
<li>Audience: sts.amazonaws.com</li>
<li>GitHub organization: 깃헙 조직이름이나 자신의 깃헙 계정 이름</li>
</ul>
<p>각 칸에 위와 같이 정보들을 제대로 입력을 했다면, 다음으로 넘어가줍니다.</p>
<p><img src="https://velog.velcdn.com/images/ssoon-m/post/1d8b9e3a-0247-4c3b-8a39-4844f2aaae0c/image.png" alt=""></p>
<p><code>AdministratorAccess</code> 정책을 선택 후 다음을 눌러줍니다.</p>
<p><img src="https://velog.velcdn.com/images/ssoon-m/post/eefb9927-0efe-4267-85b3-81ef112d892c/image.png" alt=""></p>
<p>역할 이름을 입력을 해주고 역할 생성을 하면 완료입니다.</p>
<p><img src="https://velog.velcdn.com/images/ssoon-m/post/1ffdc75b-44ea-4eb9-b784-6e49aa96e8ef/image.png" alt=""></p>
<p>생성했던 역할 이름으로 역할 검색을 해주고 <code>ARN</code>을 확인해줍니다.
(<code>arn:aws:iam::?????????:role/Ssoon</code>과 같은 형태입니다.)</p>
<h4 id="3-github-worlfow-구성하기">3. Github Worlfow 구성하기</h4>
<pre><code class="language-yml">name: SST workflow
on: push

# Concurrency group name ensures concurrent workflow runs wait for any in-progress job to finish
concurrency:
  group: merge-${{ github.ref }}

permissions:
  id-token: write # This is required for requesting the JWT
  contents: read # This is required for actions/checkout

jobs:
  DeployApp:
    runs-on: ubuntu-latest
    steps:
      - name: Git clone the repository
        uses: actions/checkout@v3
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::????????:role/Ssoon
          aws-region: us-east-1
      - name: Deploy app
        run: |
          npm i &amp;&amp; npx sst deploy --stage prod
</code></pre>
<p>두가지 부분만 수정을 해주면 됩니다.</p>
<ul>
<li><p>role-to-assume : 위에서 설정한<code>arn:aws:iam::?????????:role/Ssoon</code>를 입력해주면 됩니다.</p>
</li>
<li><p>aws-region : <code>sst.config.ts</code>에 설정한 <code>region</code>을 적어주면 됩니다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/ssoon-m/post/b31e98b4-af31-4647-a961-2677519a9761/image.png" alt=""></p>
<p>모든 과정을 마치고 github action을 실행시켜보면 정상적으로 배포가 되는걸 확인할 수 있습니다.</p>
<h2 id="sst-배포환경별-설정및-사용자-지정-도메인-구성">SST 배포환경별 설정및 사용자 지정 도메인 구성</h2>
<h3 id="배포-환경별-설정">배포 환경별 설정</h3>
<p><code>stage</code> option을 이용하여 production과 staging환경을 나눌수 있습니다.</p>
<pre><code class="language-bash">$ npx sst deploy --stage prod</code></pre>
<p>stage에 전달한 값이 sst config파일에 아래와 같이 전달이 됩니다.</p>
<pre><code class="language-typescript">//sst.config.ts
import { SSTConfig } from &quot;sst&quot;;
import { NextjsSite } from &quot;sst/constructs&quot;;

export default {
  config(_input) {
    return {
      name: _input.stage === &quot;prod&quot; ? &quot;prod-web&quot; : &quot;staging-web&quot;,
      region: &quot;us-east-1&quot;,
    };
  },
  stacks(app) {
    if (![&quot;dev&quot;, &quot;prod&quot;].includes(app.stage)) {
      throw new Error(&quot;Invalid stage&quot;);
    }
    app.stack(function Site({ stack }) {
      const site = new NextjsSite(stack, &quot;site&quot;, {
        customDomain: {
          domainName:
            stack.stage === &quot;prod&quot; ? &quot;naver.com&quot; : &quot;dev.naver.com&quot;,
            hostedZone: &quot;naver.com&quot;,
        },
      });
      stack.addOutputs({
        SiteUrl: site.url,
      });
    });
  },
} satisfies SSTConfig;</code></pre>
<h3 id="route-53을-사용할-경우">Route 53을 사용할 경우</h3>
<p><code>customDomain</code> 부분의 <code>hostedZone</code>은 <code>Route 53</code>의 <a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/hosted-zones-working-with.html">호스팅 영역</a>을 적어주면 됩니다.
<code>domainName</code> 부분에는 말 그대로 도메인명을 적어주면 됩니다. 만약 하위 도메인을 적어주면 sst에서 자동으로 하위 도메인을 생성해서 배포를 진행해줍니다.</p>
<h3 id="외부-호스팅을-사용할-경우">외부 호스팅을 사용할 경우</h3>
<p><strong>제일 간단한 방식은 Route 53으로 이동시키는 것입니다.</strong></p>
<ol>
<li><a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/migrate-dns-domain-in-use.html">사용 중인 도메인을 Route 53으로 DNS 서비스로 설정</a> - 도메인이 현재 많은 트래픽이 있을 경우에 대한 방법</li>
<li><a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/migrate-dns-domain-inactive.html">비활성화된 도메인을 Route 53으로 DNS서비스로 설정</a> - 도메인이 사용되지 않거나 많은 트래픽이 없을 경우에 대한 방법</li>
</ol>
<p><strong>Route 53을 사용하지 않을 경우</strong>
CNAME레코드를 생성하고 이를 CloudFront배포로 지정하면됩니다.
배포를 했을때 자동으로 생성되는 URL인 <code>d1p6z4d11yxxds.cloudfront.net</code>을 가리키면 됩니다.
자세한 내용은 <a href="https://docs.sst.dev/custom-domains#point-the-cname-to-cloudfront">공식문서</a>에 있습니다.</p>
<h2 id="sst의-장점">SST의 장점</h2>
<ul>
<li>vercel보다 저렴합니다.</li>
<li>개발팀이 있을 경우 organization의 저장소를 추가 과금 없이 배포가 가능합니다.</li>
<li>오픈소스 프로젝트이고 디스코드를 통해 소통이 가능합니다.</li>
<li><code>Infrastructure as code</code> 패턴입니다. (<a href="https://sst.dev/chapters/what-is-infrastructure-as-code.html">aws cdk를 이용해서 CloudFormation템플릿으로 변환시켜 배포를 진행합니다. 이 과정을 타입스크립트로 관리합니다.</a>)</li>
</ul>
<h2 id="sst의-단점">SST의 단점</h2>
<ul>
<li>Next의 경우 <code>open-next</code>기반으로 배포 처리가 되는데, <a href="https://open-next.js.org/#features">공식문서</a>를 확인해보면 현재 <strong>스트리밍 기술은 실험적인 단계로 지원</strong>을 하고 있습니다. <a href="https://open-next.js.org/inner_workings/streaming">프로덕션에서는 사용하지 않기를 권장하고 있습니다.</a></li>
<li>vercel보다 비교적 적은 커뮤니티가 형성되어 있습니다.</li>
<li>Next.js에서 제공하는 최신 기술이 나오게 되면 해당 기술을 반영하기 까지 오래 걸릴수도 있습니다.</li>
<li>vercel에 비해 배포하는 과정이 살짝 까다롭습니다.</li>
</ul>
<h2 id="번외">번외</h2>
<p>serverless-next에서 sst로 이관하는 방법에 대해 알아보겠습니다.</p>
<ol>
<li>serverless-next 관련된 패키지를 다 삭제해주고 관련 파일도 삭제를 해줍니다.</li>
<li>기존에 serverless-next를 배포함에 따라 생성된 aws에서의 cloudfront를 삭제를 해줍니다.</li>
<li>Route 53에 들어가서 배포할 호스트에 연결되어 있는 A레코드를 전부 삭제를 해줍니다.</li>
<li>위에서 설명한 내용과 같이 sst에 대한 설정을 전부 마치고 aws에 배포를 해주면 정상적으로 배포가 진행이 되는걸 확인할 수 있습니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[블로그 목차(TOC) 아직도 IntersectionObserver로 만드시나요?]]></title>
            <link>https://velog.io/@ssoon-m/%EB%B8%94%EB%A1%9C%EA%B7%B8-%EB%AA%A9%EC%B0%A8TOC-%EB%A7%8C%EB%93%A4%EA%B8%B0-with-contentlayer-rehype</link>
            <guid>https://velog.io/@ssoon-m/%EB%B8%94%EB%A1%9C%EA%B7%B8-%EB%AA%A9%EC%B0%A8TOC-%EB%A7%8C%EB%93%A4%EA%B8%B0-with-contentlayer-rehype</guid>
            <pubDate>Thu, 09 Nov 2023 08:41:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>contentlayer에서 rehype와 remark를 활용하여 Markdown 파일을 파싱했을때, 그 결과로 목차(TOC)를 scroll event와 intersection observer로 구현하는 방법을 살펴봅니다. 단, 본 글에서는 contentlayer와 rehype,remark 사용법에 대해서는 다루지 않습니다.</p>
</blockquote>
<h2 id="시작하며">시작하며</h2>
<p><a href="https://ianlog.me/">개발 블로그</a>를 만들면서 TOC기능을 어떻게 구현했는지 왜 IntersectionObserver를 안쓰게 됐는지 공유하고자 합니다.</p>
<p>TOC란?
책의 TOC를 보면 본문 내용을 간략하게 알거나, 쉽게 찾아 볼 수 있게 해줍니다.
웹 사이트에서의 TOC도 마찬가지입니다.
보통 개발 블로그에서의 TOC는 클릭시 해당 위치로 바로 스크롤을 해주고, 현재 내가 어느 지점의 글을 읽고 있는지 표시를 해줍니다.</p>
<h2 id="어떤-기능을-구현할-것인가">어떤 기능을 구현할 것인가?</h2>
<p>웹사이트 블로그 글 TOC의 기본이 되는 기능을 구현할 것입니다.(<code>react</code>로 예시코드가 작성되어 있습니다.)</p>
<ol>
<li><p>현재 읽고 있는 글이 어느 부분인지 파악하기 위해 제목 태그와 매칭되는 목차가 생기면 목차에 하이라이팅 처리</p>
</li>
<li><p>TOC를 클릭했을 경우 해당하는 제목 태그로 이동</p>
</li>
</ol>
<h2 id="1-제목-태그에-id값을-달아주기">1. 제목 태그에 id값을 달아주기</h2>
<p>TOC를 클릭 했을때 해당하는 제목 태그 위치로 이동을 시켜야 하므로 html heading에 id를 달아줘야 합니다.
<code>rehype-slug</code>라는 라이브러리를 사용하면 다음과 같이 아주 간단하게 제목 태그에 id를 달아줄 수 있습니다.
<img src="https://velog.velcdn.com/images/ssoon-m/post/41894c6d-0523-40f8-a8a9-ceed1f6f07ab/image.png" alt="heading-id"></p>
<h2 id="2-제목-태그에-id-달아주는-로직-파악하기">2. 제목 태그에 id 달아주는 로직 파악하기</h2>
<p>목차를 클릭했을 때 글 본문의 heading의 id를 찾아서 이동을 해야하므로 <code>rehype-slug</code>가 id를 어떤식으로 생성을 하는지 알아야합니다.
<code>rehype-slug</code> 가 id를 생성하는 방법을 대강 비스무리하게 구현을 하면 목차와 본문heading의 id가 정확히 매칭이 되지 않을수도 있으므로 <code>rehype-slug</code> 라이브러리 코드를 살펴봅시다.</p>
<pre><code class="language-javascript">import GithubSlugger from &#39;github-slugger&#39;
import {headingRank} from &#39;hast-util-heading-rank&#39;
import {toString} from &#39;hast-util-to-string&#39;
import {visit} from &#39;unist-util-visit&#39;

/** @type {Options} */
const emptyOptions = {}
const slugs = new GithubSlugger()

/**
 * Add `id`s to headings.
 *
 * @param {Options | null | undefined} [options]
 *   Configuration (optional).
 * @returns
 *   Transform.
 */
export default function rehypeSlug(options) {
  const settings = options || emptyOptions
  const prefix = settings.prefix || &#39;&#39;

  /**
   * @param {Root} tree
   *   Tree.
   * @returns {undefined}
   *   Nothing.
   */
  return function (tree) {
    slugs.reset()

    visit(tree, &#39;element&#39;, function (node) {
      if (headingRank(node) &amp;&amp; !node.properties.id) {
        node.properties.id = prefix + slugs.slug(toString(node))
      }
    })
  }
}</code></pre>
<p>살펴보니 내부적으로 <code>github-slugger</code>를 사용하는걸 확인할 수 있습니다.
<code>GithubSlugger</code> 인스턴스를 만들어서 별다른 옵션이 없을 경우 <code>node</code> 의 <code>id</code> 속성에 <code>slug</code>를 생성해서 넣어주고 있습니다.
TOC를 만들때 <code>github-slugger</code>를 이용하여 id값을 생성하면 될 거 같습니다.</p>
<h2 id="3-본문글에서-toc를-위한-데이터를-추출하기">3. 본문글에서 TOC를 위한 데이터를 추출하기</h2>
<p>전체적인 TOC추출 코드의 모습입니다.</p>
<pre><code class="language-typescript">
export const parseHeadersForTOC = (raw: string) =&gt; {
  const calculateHeaderLevels = (arr: Array&lt;number&gt;) =&gt; {
    const sorted = [...arr].sort((a, b) =&gt; a - b);
    const min = sorted[0];
    const adjusted = arr.map((value) =&gt; value - min + 1);
    return adjusted;
  };

  const regex = /\n(?&lt;flag&gt;#{1,3})\s+(?&lt;text&gt;.+)/g;
  const headerMatches = Array.from(raw.matchAll(regex));

  const headerLevels = calculateHeaderLevels(
    headerMatches.map((match) =&gt; match.groups?.flag.length!),
  ) as Array&lt;1 | 2 | 3&gt;;

  const slugger = new GithubSlugger();

  const headers: Toc[] = headerMatches.map((header, i) =&gt; {
    const { text } = header.groups || { text: &#39;&#39; };
    const slug = slugger.slug(text);
    return { level: headerLevels[i], text, slug };
  });
  return headers;
};</code></pre>
<p>raw파라미터에는 아래 형식과 같은 문자열이 들어오게 됩니다.</p>
<pre><code># toc
toc level1 입니다.
## toc
toc level2 입니다.
### toc
toc level3 입니다.
</code></pre><p>&quot;#&quot; 의 개수 n에 따라 heading{n} 이 결정이 됩니다.</p>
<p>정규식(<code>/\n(?&lt;flag&gt;#{1,3})\s+(?&lt;text&gt;.+)/g</code>)을 통해서 제목 태그의 level과 제목 태그의 텍스트를 그룹핑합니다.</p>
<blockquote>
<p>각 제목 태그가 개행 문자로 시작하므로 <code>\n</code>을 넣어줍니다.
<code>(?&lt;flag&gt;#{1,3})</code>는 flag라는 이름의 그룹을 정의합니다. 이 그룹은 # 문자 1개에서 3개까지 연속으로 나타나는 것을 의미합니다. 즉, #, ##, 또는 ###와 일치하는 것을 찾습니다.
<code>\s+</code>는 하나 이상의 공백 문자를 나타냅니다. 이 공백은 #과 제목 텍스트를 구분합니다.
<code>(?&lt;text&gt;.+)</code>는 text라는 이름의 그룹을 정의합니다. 이 그룹은 하나 이상의 문자(.+)를 의미하며, 제목의 실제 텍스트를 찾습니다.
<code>/g</code>는 전역 검색을 활성화하는 플래그로, 문자열 내에서 모든 일치 항목을 찾아야 함을 나타냅니다.</p>
</blockquote>
<p>matchAll을 통해 일치하는 패턴들을 다 찾아주고 배열로 만들어 줍니다.</p>
<pre><code class="language-typescript">const headerMatches = Array.from(raw.matchAll(regex));
console.log(&#39;headerMatches&#39;, headerMatches);
// headerMatches [
  //   [
  //     &#39;\n# toc&#39;,
  //     &#39;#&#39;,
  //     &#39;toc&#39;,
  //     index: 0,
  //     input: &#39;\n# toc\ntoc level1 입니다.\n## toc\ntoc level2 입니다.\n### toc\ntoc level3 입니다.\n\n&#39;,
  //     groups: [Object: null prototype] { flag: &#39;#&#39;, text: &#39;toc&#39; }
  //   ],
  //   [
  //     &#39;\n## toc&#39;,
  //     &#39;##&#39;,
  //     &#39;toc&#39;,
  //     index: 22,
  //     input: &#39;\n# toc\ntoc level1 입니다.\n## toc\ntoc level2 입니다.\n### toc\ntoc level3 입니다.\n\n&#39;,
  //     groups: [Object: null prototype] { flag: &#39;##&#39;, text: &#39;toc&#39; }
  //   ],
  //   [
  //     &#39;\n### toc&#39;,
  //     &#39;###&#39;,
  //     &#39;toc&#39;,
  //     index: 45,
  //     input: &#39;\n# toc\ntoc level1 입니다.\n## toc\ntoc level2 입니다.\n### toc\ntoc level3 입니다.\n\n&#39;,
  //     groups: [Object: null prototype] { flag: &#39;###&#39;, text: &#39;toc&#39; }
  //   ]
  // ]</code></pre>
<p><code>calculateHeaderLevels</code> 함수는 제목 태그의 수준을 계산합니다.</p>
<p>입력으로 level이 담긴 배열을 받아 해당 배열의 각 요소 간의 차이를 계산하여 최소 level을 1로 조정한 배열을 반환합니다.(예를들어 제목태그에 heading2 밖에 존재하지 않을 경우 level이 1로 계산이 되고, heading1과 headgin2가 존재할 경우 heading1의 level이 1로 계산이 됩니다.)</p>
<pre><code class="language-typescript">const headerLevels = calculateHeaderLevels(
    headerMatches.map((match) =&gt; match.groups?.flag.length!),
) as Array&lt;1 | 2 | 3&gt;;</code></pre>
<p><code>headerMathces</code> 배열을 map함수를 이용하여 <code>level,text,slug</code> 속성을 가진 배열을 반환해줍니다. slug는 아까 위에서 받은 <code>GithubSlugger</code> 를 통해서 생성을 해주면 됩니다.</p>
<pre><code class="language-typescript">  const slugger = new GithubSlugger();

  const headers: Toc[] = headerMatches.map((header, i) =&gt; {
    const { text } = header.groups || { text: &#39;&#39; };
    const slug = slugger.slug(text);
    return { level: headerLevels[i], text, slug };
  });
  return headers;
</code></pre>
<p><code>parseHeadersForTOC</code> 함수의 최종 결과입니다.</p>
<pre><code>[
  { level: 1, text: &#39;toc&#39;, slug: &#39;toc&#39; },
  { level: 2, text: &#39;toc&#39;, slug: &#39;toc-1&#39; },
  { level: 3, text: &#39;toc&#39;, slug: &#39;toc-2&#39; }
]</code></pre><h2 id="4-제목-태그-감지하기">4. 제목 태그 감지하기</h2>
<p>글을 스크롤하며 읽을 때 제목 태그를 자동 감지하여 TOC를 자동으로 활성화하는 기능을 구현해야 합니다.
위 기능을 구현하는 방법은 두가지 정도가 있습니다.</p>
<ol>
<li>intersection observer를 이용하여 제목 태그가 감지되면 TOC활성화</li>
<li>scroll event를 이용하여 특정 높이에 위치하면 TOC활성화</li>
</ol>
<blockquote>
<p>저는 1번 방법으로 구현을 했다가 2번 방법으로 변경했습니다.
1번 방법은 <strong>고속 스크롤 상황</strong>에서 요소를 제대로 감지하지 못하는 이슈가 있었습니다.
그래서 생각이 난 방법은 scroll event로 처리를 하는 방법이였습니다.
<code>velog</code>의 toc는 감지가 잘 된다고 느껴서 코드를 참고 해보니 scroll event를 사용하고 있었습니다.</p>
</blockquote>
<p>intersection observer, scroll event를 이용한 코드를 각각 살펴보겠습니다. (스타일링 코드는 제외 했습니다.)</p>
<h3 id="intersection-observer로-구현">intersection observer로 구현</h3>
<p>intersection observer를 이용했을 때 느낀 최대 장점은 코드가 간결해서 이해하기가 쉽다는 점이였습니다. 
또한, 요소의 상태 변경을 관찰할 때 비동기 콜백으로 동작하기 때문에 scroll event보다 성능상의 이점도 챙길 수 있습니다.
단점은 <a href="https://stackoverflow.com/questions/61951380/intersection-observer-fails-sometimes-when-i-scroll-fast">고속 스크롤 상황에서 intersection observer</a>가 제대로 동작하지 않아서 TOC하이라이팅이 제대로 되지 않는 이슈 입니다.</p>
<p><img src="https://velog.velcdn.com/images/ssoon-m/post/33e96c52-8c97-4670-aac6-bb6138160311/image.gif" alt="intersection-observer-toc"></p>
<p>아래 코드는 블로그 글 안의 h1,h2,h3태그를 다 가져와서 관찰할 요소로 지정을 해주고 감지가 될 경우 해당 요소의 id를 <code>activeToc</code> 에 세팅을 해줍니다.
<code>activeToc</code> 와 일치하는 toc의 slug에 스타일링을 해주면 끝입니다. (뎁스에 따른 스타일 처리는 여기서 
구현하지 않겠습니다.)</p>
<pre><code class="language-tsx">import { type Toc } from &#39;@/lib/types/toc-type&#39;;
import Link from &#39;next/link&#39;;
import { useEffect, useRef, useState } from &#39;react&#39;;

interface TocSideProps {
  tableOfContents: Toc[];
}

const TocSide = ({ tableOfContents }: TocSideProps) =&gt; {
  const observer = useRef&lt;IntersectionObserver&gt;();
  const [activeToc, setActiveToc] = useState(&#39;&#39;);

  useEffect(() =&gt; {
    observer.current = new IntersectionObserver(
      (entries) =&gt; {
        entries.forEach((entry) =&gt; {
          if (!entry.isIntersecting) return;
          setActiveToc(entry.target.id);
        });
      },
      {
        rootMargin: &#39;0px 0px -95% 0px&#39;,
        threshold: 1.0,
      },
    );
    const headingElements = document.querySelectorAll(&#39;.prose h1,h2,h3&#39;);
    headingElements.forEach((element) =&gt; observer.current?.observe(element));
    return () =&gt; observer.current?.disconnect();
  }, []);

  return (
    &lt;&gt;
      {tableOfContents.length ? (
        &lt;ul&gt;
          &lt;div&gt;
            목차
          &lt;/div&gt;
          {tableOfContents.map((toc, i) =&gt; (
            &lt;li
              data-level={numberToStringMap[toc.level]}
              key={i}
              className={`${activeToc === toc.slug ? &#39;active&#39; : &#39;&#39;}`}
            &gt;
              &lt;Link href={`#${toc.slug}`}&gt;{toc.text}&lt;/Link&gt;
            &lt;/li&gt;
          ))}
        &lt;/ul&gt;
      ) : null}
    &lt;/&gt;
  );
};

export default TocSide;</code></pre>
<h3 id="scroll-event로-구현">scroll event로 구현</h3>
<p>intersection observer에 비해 코드가 간결하지 않습니다.
하지만, 동작은 intersection observer에 비해 정교해졌습니다.</p>
<p><img src="https://velog.velcdn.com/images/ssoon-m/post/57ae5bf5-5536-4297-ba84-1f5f68592e04/image.gif" alt=""></p>
<p><code>tableOfContents</code>를 map함수를 이용하여 <code>doucment</code> 에서 각 slug별로 해당하는 heading id를 찾고 찾은 요소는 현재 viewport에서 얼마나 떨어져 있는지(<code>el.getBoundingClientRect().top</code>)와 현재 document가 스크롤된 위치(<code>scrollTop</code>)을 더해줍니다. (<code>el.getBoundingClientRect().top</code>은 요소와 viewport사이의 상대적인 값을 가져오므로 만약 document를 100만큼 스크롤을 했으면 값이 -100이 되기 때문에 <code>scrollTop</code>은 꼭 더해줘야 합니다.)</p>
<p>블로그 게시글의 제목 태그가 어느정도로 스크롤 했을때 어떤 TOC요소와 매칭되는지에 대한 값을 구했으므로 scroll event를 통해 비교를 해주면서 하이라이팅 처리를 해줍니다.</p>
<p>그리고 <code>trackScrollHeight</code> 라는 메서드를 이용해서 <code>0.25</code>초 마다 주기적으로 이전 <code>scrollHeight</code>와 현재 <code>scrollHeight</code>가 같지 않을 경우 <code>settingHeadingTops</code>을 호출해주면서 <strong>제목 태그의 top 값을 재조정 해줍니다.</strong> (화면의 높이가 변경된 경우 재조정을 해주기 위함)</p>
<p>intersection observer와 마찬가지로 이제 activeToc 와 일치하는 toc의 slug에 스타일링을 해주면 끝입니다. </p>
<pre><code class="language-tsx">import { type Toc } from &#39;@/lib/types/toc-type&#39;;
import Link from &#39;next/link&#39;;
import { useCallback, useEffect, useState } from &#39;react&#39;;

const numberToStringMap = {
  1: &#39;one&#39;,
  2: &#39;two&#39;,
  3: &#39;three&#39;,
};

const getScrollTop = () =&gt; {
  if (!document.body) return 0;
  if (document.documentElement &amp;&amp; &#39;scrollTop&#39; in document.documentElement) {
    return document.documentElement.scrollTop || document.body.scrollTop;
  } else {
    return document.body.scrollTop;
  }
};

interface IHeadingTops {
  slug: string;
  top: number;
}

interface TocSideProps {
  tableOfContents: Toc[];
}

const TocSide = ({ tableOfContents }: TocSideProps) =&gt; {
   const [activeToc, setActiveToc] = useState(&#39;&#39;);
  const [headingTops, setHeadingTops] = useState&lt;null | IHeadingTops[]&gt;([]);

  const settingHeadingTops = useCallback(() =&gt; {
    const scrollTop = getScrollTop();
    const headingTops = tableOfContents.map(({ slug }) =&gt; {
      const el = document.getElementById(slug);
      const top = el ? el.getBoundingClientRect().top + scrollTop : 0;
      return { slug, top };
    });
    setHeadingTops(headingTops);
  }, [tableOfContents]);

  useEffect(() =&gt; {
    settingHeadingTops();
    let prevScrollHeight = document.body.scrollHeight;
    let timeoutId: ReturnType&lt;typeof setTimeout&gt; | null = null;

    const trackScrollHeight = () =&gt; {
      const scrollHeight = document.body.scrollHeight;
      if (prevScrollHeight !== scrollHeight) {
        settingHeadingTops();
      }
      prevScrollHeight = scrollHeight;
      timeoutId = setTimeout(trackScrollHeight, 250);
    };

    timeoutId = setTimeout(trackScrollHeight, 250);

    return () =&gt; {
      timeoutId &amp;&amp; clearTimeout(timeoutId);
    };
  }, [settingHeadingTops]);

  useEffect(() =&gt; {
    const onScroll = () =&gt; {
      const scrollTop = getScrollTop();
      if (!headingTops) return;
      const currentHeading = headingTops
        .slice()
        .reverse()
        .find((headingTop) =&gt; scrollTop &gt;= headingTop.top - 4);

      if (currentHeading) {
        setActiveToc(currentHeading.slug);
      } else {
        setActiveToc(&#39;&#39;);
      }
    };
    onScroll();
    window.addEventListener(&#39;scroll&#39;, onScroll);
    return () =&gt; {
      window.removeEventListener(&#39;scroll&#39;, onScroll);
    };
  }, [headingTops]);

  return (
    &lt;&gt;
      {tableOfContents.length ? (
        &lt;ul&gt;
          &lt;div&gt;목차&lt;/div&gt;
          {tableOfContents.map((toc, i) =&gt; (
            &lt;li
              data-level={numberToStringMap[toc.level]}
              key={i}
              className={`${activeToc === toc.slug ? &#39;active&#39; : &#39;&#39;}`}
            &gt;
              &lt;Link href={`#${toc.slug}`}&gt;{toc.text}&lt;/Link&gt;
            &lt;/li&gt;
          ))}
        &lt;/ul&gt;
      ) : null}
    &lt;/&gt;
  );
};

export default TocSide;</code></pre>
<h2 id="마치며">마치며</h2>
<p>성능상 이점을 챙기면서 빠르게 TOC를 구현하고 싶으면 intersection observer를 사용하고 시간을 더 들이더라도 ux에 신경을 쓸거면 scroll event를 사용하면 될 거 같습니다.
intersection observer를 이용하든 scroll event를 이용하든 뭐가 더 좋다고 단정은 지을수 없을거 같습니다.
상황에 맞는 기술을 적용하는게 좋은 개발자가 되는 길이라고 생각합니다.
읽어주셔서 감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js SSG에서 이미지 최적화 방법]]></title>
            <link>https://velog.io/@ssoon-m/Next.js-SSG%EC%97%90%EC%84%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@ssoon-m/Next.js-SSG%EC%97%90%EC%84%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Wed, 08 Nov 2023 13:00:25 GMT</pubDate>
            <description><![CDATA[<h2 id="시작하며">시작하며</h2>
<p>Next.js의 <code>static export</code> 옵션을 활성화하면, <code>&lt;Image&gt;</code> 컴포넌트의 이미지 최적화 기능을 제대로 활용할 수 없습니다.
이는 Next.js가 기본적으로 <a href="https://nextjs.org/learn-pages-router/seo/improve/images">on-demand</a> 형식으로 서버에서 이미지 최적화를 수행하기 때문입니다.
이 글에서는 <code>static export</code>옵션 활성화시 이미지 최적화 기능을 어떻게 적용할지에 대해 다루고자 합니다.</p>
<h2 id="static-export시-이미지-최적화-방법">static export시 이미지 최적화 방법</h2>
<p>Next.js SSG에서 이미지 최적화는 꽤 오래전부터 많은 <a href="https://github.com/vercel/next.js/discussions/19065">논의</a>가 있었습니다.</p>
<p>아래 두가지 방법에 대한 내용입니다.</p>
<ol>
<li>빌드 타임시 이미지 최적화</li>
<li>이미지 관리 및 최적화 서비스를 해주는 클라우드 서비스를 이용</li>
</ol>
<p>1번의 경우엔 아직까지는 Next.js에서 공식적으로 제공할 생각이 없어 보였습니다.
(빌드 타임시 이미지 최적화는 <a href="https://github.com/Niels-IO/next-image-export-optimizer">next-image-export-optimizer</a>와 같은 라이브러리를 이용하거나 직접 구현해야 합니다.)</p>
<p>2번은 Next.js의 <a href="https://nextjs.org/docs/app/building-your-application/deploying/static-exports#image-optimization">공식문서</a>에서 이미지 최적화에 <code>Cloudinary</code>를 권장하고 있기 때문에 2번 방법으로 최적화를 하고자 합니다.
(물론 2번 방법을 <code>Cloudinary</code>로만 구현이 가능한건 아닙니다. <a href="https://aws.amazon.com/ko/blogs/networking-and-content-delivery/image-optimization-using-amazon-cloudfront-and-aws-lambda/">CloudFront 및 AWS Lambda</a>를 이용한 방법도 있습니다.)</p>
<h2 id="cloudinary란">Cloudinary란?</h2>
<p><a href="https://cloudinary.com/">Cloudinary</a>는 이미지 및 비디오 관리 및 최적화 서비스를 제공하는 클라우드 기반 플랫폼입니다.<br>이 서비스를 통해 사용자는 웹 페이지의 이미지 및 비디오 관리를 간편하게 할 수 있으며, 성능 최적화와 빠른 로딩을 실현할 수 있습니다. 
Cloudinary는 이미지 변환, 크롭, 리사이즈, 효과 및 다운로드 및 전송 관리 등 다양한 이미지 관리 작업을 지원합니다.</p>
<blockquote>
<p>그리고 제일 중요한(?) <strong>요금제 정책</strong>을 살펴보겠습니다.
<img src="https://velog.velcdn.com/images/ssoon-m/post/1b8bd7f4-8077-4d50-8e87-a9054964e1fd/image.png" alt="">
무료플랜 기준으로 한 달에 25 크레딧을 제공해준다고 합니다. (1크레딧 = 1000번의 파일변환, 1GB의 저장용량, 1GB의 대역폭)
개인 블로그와 같은 SSG사이트를 운영할때는 무료플랜으로 해도 문제가 없어 보입니다.</p>
</blockquote>
<h2 id="cloudinary-사용법">Cloudinary 사용법</h2>
<p>우선 Cloudinary 에 로그인을 해줘야 합니다.
그 후 Cloudinary에 사진을 업로드하고 사진의 url을 프로젝트에서 이용하면 끝입니다.</p>
<p>업로드를 하는 방법은 매우 간단합니다.</p>
<ol>
<li><code>Digital Asset Management</code> 탭으로 이동</li>
<li>우측 상단의 Upload버튼을 누르고 원하는 이미지를 업로드
<img src="https://velog.velcdn.com/images/ssoon-m/post/2cbc67d7-efec-4988-b221-0af02b207f53/image.png" alt=""></li>
</ol>
<p>위 과정을 마쳤다면, Next.js 프로젝트의 config에 loader 설정을 해줘야 합니다.
(<code>static export</code> 옵션이 있다면, <code>unoptimized</code>나 <code>loader</code> 둘 중에 하나를 무조건 설정해줘야 합니다.)</p>
<pre><code class="language-javascript">// next.config.js

/** @type {import(&#39;next&#39;).NextConfig} */
const nextConfig = {
  output: &#39;export&#39;,
  images: {
    loader: &#39;custom&#39;,
    loaderFile: &#39;./my-loader.ts&#39;,
  },
}

module.exports = nextConfig
</code></pre>
<p>이 로더는 원격 소스에서 이미지를 가져오는 방법을 정의합니다. Cloudinary에 대한 URL을 구성해줍니다.</p>
<pre><code class="language-typescript">export default function cloudinaryLoader({
  src,
  width,
  quality,
}: {
  src: string
  width: number
  quality?: number
}) {
  const params = [&#39;f_auto&#39;, &#39;c_limit&#39;, `w_${width}`, `q_${quality || &#39;auto&#39;}`]
  return `https://res.cloudinary.com/demo/image/upload/${params.join(
    &#39;,&#39;
  )}${src}`
}</code></pre>
<p>loader 설정까지 완료가 됐다면, Next.js Image 컴포넌트를 다음과 같이 구성을 했을 때</p>
<pre><code class="language-tsx">&lt;Image src=&#39;/demo&#39; width={50} height={50}/&gt;</code></pre>
<p>image src가 <code>cloudinaryLoader</code> 의 return 값으로 바뀌게 됩니다.</p>
<p>loader덕분에 기존 SSG프로젝트의 소스코드를 건드리지 않고 설정만 바꿔서 적용이 가능합니다.</p>
<h2 id="cloudinary-업로드-자동화-하기">Cloudinary 업로드 자동화 하기</h2>
<p>SSG 프로젝트를 만들면서 필요한 이미지들을 일일이 Cloudinary에 업로드 시키는건 상상만해도 생산성이 많이 떨어지는 작업입니다.
따라서 <a href="https://cloudinary.com/documentation/node_quickstart">Cloudinary Node.js SDK</a>를 이용하여 이미지 업로드를 자동화를 하고자 합니다.</p>
<blockquote>
<p>이루고자 하는 기능은 다음과 같습니다.</p>
</blockquote>
<ol>
<li><code>public</code> 폴더 아래의 이미지들을 전부 Cloudinary에 업로드</li>
<li>기존에 Next.js에서 정적 assets 들을 불러오는 방식에 영향을 주어선 안된다. (예를들면, <code>public/my-profile.png</code> 파일을 불러올때 <code>/my-profile.png</code> 로 파일 참조)</li>
<li>script를 한 번 실행시켜서 이미지 업로드가 동작</li>
</ol>
<h3 id="cloudinary-설정-확인">cloudinary 설정 확인</h3>
<p>Cloudinary로그인시 보이는 화면입니다.
Node.js에서 어떤식으로 설정을 해야하는지 예시가 나옵니다.
<img src="https://velog.velcdn.com/images/ssoon-m/post/e3d112e5-948c-47d4-9853-6a0681204fdf/image.png" alt=""></p>
<p>만약 cloud_name을 바꾸고 싶다면 <code>setting -&gt; account</code> 에 들어간 후 edit을 눌러서 수정을 해주면 됩니다.
<img src="https://velog.velcdn.com/images/ssoon-m/post/ab186ed5-2603-4e83-a9d0-7bec6e739c8a/image.png" alt=""></p>
<h3 id="script-환경-세팅">script 환경 세팅</h3>
<p>cloudinary와 glob을 이용해줍니다.</p>
<blockquote>
<p>glob 라이브러리를 사용하면 패턴 매칭을 통해 파일 및 디렉토리를 검색할 수 있습니다.</p>
</blockquote>
<pre><code class="language-shell">$ yarn add -D cloudinary
$ yarn add -D glob</code></pre>
<p>저는 <code>api_key</code>와 <code>api_secret</code>의 값을 <code>env</code>로 관리할 예정이므로 <code>dotenv</code>도 추가로 받아줬습니다.</p>
<p>scripts폴더 아래에 <code>upload-cloudinary.ts</code> 파일을 생성해서 cloudinary 설정 관련 코드를 작성해줬습니다.</p>
<pre><code class="language-typescript">// scripts/upload-cloudinary.ts
import { v2 as cloudinary } from &#39;cloudinary&#39;;
import { glob } from &#39;glob&#39;;
import dotenv from &#39;dotenv&#39;;

dotenv.config();

cloudinary.config({
  cloud_name: &#39;ssoon&#39;,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
  secure: true, // Return &quot;https&quot; URLs by setting secure: true
});

// Log the configuration
console.log(cloudinary.config());
</code></pre>
<p><code>upload-cloudinary.ts</code> 스크립트를 실행시키기 위해 <code>package.json</code>의 scripts에 작성을 해줍니다.
(ts파일로 작성을 해서 <code>ts-node</code> 로 실행을 시켰습니다. js파일로 작성하고 <code>node ./scripts/upload-cloudinary.js</code>로 실행시켜도 무관합니다. )</p>
<pre><code class="language-javascript">
 &quot;scripts&quot;: {
    &quot;upload-cloudinary&quot;: &quot;ts-node --project tsconfig.node.json ./scripts/upload-cloudinary.ts&quot;
  },</code></pre>
<p><code>upload-cloudinary</code> 스크립트를 실행 시킨후에 위에서 설정한 config가 제대로 로그에 다음과 같이 찍히는지 확인을 해줍니다.</p>
<pre><code class="language-javascript">{
  cloud_name: &#39;ssoon&#39;,
  api_key: &#39;*********&#39;,
  api_secret: &#39;*********&#39;,
  secure: true
}</code></pre>
<p>설정을 완료 했으므로 이미지 업로드 관련 로직을 작성해줍니다.
<a href="https://cloudinary.com/documentation/node_quickstart#2_upload_an_image">업로드 관련 문서</a>를 확인해 보면 <code>cloudinary.uploader.upload</code> 메서드를 사용하는걸 확인할 수 있습니다.</p>
<p>upload에 필요한 각 옵션을 알아보겠습니다.</p>
<ul>
<li><code>use_filename</code> : 업로드된 파일의 원래 이름을 공개 ID로 사용하도록 해줍니다. 만약 <code>true</code> 로 설정하지 않는다면, <code>8jsb1xofxdqamu2rzwt9q</code> 와 같은 무작위 ID가 생성이 되므로 무조건 <code>true</code>로 설정을 해야 합니다.</li>
<li><code>unique_filename</code> : 옵션은 공개 ID에 임의의 문자를 적용해줍니다. <code>false</code>로 설정해야 임의의 문자를 적용하지 않습니다.</li>
<li><code>overwrite</code> : 업로드 시 동일한 공개 ID를 가진 이미지를 덮어씁니다.</li>
</ul>
<p>upload문서 예시에는 없지만 저는 추가적으로 <code>folder</code> 옵션도 추가를 해줬습니다.</p>
<blockquote>
<p><code>folder</code> 옵션이 없다면, 내가 원하는 뎁스에 이미지를 저장하지 못합니다.
Next.js프로젝트에서 <code>public/logo/site-og-image.png</code> 의 이미지 파일을 <code>folder</code> 옵션 없이 Cloudinary에 업로드를 한다면 무조건 root경로에 업로드가 됩니다. (<code>https://res.cloudinary.com/{cloud_name}/image/upload/site-og-image.png</code> 경로에 업로드가 됩니다.)
Cloudinary를 적용하기전 프로젝트에선 <code>&lt;Image src=&#39;logo/site-og-image.png&#39;/&gt;</code> 와 같은 방식으로 호출을 하고 있었을텐데 만약 <code>~~~/upload/site-og-image.png</code> 경로에 업로드가 됐다면 기존 소스코드를 <code>&lt;Image src=&#39;/site-og-image.png&#39;/&gt;</code> 로 바꿔줘야 합니다. (cloudinaryLoader를 설정 했으므로 <code>~~~/upload/</code> 부분은 적어줄 필요가 없습니다.) <code>folder</code> 옵션에 &quot;logo&quot; 를 적어주게 된다면 <code>~~~/upload/logo/site-og-image.png</code> 경로에 이미지가 업로드가 됩니다.</p>
</blockquote>
<p>아래와 같이 옵션을 구성해서 uploadImage 함수를 작성해 줍니다.</p>
<pre><code class="language-typescript">// scripts/upload-cloudinary.ts
const uploadImage = async ({
  imagePath,
  folder,
}: {
  imagePath: string;
  folder: string;
}) =&gt; {
  const options = {
    use_filename: true, // Use the uploaded file&#39;s name as the asset&#39;s public ID and
    unique_filename: false,
    overwrite: true, // allow overwriting the asset with new versions
    folder,
  };

  // Upload the image
  const result = await cloudinary.uploader.upload(imagePath, options);
  return result.public_id;
};</code></pre>
<p>glob으로 public 폴더 아래의 <code>.jpg, .jpeg, .png, .gif</code> 등의 확장자가 있는 경로를 패턴매칭으로 검색하고 일치하는 파일 경로를 반환 받습니다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/ssoon-m/post/c0792934-0da0-4c50-aecb-26cf9c300cdc/image.png" alt="">
예를 들어 폴더구조가 위와 같이 구성되어 있으면
<code>[&#39;public/posts/blog/example/example-1.png&#39;,&#39;public/posts/blog/example/example-2.png&#39;,&#39;public/static/my-profile.webp&#39;]</code>
위 목록을 반환받을 수 있게 패턴을 작성해줍니다.</p>
</blockquote>
<p>glob을 이용해서 반환받은 파일 경로는 <code>uploadImage</code> 함수의 <code>imagepath</code> 파라미터에 그대로 넣어줍니다.
그리고 <code>folder</code> 의 경로를 public을 제외하고 넣어줄 수 있게 로직을 작성해줍니다.
(public을 제외하는 이유는 위에서 계속 설명을 했듯이 Next.js 프로젝트 내부에서 이미지를 사용할때 public을 붙이고 가져오지 않기 때문입니다.)</p>
<pre><code class="language-typescript">// scripts/upload-cloudinary.ts
(async () =&gt; {
  const blogImgPaths = await glob([&#39;public/**/*.{jpg,jpeg,png,gif,webp,svg}&#39;]);

  if (blogImgPaths.length) {
    const uploadPromises = blogImgPaths.map(async (imgPath) =&gt; {
      const folder = imgPath
        .replace(&#39;public/&#39;, &#39;&#39;)
        .replace(/\/[^/]+\.(jpg|jpeg|png|gif|webp|svg)$/, &#39;&#39;);
      return uploadImage({
        imagePath: imgPath,
        folder,
      });
    });
    try {
      const result = await Promise.all(uploadPromises);
      console.log(&#39;result&#39;, result);
    } catch (e: any) {
      throw new Error(e);
    }
  }
})();</code></pre>
<p>최종 코드의 모습입니다.</p>
<pre><code class="language-typescript">// scripts/upload-cloudinary.ts
import { v2 as cloudinary } from &#39;cloudinary&#39;;
import { glob } from &#39;glob&#39;;
import dotenv from &#39;dotenv&#39;;

dotenv.config();

cloudinary.config({
  cloud_name: &#39;ssoon&#39;,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
  secure: true, // Return &quot;https&quot; URLs by setting secure: true
});

// Log the configuration
console.log(cloudinary.config());
/////////////////////////
// Uploads an image file
/////////////////////////
const uploadImage = async ({
  imagePath,
  folder,
}: {
  imagePath: string;
  folder: string;
}) =&gt; {
  const options = {
    use_filename: true, // Use the uploaded file&#39;s name as the asset&#39;s public ID and
    unique_filename: false,
    overwrite: true, // allow overwriting the asset with new versions
    folder,
  };

  // Upload the image
  const result = await cloudinary.uploader.upload(imagePath, options);
  return result.public_id;
};

(async () =&gt; {
  const blogImgPaths = await glob([&#39;public/**/*.{jpg,jpeg,png,gif,webp,svg}&#39;]);
  if (blogImgPaths.length) {
    const uploadPromises = blogImgPaths.map(async (imgPath) =&gt; {
      const folder = imgPath
        .replace(&#39;public/&#39;, &#39;&#39;)
        .replace(/\/[^/]+\.(jpg|jpeg|png|gif|webp|svg)$/, &#39;&#39;);
      return uploadImage({
        imagePath: imgPath,
        folder,
      });
    });
    try {
      const result = await Promise.all(uploadPromises);
      console.log(&#39;result&#39;, result);
    } catch (e: any) {
      throw new Error(e);
    }
  }
})();
</code></pre>
<p>public폴더에 이미지를 막 넣고 <code>yarn upoad-cloudinary</code> 스크립트를 실행 시킨 결과입니다.
<img src="https://velog.velcdn.com/images/ssoon-m/post/56ad94ce-89b5-4633-bddc-9fa8892ab1ef/image.png" alt=""></p>
<p>cloudinary에 자동으로 업로드가 되는 모습을 볼 수 있습니다.
(아래 사진은 <code>posts/blog</code> 아래에 업로드된 이미지들만 본 결과입니다. )
<img src="https://velog.velcdn.com/images/ssoon-m/post/bb126acd-9412-47c7-85d9-2dc72dfd3b1b/image.png" alt=""></p>
<p>이제 프로젝트에서 이미지를 사용할 때 <code>static assets</code>경로를 적어주면 기존 코드 수정 없이 cloudinaryLoader 통해 Cloudinary에 업로드된 이미지를 사용할 수 있습니다.</p>
<pre><code class="language-tsx">&lt;Image
  src=&quot;/logo/white-logo.png&quot;
  width={80}
  height={40}
  priority
  alt=&quot;site logo&quot;
/&gt;</code></pre>
<h2 id="마치며">마치며</h2>
<p>Next.js에서 자체적으로 빌드타임때 이미지 최적화를 해준다면 이런 번거로운 작업은 안 해도 될 거 같은데 살짝 아쉬운거 같습니다.
Next.js를 SSG로 마케팅하는 것은 올바르지 않다는 <a href="https://github.com/vercel/next.js/issues/36431#issue-1214119137">의견</a>도 보이네요 😅
(그래도 Next.js SSG로 <a href="https://ianlog.me/">개인 블로그</a>를 만들때 이미지 최적화 작업을 제외하면 좋은 경험이였습니다..)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[js 모듈 캐싱에 대해 알고 계신가요?]]></title>
            <link>https://velog.io/@ssoon-m/js-%EB%AA%A8%EB%93%88-%EC%8A%A4%EC%BD%94%ED%94%84-%EC%BA%90%EC%8B%B1%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EA%B3%A0-%EA%B3%84%EC%8B%A0%EA%B0%80%EC%9A%94</link>
            <guid>https://velog.io/@ssoon-m/js-%EB%AA%A8%EB%93%88-%EC%8A%A4%EC%BD%94%ED%94%84-%EC%BA%90%EC%8B%B1%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EA%B3%A0-%EA%B3%84%EC%8B%A0%EA%B0%80%EC%9A%94</guid>
            <pubDate>Sun, 20 Aug 2023 14:40:28 GMT</pubDate>
            <description><![CDATA[<h2 id="시작하며">시작하며</h2>
<p>여러분들은 혹시 <strong>모듈 캐싱</strong>에 대해 알고 계신가요?
모던 웹 개발을 하면서 자연스럽게 활용되는 <strong>모듈 시스템</strong>의 빠질 수 없는 개념인 <strong>모듈 캐싱</strong>에 대해 함께 알아보고자 합니다.
<strong>모듈 캐싱</strong>의 동작 원리와 함께 주의해야 할 사항들을 예시 코드들을 통해 자세히 살펴보도록 하겠습니다. </p>
<h2 id="예시1-js-모듈-스코프">예시1) js 모듈 스코프</h2>
<pre><code class="language-jsx">// moduleA.js
let counter = 0;

export function incrementCounter() {
  counter++;
}

export function getCounterValue() {
  return counter;
}
</code></pre>
<pre><code class="language-jsx">// moduleB.js
import { incrementCounter, getCounterValue } from &#39;./moduleA&#39;;

console.log(getCounterValue()); // 출력: 0

incrementCounter(); // counter 값 증가

console.log(getCounterValue()); // 출력: 1
</code></pre>
<pre><code class="language-jsx">// moduleC.js
import {getCounterValue} from &#39;./moduleA&#39;

console.log(getCounterValue()); // 출력: 1
</code></pre>
<p><code>moduleC</code> 파일을 살펴보면, <code>counter</code> 변수가 1로 출력되는걸 알 수 있습니다.
마치 <code>moduleB</code>와 <code>moduleC</code>가 <strong>한 모듈 스코프 내에 있는것 처럼</strong> 결과가 나타났습니다.</p>
<p>왜 이런 결과가 나왔을까요?</p>
<h3 id="모듈-캐싱에-대하여">모듈 캐싱에 대하여</h3>
<p>모듈 시스템에서의 중요한 개념 중 하나는 <strong>모듈은 기본적으로 한 번만 로드되고 실행된다는 것</strong>입니다.
이는 여러 번 모듈을 불러와 사용하더라도 해당 모듈은 <strong>최초 호출 시에만 로드되고 실행된다는 것을 의미합니다.</strong>
만약 매번 호출할 때마다 모듈을 실행시키는 방식이라면 <code>moduleC</code> 에서 0이 출력이 됬겠지만, 메모리 사용이 불필요하게 늘어날 수 있습니다. 
최초 호출 시에만 모듈을 로드하여 캐싱하는 것은 메모리 사용을 최적화하는 데 도움을 줍니다.
이후 요청 시 해당 모듈이 캐시에 존재하면 재로딩하지 않고, 캐싱된 인스턴스를 활용합니다.</p>
<h3 id="예시1-코드-단계별-동작-과정">예시1 코드 단계별 동작 과정</h3>
<ol>
<li><code>moduleA</code> 파일에 <code>counter</code> 변수와 <code>getCounterValue</code> 함수를 정의합니다.</li>
<li><code>moduleB</code>에서 <code>moduleA</code>의 함수를 import하고 호출합니다. 이때 <code>moduleA</code>가 로드됩니다.</li>
<li><code>moduleB</code>의 코드 실행 후, <code>moduleC</code>가 실행됩니다. 이때 이미 로드된 <code>moduleA</code>를 다시 로드하지 않고, 이전에 캐싱된 <code>moduleA</code>의 항목들을 그대로 사용합니다.</li>
<li>따라서 <code>moduleC</code>에서 <code>getCounterValue</code> 함수를 호출하면, 이미 증가된 <code>counter</code> 변수의 값인 1이 출력됩니다.</li>
</ol>
<p>이렇게 <code>moduleB</code>와 <code>moduleC</code>에서 공유하는 <code>moduleA</code>의 모듈 스코프를 통해 변수와 함수가 상태를 유지하며 공유되는 것을 볼 수 있습니다.</p>
<h2 id="예시2-리액트-함수형-컴포넌트의-바깥-변수-문제">예시2) 리액트 함수형 컴포넌트의 바깥 변수 문제</h2>
<blockquote>
<p>리액트에서 함수형 컴포넌트를 사용할 때, 컴포넌트 바깥에 선언된 변수를 사용하는 경우가 종종 있습니다.
이때 문제가 발생할 수 있는데, 변경 가능한 변수를 컴포넌트 바깥에 선언을 하고 함수형 컴포넌트 내에서 바깥의 변수를 사용할 경우 입니다. 해당 문제도 모듈 스코프의 캐싱 동작 문제입니다.</p>
</blockquote>
<p><code>Counter</code> 라는 함수형 컴포넌트의 바깥에 count 변수를 선언하고 <code>handleCountIncrease</code> 함수를 통해 count 변수를 증가시키는 코드입니다.</p>
<pre><code class="language-jsx">// Counter.jsx
import React, { useState } from &quot;react&quot;;

let count = 1;

const Counter = () =&gt; {
  const [_, setCount] = useState(0);
  const handleCountIncrease = () =&gt; {
    setCount(count++);
  };
  return (
    &lt;div style={{ display: &quot;flex&quot;, alignItems: &quot;center&quot;, gap: &quot;5px&quot; }}&gt;
      &lt;div&gt;{count}&lt;/div&gt;
      &lt;button onClick={handleCountIncrease}&gt;+&lt;/button&gt;
    &lt;/div&gt;
  );
};

export default Counter;
</code></pre>
<p><code>Counter</code> 컴포넌트를 사용하는 부분에서 <code>Counter</code> 컴포넌트를 중복 호출을 했습니다.</p>
<pre><code class="language-jsx">import React from &quot;react&quot;;
import Counter from &quot;./components/Counter&quot;;

function App() {
  return (
    &lt;div className=&quot;App&quot;&gt;
      &lt;Counter /&gt; {/* 1번 Counter */}
      &lt;hr /&gt;
      &lt;Counter /&gt; {/* 2번 Counter */}
    &lt;/div&gt;
  );
}

export default App;
</code></pre>
<p>1번의 컴포넌트의 counter 값을 5까지 올리고 2번 컴포넌트의 counter 값을 올리면 결과가 어떻게 나올까요?
리액트 컴포넌트를 활용해서 개발을 해온 입장에서는 counter값이 2가 나오는걸 기대하는게 일반적이겠지만, 정답은 6 입니다.
6이라는 값이 나온 이유는 위에서 설명한 <strong>모듈 캐싱</strong> 동작과 관련이 있습니다.</p>
<p>동작 과정을 살펴 보자면, 예시1의 단계별 동작 과정과 유사한 과정을 거치게 됩니다.</p>
<ol>
<li><code>App</code> 컴포넌트에서 <code>Counter</code> 컴포넌트를 import하고 호출합니다. 이때 <code>Counter</code> 컴포넌트의 모듈이 로드 됩니다.</li>
<li><code>1번 Counter</code> 컴포넌트 실행 후 <code>2번 Counter</code> 컴포넌트가 실행이 됩니다. 이때 이미 로드된 <code>Counter</code> 컴포넌트의 모듈은 실행되지 않습니다.</li>
<li><code>1번 Counter</code> 컴포넌트에서 count 변수를 5까지 올린 후 <code>2번 Counter</code> 컴포넌트에서 <code>handleCountIncrease</code> 함수를 실행 시키면 이미 메모리에 올라가있는 count 변수에 1이 더해져서 6이 출력이 됩니다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/ssoon-m/post/3e3094c3-fd68-405f-b95d-11de01fb53b2/image.png" alt=""></p>
<blockquote>
<p><a href="https://codesandbox.io/p/sandbox/sad-kilby-x32tgv?file=%2Fsrc%2FApp.tsx%3A13%2C1">함수형 컴포넌트 외부 변수 예시</a>에서 상세 코드 확인이 가능합니다.</p>
</blockquote>
<p>리액트에서 모듈 스코프의 캐싱 동작에 대해 알아봤습니다.
그렇다면, 컴포넌트가 언마운트(unmount) 일때 모듈 캐싱은 어떻게 동작을 할까요?</p>
<h3 id="컴포넌트-언마운트시-모듈-캐싱-동작">컴포넌트 언마운트시 모듈 캐싱 동작</h3>
<p>리액트에서 컴포넌트가 언마운트되면 해당 컴포넌트와 관련된 리소스 및 상태 등이 정리됩니다. 하지만, 모듈의 캐싱은 모듈 시스템 자체의 동작 방식에 의해 처리되므로, 컴포넌트의 언마운트와 직접적인 연관이 없습니다.
<img src="https://velog.velcdn.com/images/ssoon-m/post/a5088465-fc4c-461e-96d1-52e2a1d5fa5f/image.png" alt=""></p>
<blockquote>
<p><a href="https://codesandbox.io/p/sandbox/outer-let-forked-8gsg4x?file=/src/App.tsx:10,26">컴포넌트 언마운트 예시</a>에서 상세 코드 확인이 가능합니다.</p>
</blockquote>
<h2 id="예시3-리액트에서-debounce-사용시-예제">예시3) 리액트에서 debounce 사용시 예제</h2>
<h3 id="즉시실행-함수와-클로저를-조합한-util-함수-문제점">즉시실행 함수와 클로저를 조합한 util 함수 문제점</h3>
<p>react프로젝트에서 util 함수로 IIFE를 이용하여 debounce를 구현했습니다.</p>
<pre><code class="language-tsx">// /utils/debounce.ts
export const debounce = (() =&gt; {
  let timeoutId: ReturnType&lt;typeof setTimeout&gt;;
  return (callback: () =&gt; void, delay: number) =&gt; {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(callback, delay);
  };
})();
</code></pre>
<p>input에서 util의 debounce함수를 이용하는 예시 입니다.</p>
<p>onChange이벤트가 발생하고 1초 동안 onChange 이벤트가 없으면 입력한 값이 보이게 됩니다.</p>
<p>자세한 동작 과정은 이렇습니다.</p>
<ol>
<li><code>debounce</code> 함수는 모듈 스코프에서 생성되고 실행됩니다.</li>
<li>이 함수는 클로저로서 <code>timeoutId</code> 변수에 접근이 가능합니다.</li>
<li>debounce 함수를 import시 모듈이 로드가 되고 <code>timeoutId</code> 변수가 <code>debounce</code> 함수의 렉시컬 스코프 내에 선언되었습니다.</li>
<li>InputField가 리렌더링 되어도 <code>timeoutId</code> 는 모듈 스코프 내에서 선언되어 있기 때문에 값이 유지가 됩니다.</li>
<li>따라서 <code>InputField</code> 컴포넌트는 원하는 대로 동작을 하게 됩니다.</li>
</ol>
<pre><code class="language-tsx">// /components/InputField

import { useState } from &quot;react&quot;;
import { debounce } from &quot;../utils/debounce&quot;;

interface Props {
  type: string;
}

const InputField = ({ type }: Props) =&gt; {
  const [value, setValue] = useState(&quot;&quot;);
  const [debounceValue, setDebounceValue] = useState(&quot;&quot;);
  const handleInputChange = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const value = e.currentTarget.value;
    setValue(value);
    debounce(() =&gt; setDebounceValue(value), 1000);
  };

  return (
    &lt;&gt;
      &lt;input value={value} onChange={handleInputChange} /&gt;
      &lt;div&gt;
        {type}값 : {debounceValue}
      &lt;/div&gt;
    &lt;/&gt;
  );
};

export default InputField;
</code></pre>
<p>debounce 함수를 따로 hook으로 작성을 안 하고 IIFE 로 작성한 결과 컴포넌트에서 바로 import 후 사용하는 형태여서 매우 편하게 사용이 가능한 모습입니다.</p>
<p>그런데 만약 InputField컴포넌트를 사용하는 곳에서 컴포넌트를 두 번 호출을 했다면 정상적으로 동작을 할까요?</p>
<pre><code class="language-tsx">import InputField from &quot;./components/InputField&quot;;

function App() {
  return (
    &lt;div className=&quot;App&quot;&gt;
      &lt;InputField type=&quot;A&quot; /&gt;
      &lt;hr /&gt;
      &lt;InputField type=&quot;B&quot; /&gt;
    &lt;/div&gt;
  );
}

export default App;
</code></pre>
<p>위의 예시에서 <code>InputField</code> 컴포넌트 내부에서 같은 모듈에서 가져온 <code>debounce</code> 함수를 사용하는 경우, 두 컴포넌트 간에 같은 <code>timeoutId</code> 변수를 공유하게 됩니다. 이로 인해 <code>type A</code>인 <code>InputField</code>에 값을 입력하다가 1초 이내에 <code>type B</code>인 <code>InputField</code>에 값을 입력하면, 두 컴포넌트에서 동일한 <code>debounce</code> 함수를 동시에 조작하게 됩니다.</p>
<p><code>timeoutId</code> 변수를 공유하기 때문에 <code>type A</code> 컴포넌트의 입력이 <code>type B</code> 컴포넌트의 입력에 영향을 줄 수 있습니다. 즉, <code>type A</code> 입력에서의 디바운싱(delay) 동작이 <code>type B</code> 입력에 영향을 미칠 수 있게 되는 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/ssoon-m/post/87f52290-2aee-4077-bdd3-b3e4ba713f96/image.png" alt=""></p>
<blockquote>
<p><a href="https://codesandbox.io/p/sandbox/react-debounce-util-r2ttrs">util IIFE deboune</a> 에서 상세 코드 확인이 가능합니다.</p>
</blockquote>
<h3 id="개선-방향">개선 방향</h3>
<p>이러한 문제를 방지하기 위해서는 <code>InputField</code> 컴포넌트 내부에서 <code>debounce</code> 함수를 사용할 때 즉시 실행 함수(IIFE)를 활용하여 <code>timeoutId</code>를 모듈 스코프에서 캐싱하지 않도록 해야 합니다.</p>
<p>해결 방법은 다음과 같이 <code>debounce</code> 함수 사용 시, 컴포넌트마다 독립적인 <code>timeoutId</code> 변수를 활용하는 것입니다. 이를 통해 각 <code>InputField</code> 컴포넌트는 자신만의 타이머를 유지하며 디바운싱을 수행하게 됩니다.</p>
<pre><code class="language-tsx">export const debounce = &lt;T extends unknown[]&gt;(
  callback: (...args: T) =&gt; void,
  delay: number
) =&gt; {
  let timeoutId: ReturnType&lt;typeof setTimeout&gt;;
  return (...args: T) =&gt; {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() =&gt; callback(...args), delay);
  };
};
</code></pre>
<p>컴포넌트 리렌더링시 <code>timeoutId</code> 가 초기화 되는 문제도 고려해야 하기 때문에 커스텀 훅에서 <code>useCallback</code> 을 이용하는 방법으로 개선시킬 수 있습니다.</p>
<pre><code class="language-tsx">import { useCallback } from &quot;react&quot;;
import { debounce } from &quot;../utils/debounce&quot;;

export const useDebounce = &lt;T extends unknown[]&gt;(
  callback: (...args: T) =&gt; void,
  delay: number,
  deps: unknown[]
) =&gt; {
  const debounceCallback = useCallback(
    debounce((...args: T) =&gt; callback(...args), delay),
    [...deps]
  );
  return debounceCallback;
};
</code></pre>
<p><img src="https://velog.velcdn.com/images/ssoon-m/post/e3239d20-29db-4888-bb12-468a7c284d13/image.png" alt=""></p>
<blockquote>
</blockquote>
<p><a href="https://codesandbox.io/p/sandbox/react-debounce-hooks-vxyr28">debounce hooks</a> 에서 상세 코드 확인이 가능합니다.</p>
<h2 id="마치며">마치며</h2>
<p>모듈을 import하여 사용하는 과정에서 발생할 수 있는 모듈 캐싱의 문제들을 예시코드로 다뤄봤습니다.</p>
<p>실제 프로젝트에서도 모듈 캐싱 동작에 대한 이해를 바탕으로 안정성 있는 코드를 작성할 수 있을 것입니다.</p>
<p>제가 작성한 내용이 도움이 되었으면 좋겠습니다. 만약 잘못된 정보가 있다면, 부담 없이 댓글로 알려주세요 😀</p>
<p>부족한 글 읽어주셔서 감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[react] key 제대로 다루기]]></title>
            <link>https://velog.io/@ssoon-m/react-key-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%8B%A4%EB%A3%A8%EA%B8%B0</link>
            <guid>https://velog.io/@ssoon-m/react-key-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%8B%A4%EB%A3%A8%EA%B8%B0</guid>
            <pubDate>Fri, 21 Apr 2023 08:02:23 GMT</pubDate>
            <description><![CDATA[<p>react에서 <code>key props</code> 란 뭘까?
<a href="https://react.dev/learn/rendering-lists#where-to-get-your-key">react docs</a>에서는 react 컴포넌트가 렌더링 되는 동안 형제 간에 항목을 고유하게 식별할 수 있게 도와준다고 나와있다.</p>
<p>아래 예시들을 통해 항목을 고유하게 식별할 때의 이점과 리액트에서 <code>key</code> 를 잘 활용하는 방법을 알아보자</p>
<h2 id="배열-랜더링-예시">배열 랜더링 예시</h2>
<p>다음과 같은 배열이 있을 때 map을 이용해서 랜더링을 한다고 생각해보자</p>
<pre><code class="language-tsx">export default function App() {
  const [list,setList] = useState([&#39;a&#39;,&#39;b&#39;,&#39;c&#39;,&#39;d&#39;]);
  return (
    &lt;ul&gt;{list.map(item =&gt; &lt;li&gt;{item}&lt;/li&gt;)}&lt;/ul&gt;
  );
}</code></pre>
<p>a,b,c,d 순서대로 화면에 랜더링이 될 것이다.</p>
<p>그런데 위와 같이 배열을 랜더링하면 문제가 생긴다.</p>
<blockquote>
<p>❗ <strong>Warning: Each child in a list should have a unique “key” prop.</strong></p>
</blockquote>
<p>이 짜증나는 에러를 누구나 한 번쯤은 본 적이 있을것이다. (저 에러 없애려고 배열 index를 key로 넣은 사람 🤚)</p>
<p>리액트팀에서 저 에러를 보여주는 이유는 컴포넌트의 효율적인 랜더링을 위해서다.</p>
<h2 id="key는-왜-존재할까">key는 왜 존재할까?</h2>
<p>리액트의 효율적인 랜더링을 위해서 존재한다.</p>
<blockquote>
<p>철수와 짱구가 공용으로 사용하는 컴퓨터에 파일이 있다고 생각해보자. 
각 파일명은 <strong>맹구</strong>,<strong>유리</strong> 이다. 
어느날 철수가 두 파일 사이에 <strong>훈이</strong>를 추가 하면 짱구는 어떻게 생각을 할까?
다음날 짱구가 해당 컴퓨터 파일을 볼 때 <strong>유리</strong>자리에 <strong>훈이</strong>가 추가된 걸 순식간에 알 수 있을 것이다.
그런데 만약 컴퓨터 파일에 파일명이 존재하지 않는다면? 폴더 아이콘만 있다고 생각해보자.
<strong>맹구,유리</strong> 사이에 <strong>훈이</strong>가 추가 됐다면 어떤게 추가된 파일이 뭔지 바로 알 수 있을까?
일일이 파일을 들어가봐야 <strong>유리</strong> 자리에 <strong>훈이</strong>가 추가 된 것을 알 수 있을것이다.</p>
</blockquote>
<p>이러한 비효율적인 일을 하지 않기 위해 react팀에서는 <code>key</code>(파일명)를 만들었다.</p>
<p><code>key</code>가 없다면 react 입장에서는 어떤게 새롭게 추가된 요소고 어떤게 기존에 존재하던 요소인지 효율적으로 알 수가 없다. ( 자세한 내용은 <a href="https://react.dev/learn/preserving-and-resetting-state">상태 유지 및 재설정</a> 문서에 나와있다. )</p>
<p>리액트는 형제요소 간에 항목을 고유하게 식별하기 위해 <code>key</code> 가 필요하다.<code>key</code> 가 변하면 항목이 변했다고 간주한다.</p>
<p>이런 리액트의 효율적인 알고리즘을 위해 리액트로 개발하는 개발자 분들이 <code>key</code>를 꼭 넣어주는 수고스러움을 견뎌야 한다.</p>
<blockquote>
<p>💡 중요 : key는 전역적으로 유지되지 않고 부모 요소의 내부에서만 유효하다!</p>
</blockquote>
<h2 id="배열에서의-key-활용법">배열에서의 key 활용법</h2>
<p>이제 <code>key</code>를 왜 추가해야 하는지 알겠는데 다음 코드처럼 배열의 <code>index</code>를 <code>key props</code>로 활용하면 쉽게 해결 되겠네? 라고 생각한다면, 맞기도하고 틀리기도 하다.</p>
<p>콘솔 창의 에러만 없앤 것이지 <code>key</code>를 쓰기 전 동작과 동일하다.</p>
<pre><code class="language-tsx">export default function App() {
  const [list,setList] = useState([&#39;a&#39;,&#39;b&#39;,&#39;c&#39;,&#39;d&#39;]);
  return (
    &lt;ul&gt;{list.map((item,index) =&gt; &lt;li key={index}&gt;{item}&lt;/li&gt;)}&lt;/ul&gt;
  )
}</code></pre>
<h3 id="배열의-index를-key로-활용할-경우">배열의 index를 key로 활용할 경우</h3>
<p><code>key</code>를 배열의 <code>index</code>로 사용하는 경우는 두 가지 조건을 모두 만족해야 한다.</p>
<ul>
<li>리스트에 수정, 삭제, 삽입을 할 일이 없을 때</li>
<li>리스트의 순서가 변하지 않을 때</li>
</ul>
<p>이유는 list가 변할 때 컴포넌트가 리렌더링 되면서 배열의 0번 째 <code>index</code> 에 새로운 요소가 추가 됐다면 리액트는 기존의 <code>key</code> 가 0인 컴포넌트가 변하지 않았다고 생각한다.</p>
<p>그래서 변경이 있을거 같은 리스트인 경우 <code>key</code> 를 배열의 <code>index</code> 로 적어주는건 매우 위험하다.</p>
<h3 id="변하는-리스트에서-key를-배열의-index로-사용한다면">변하는 리스트에서 key를 배열의 index로 사용한다면?</h3>
<p>다음 코드 예시로 위험성을 알아보자.</p>
<pre><code class="language-tsx">import { useState } from &quot;react&quot;;

const Input = (props) =&gt; {
  const { placeholder } = props;
  const [value, setValue] = useState(&quot;&quot;);
  const handleInputChange = (e) =&gt; {
    setValue(e.currentTarget.value);
  };

  return (
    &lt;input
      placeholder={placeholder}
      value={value}
      onChange={handleInputChange}
    /&gt;
  );
};

export default function App() {
  const [list, setList] = useState([&quot;aaa&quot;, &quot;bbb&quot;, &quot;ccc&quot;, &quot;ddd&quot;]);
  const handleListInsertClick = () =&gt; {
    setList((prevList) =&gt; [&quot;eee&quot;].concat(prevList));
  };
  return (
    &lt;div&gt;
      &lt;div&gt;
        {list.map((item,index) =&gt; (
          &lt;Input key={index} placeholder={item} /&gt;
        ))}
      &lt;/div&gt;
      &lt;div&gt;
        &lt;button onClick={handleListInsertClick}&gt;요소 삽입&lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>요소 삽입 버튼 클릭시 배열의 맨 앞에 리스트를 추가하는 상황이다.</p>
<p>그리고 input값은 각각 다음과 같다.<img src="https://velog.velcdn.com/images/ssoon-m/post/b6454d1e-0bc7-45c0-8e2d-d5f57b7ce74e/image.png" alt="input-value.png"></p>
<p>위 상황에서 요소 삽입 버튼을 누른다면 eee를 <code>placeholder</code>로 보여주는 <code>input</code> 창이 맨 앞에 나오고 aaa입니다. bbb입니다. 순으로 화면에 보여지는게 기대하는 자연스러운 동작일 것이다.</p>
<p>하지만, 기대와 다르게 요소 삽입 버튼을 누르면 다음과 같이 컴포넌트가 랜더링이 된다.
<img src="https://velog.velcdn.com/images/ssoon-m/post/2a65132f-b790-4af7-8172-eba0febbaf0d/image.png" alt="input-value.png"></p>
<p>eee를 0번 째 <code>index</code> 에 삽입을 했으니 eee를 기존의 <code>key</code> 값이 0이였던 컴포넌트로 인식을 해서 상태가 그대로 유지가 된 상황이다.</p>
<p>리액트는 <code>key</code>값을 기준으로 요소가 변경되었는지 판단하기 때문에 당연한 결과이다.</p>
<p><code>key</code> 값을 리스트 각 하위 항목별로 고유한 id로 적어준다면 예상한대로 동작을 한다.</p>
<p>코드를 다음과 같이 바꿔 보자!</p>
<pre><code class="language-tsx">const [list, setList] = useState([
    { id: 0, text: &quot;aaa&quot; },
    { id: 1, text: &quot;bbb&quot; },
    { id: 2, text: &quot;ccc&quot; },
    { id: 3, text: &quot;ddd&quot; }
  ]);
const handleListInsertClick = () =&gt; {
  setList((prevList) =&gt; [{ id: 4, text: &quot;eee&quot; }].concat(prevList));
};

//...
{list.map((item, index) =&gt; (
  &lt;Input key={item.id} placeholder={item.text} /&gt;
))}
//...</code></pre>
<p><img src="https://velog.velcdn.com/images/ssoon-m/post/28896b80-3a85-41f6-bd69-555702f8a291/image.png" alt="input-value.png"></p>
<p>eee를 삽입시 새로운 항목이 추가됐다고 올바르게 인식을 하고 추가가 정상적으로 잘 됐다.</p>
<p>혹시나 고유한 id를 적어야 하므로 <code>key</code> 에 다음 형태로 넣는건 제발 하지 말자. </p>
<pre><code class="language-tsx">&lt;Input key={Math.random()} /&gt;</code></pre>
<p>리스트가 변하고 컴포넌트가 리렌더링되면 기존의 <code>key</code>가 다른값으로 바뀌기 때문에 전체 리스트가 새로운 컴포넌트로 인식이 된다.</p>
<h3 id="그렇다면-변하는-리스트의-key에는-교유한-값을-어떻게-넣어줘야-할까">그렇다면 변하는 리스트의 key에는 교유한 값을 어떻게 넣어줘야 할까?</h3>
<ul>
<li>서버에서 리스트를 내려줄 때의 id</li>
<li>로컬에서 데이터를 생성했다면 <strong>항목을 생성할 때 id를 미리 추가</strong>. (incrementing counter , uuid , crypto.randomUUID() 등.. )</li>
</ul>
<p>리스트의 <code>key</code>에는 컴포넌트가 랜더링 될 때마다 리스트 항목의 <code>key</code>가 바뀌는 값을 넣어주면 안된다.</p>
<p>한 번 생성된 리스트의 <code>key</code>는 다시 렌더링 되더라도 바뀌면 안된다.</p>
<h2 id="컴포넌트-상태-초기화시-key-활용법">컴포넌트 상태 초기화시 key 활용법</h2>
<p><code>key</code>는 배열을 렌더링 할때만 사용하는것이 아니다.<code>key</code> 는 컴포넌트의 식별자라고 생각하면 된다. </p>
<p><code>key</code> 가 바뀌면 리액트는 새로운 컴포넌트로 인식을 한다는 점을 이용하면 아주 간편하게 컴포넌트의 상태를 초기화 시킬 수 있다.</p>
<p>예시 상황을 보자</p>
<p><code>ProfilePage</code> 컴포넌트는 <code>useId</code> 를 props로 받는다. </p>
<p>페이지에는 댓글이 포함되어 있으며 <code>comment</code> 상태값을 이용하여 해당 값을 보유한다.</p>
<p>한 프로필에서 다른 프로필로 이동할 때 <code>comment</code> 의 상태가 재설정되지 않는 오류가 있을 때 어떻게 해결을 할 것인가?</p>
<p>다음 코드와 같이 작성하면 된다.</p>
<pre><code class="language-tsx">export default function ProfilePage({ userId }) {
  return (
    &lt;Profile
      userId={userId}
      key={userId}
    /&gt;
  );
}

function Profile({ userId }) {
  // ✅ This and any other state below will reset on key change automatically
  const [comment, setComment] = useState(&#39;&#39;);
  // ...
}</code></pre>
<p>key를 사용해서 <code>useEffect</code> 사용을 없애고 아주 우아하게 컴포넌트의 상태값을 초기화 하는 방법을 공식문서에서 확인할 수 있다.(<a href="https://react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes">useEffect가 필요하지 않을수도 있다!</a>)</p>
<h2 id="요약">요약</h2>
<ol>
<li>리스트가 변할 가능성이 없다면 배열의 index를 key로 적어도 된다.</li>
<li>리스트가 변할 가능성이 있다면 배열의 index를 key로 쓰는 행동은 절대 하지 말자.</li>
<li>컴포넌트의 상태를 초기화 시키고 싶다면 key를 이용해보자!</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[esm으로 번들링한 모듈을 ssr프레임워크에서 사용시 import 에러 해결법(SyntaxError: Cannot use import statement outside a module)]]></title>
            <link>https://velog.io/@ssoon-m/esm%EC%9C%BC%EB%A1%9C-%EB%B2%88%EB%93%A4%EB%A7%81%ED%95%9C-%EB%AA%A8%EB%93%88%EC%9D%84-ssr%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%EC%8B%9C-import-%EC%97%90%EB%9F%ACSyntaxError-Cannot-use-import-statement-outside-a-module</link>
            <guid>https://velog.io/@ssoon-m/esm%EC%9C%BC%EB%A1%9C-%EB%B2%88%EB%93%A4%EB%A7%81%ED%95%9C-%EB%AA%A8%EB%93%88%EC%9D%84-ssr%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%EC%8B%9C-import-%EC%97%90%EB%9F%ACSyntaxError-Cannot-use-import-statement-outside-a-module</guid>
            <pubDate>Fri, 23 Sep 2022 11:38:37 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ssoon-m/post/f7919b3f-a0ab-4440-b06c-961c578f5a08/image.png" alt=""></p>
<h2 id="❗️library-maintainer-일-경우에-대한-에러-해결법-입니다">❗️library maintainer 일 경우에 대한 에러 해결법 입니다.</h2>
<p>만약 maintainer가 아니고 <code>next</code>를 사용하고 있다면 <code>next-transpile-modules</code> 를 사용하시면 됩니다.
<code>next</code>에서 사용이 가능하게 <code>transfile</code>을 도와줍니다.</p>
<h2 id="1️⃣-사용하는-프레임워크의-esm-모듈-지원-여부를-확인-해준다">1️⃣ 사용하는 프레임워크의 esm 모듈 지원 여부를 확인 해준다.</h2>
<p>Next.js 11.1 부터 <strong>ES Modules</strong> (e.g. &quot;type&quot;: &quot;module&quot; in their package.json) 사용이 가능하다.
<code>config.js</code>에 아래 속성을 꼭 적어줘야 한다.</p>
<pre><code class="language-js">// next.config.js
module.exports = {
  // Prefer loading of ES Modules over CommonJS
  experimental: { esmExternals: true },
};</code></pre>
<p><a href="https://nextjs.org/blog/next-11-1#es-modules-support">https://nextjs.org/blog/next-11-1#es-modules-support</a></p>
<blockquote>
<p>ESM 라이브러리의 <code>package.json</code>에 <code>{&quot;type&quot;: &quot;module&quot;}</code>이 있는 한 <strong>Next.js 12</strong>부터는 <strong>ES Module</strong>에 대한 지원이 기본적으로 활성화됩니다. (esmExternals이 기본적으로 true)
<a href="https://stackoverflow.com/questions/65974337/import-es-module-in-next-js-err-require-esm">https://stackoverflow.com/questions/65974337/import-es-module-in-next-js-err-require-esm</a></p>
</blockquote>
<h2 id="2️⃣-사용한-모듈의-packagejson-확인">2️⃣ 사용한 모듈의 package.json 확인</h2>
<blockquote>
<p><code>package.json</code> 에 <code>&quot;type&quot;</code> 필드가 없거나 <code>&quot;type:commonjs&quot;</code> 인 경우 <code>.js</code> 파일은 <strong>CommonJS</strong>로 처리된다.
<code>&quot;type&quot; : &quot;module&quot;</code> 인 경우엔 <strong>ES Module</strong> 으로 인식한다. <br/> <code>&quot;type&quot;</code> 필드 값에 관계없이 <code>.mjs</code> 확장자를 가진 파일은 항상 <strong>ES Module</strong>로 처리되고 <code>.cjs</code> 파일은 항상 <strong>CommonJS</strong>로 처리된다. <br/><a href="https://nodejs.org/dist/latest-v18.x/docs/api/packages.html#type">https://nodejs.org/dist/latest-v18.x/docs/api/packages.html#type</a></p>
</blockquote>
<p>위 글을 보면 알 수 있듯이 esm으로 번들링후 해당 모듈을 코드베이스에서 사용할 때 <strong>ES Module</strong>의 <code>package.json</code> 이나 파일의 확장자에 <em>이 모듈은 <strong>ES Module</strong>입니다. 라는 명시를 해주지 않으면 사용하는 쪽에서 해당 모듈을 해석할 때 <strong>ES Module</strong>로 인식을 하지 않고 <strong>CommonJS</strong>로 인식 함</em> </p>
<h2 id="🔥-그래서-import-에러는-왜-나는건데">🔥 그래서 import 에러는 왜 나는건데?</h2>
<p><code>node</code> 에서 <strong>ECMAScript 모듈 로더</strong>를 사용해야 <strong>ES Module</strong> 사용이 가능한데 <strong>ES Module</strong>에서 <strong>ECMAScript 모듈 로더</strong>를 사용하기 위한 설정을 하지 않아서 생긴 에러이다.
요약하자면, <code>node</code>가 <strong>ES Module</strong>을 <strong>CommonJS 모듈 로더</strong>를 사용해서 생긴 문제이다.
<a href="https://nodejs.org/dist/latest-v16.x/docs/api/esm.html">https://nodejs.org/dist/latest-v16.x/docs/api/esm.html</a></p>
<h2 id="🚀-결론">🚀 결론</h2>
<p>csr 에서 사용이 잘 되는 <strong>ES Module</strong> 인데 ssr에서 동작이 안된다면?
ssr쪽에서 <strong>ES Module</strong>로 인식을 못 하고 있는 것이다.(<strong>ES Module</strong> 지원을 안 하는 프레임워크거나...)
<code>package.json</code> 에 <code>{&quot;type&quot; : &quot;module&quot;}</code>를 적어주거나 확장자를 변경하도록 하자!</p>
<p>잘못된 정보가 있다면 댓글로 지적 부탁드립니다!🙇‍♂️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] Array.includes 와 Set.has]]></title>
            <link>https://velog.io/@ssoon-m/JS-Array.includes-%EC%99%80-Set.has</link>
            <guid>https://velog.io/@ssoon-m/JS-Array.includes-%EC%99%80-Set.has</guid>
            <pubDate>Wed, 14 Sep 2022 13:54:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><code>incldues</code> 와 <code>has</code> 중 뭐가 더 빠를까?
자료구조를 생각해보면 바로 예상이 갈 것이다.</p>
</blockquote>
<pre><code class="language-js">let n = 20000;
let arr = Array.from({ length: n }, (v, i) =&gt; i + 1); 
console.time( &#39;includes Test&#39; );
arr.includes(10000)
console.timeEnd( &#39;includes Test&#39; );
let set = new Set( arr );
console.time( &#39;has Test&#39; );
set.has(10000)
console.timeEnd( &#39;has Test&#39; );</code></pre>
<h2 id="🚀-코드-실행-결과">🚀 코드 실행 결과</h2>
<p>incldus Test -&gt; 0.010009765625 ms
has  Test -&gt; 0.003173828125 ms</p>
<p>당연하게도 <code>has</code>로 조회하는게 더 빠르다.</p>
<table>
<thead>
<tr>
<th>조회 함수</th>
<th>시간 복잡도</th>
</tr>
</thead>
<tbody><tr>
<td>Array.incldues</td>
<td>O(n)</td>
</tr>
<tr>
<td>Set.has</td>
<td>O(1)</td>
</tr>
</tbody></table>
<p>결과를 보고 <code>includes</code> 대신 무조건 <code>has</code>를 써야겠다! 라는 생각이 들 수 있다.
하지만, 항상 <code>has</code>를 사용하는게 좋은건 아니다.
두가지 경우를 보자!</p>
<h2 id="1️⃣-client에서의-has">1️⃣ client에서의 has</h2>
<p>사이즈가 큰 배열(10만 이상)을 조회하고 싶을 때
set 자료구조에 넣어서 조회하면 매우 빠르게 조회가 가능할 것이다.
하지만, <code>client</code>단에서 굳이 <code>has</code>를 사용하기 위해 <code>set</code> 에 배열 데이터를 넣어야할까?
10만 이상의 데이터를 <code>client</code>단에서 다룰일이 없다고 생각한다.
또한, 10만 이상의 데이터를 조회할 일은 더더욱 없다.</p>
<p><a href="https://stackoverflow.com/questions/39007637/javascript-set-vs-array-performance">https://stackoverflow.com/questions/39007637/javascript-set-vs-array-performance</a></p>
<p>위 글을 보면 10만 이상의 데이터를 다룰때 유의미한 차이가 발생한다.</p>
<p><img src="https://velog.velcdn.com/images/ssoon-m/post/de77594d-2364-40c9-b3ee-e607e0511158/image.png" alt="">
그리고 위 사진처럼 현재 크롬 브라우저에선 배열의 사이즈가 <code>16,777,216</code>를 넘어서면 <code>maximum size exceeded</code> 에러가 발생한다.</p>
<p>client단에선 includes를 사용하자..!</p>
<h2 id="2️⃣-server에서의-has">2️⃣ server에서의 has</h2>
<p>서버단에서는 <code>db</code>에 있는 데이터를 조회하는 일이 아니라면 <code>inlcudes</code> 대신 <code>has</code> 를 사용하면 좋을거 같다.</p>
<h2 id="결론">결론</h2>
<p>조회에 있어선 <code>has</code> 가 <code>includes</code> 보다 빠르지만, 상황에 맞게 사용해야할 거 같다.</p>
<p>잘못된 정보가 있다면 댓글로 지적 부탁드립니다!🙇‍♂️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] click 이벤트시 blur이벤트 무시하기]]></title>
            <link>https://velog.io/@ssoon-m/js-click-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EC%8B%9C-blur%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%AC%B4%EC%8B%9C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ssoon-m/js-click-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EC%8B%9C-blur%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%AC%B4%EC%8B%9C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 10 Sep 2022 09:57:40 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>어떠한 요소 <code>A</code> 가 blur 이벤트를 기다리는중이다. (요소가 focus 되어있는 상황)
이때 다른 요소 <code>B</code> 의 click 이벤트를 먼저 실행시키고 싶다면 어떻게 해야 할까?</p>
</blockquote>
<p><code>B</code>의 click 이벤트를 실행 시키면 <code>mousedown</code> -&gt; <code>blur</code> -&gt; <code>click</code>
순으로 이벤트가 실행이 된다.
브라우저의 <code>click</code> 이벤트에 대한 기본 동작이다.
위 순서를 봤을때 <code>B</code>의 <code>click</code> 이벤트가 실행이 된다면 <code>blur</code> 이벤트가 <code>click</code> 이벤트 보다 먼저 실행이 될 것이다.</p>
<h2 id="그럼-click-이벤트시-blur-이벤트를-무시하는-방법은-없을까">그럼 click 이벤트시 blur 이벤트를 무시하는 방법은 없을까?</h2>
<p><code>preventDefault()</code>메서드를 사용하면 된다.
<code>preventDefault</code>는 브라우저의 기본 동작을 실행하지 않도록 해준다.</p>
<h2 id="blur이벤트를-무시하는-법"><code>blur</code>이벤트를 무시하는 법</h2>
<ol>
<li><code>click</code> 이벤트를 달아준 <code>B</code> 에 <code>mousedown</code> 이벤트를 추가 해준다. </li>
<li><code>preventDefault</code> 메서드를 실행시켜 해당 이벤트에 대한 사용자 에이전트의 기본 동작을 실행하지 않도록 지정한다.</li>
</ol>
<p>참고자료
🔗 <a href="https://developer.mozilla.org/ko/docs/Web/API/Event/preventDefault">https://developer.mozilla.org/ko/docs/Web/API/Event/preventDefault</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Nuxt] ssr 훅 에서 인스턴스 생성시 문제점(WARN Cannot stringify arbitrary non-POJOs)]]></title>
            <link>https://velog.io/@ssoon-m/Nuxt-ssr-%ED%9B%85-%EC%97%90%EC%84%9C-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%83%9D%EC%84%B1%EC%8B%9C-%EB%AC%B8%EC%A0%9C%EC%A0%90WARN-Cannot-stringify-arbitrary-non-POJOs</link>
            <guid>https://velog.io/@ssoon-m/Nuxt-ssr-%ED%9B%85-%EC%97%90%EC%84%9C-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%83%9D%EC%84%B1%EC%8B%9C-%EB%AC%B8%EC%A0%9C%EC%A0%90WARN-Cannot-stringify-arbitrary-non-POJOs</guid>
            <pubDate>Mon, 06 Jun 2022 03:09:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>nuxt ssr훅에서 인스턴스 생성 후 해당 메서드에 접근을 할 때 왜 문제가 생길까??</p>
</blockquote>
<pre><code class="language-javascript">export default class Item {
  id;
  title;
  subTitle;
  writer;
  constructor(id, title, subTitle, writer) {
    this.id = id;
    this.title = title;
    this.subTitle = subTitle;
    this.writer = writer;
  }
  hello(): void {
    console.log(&#39;hello~~~~&#39;);
  }
}</code></pre>
<p>위의 클래스를 <code>fetch</code>나 <code>asyncData</code>훅에서 생성을 하게 되면 ssr훅의 컨텍스트가 종료가 될때 ssr에서 생성한 인스턴스의 전체 구성 요소 상태가 문자열화된다.</p>
<h2 id="⭐️-생성한-인스턴스의-메서드-접근-가능-할-때">⭐️ 생성한 인스턴스의 메서드 접근 가능 할 때</h2>
<pre><code class="language-javascript">data(){
  return{
    item : null
  }
},
fetch(){
  this.item = new Item(1,&#39;title&#39;,&#39;subtitle&#39;,&#39;writer&#39;);
  this.item.hello();
}</code></pre>
<h2 id="❗️생성한-인스턴스의-메서드-접근이-불가능-할-때">❗️생성한 인스턴스의 메서드 접근이 불가능 할 때</h2>
<pre><code class="language-javascript">mounted(){
  this.item.hello();
}</code></pre>
<p>fetch훅이 끝난 후 mounted에서 메서드에 접근을 하게 되면 메서드에 접근을 못 하게 된다.</p>
<h2 id="참고자료">참고자료</h2>
<p>🔗 <a href="https://github.com/nuxt/nuxt.js/issues/10277">https://github.com/nuxt/nuxt.js/issues/10277</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[safari 에서 화면 깜빡이는 버그]]></title>
            <link>https://velog.io/@ssoon-m/safari-transition-%EB%B2%84%EA%B7%B8</link>
            <guid>https://velog.io/@ssoon-m/safari-transition-%EB%B2%84%EA%B7%B8</guid>
            <pubDate>Sun, 22 May 2022 11:09:50 GMT</pubDate>
            <description><![CDATA[<h2 id="❗️문제점">❗️문제점...</h2>
<p>배너에 <code>splide</code> 모듈을 사용 했는데 <code>transition</code>이 일어날때 마다 <code>safari</code>에서 깜빡이면서 넘어가는 현상이 있었다.</p>
<h2 id="💡해결책">💡해결책</h2>
<p>처음엔 어떠한 이유(모듈 자체의 이슈?) 때문에 버그가 발생했는지 명확히 몰라서 헤맸는데 <code>safari transition bug</code> 라는 키워드로 검색을 해보니</p>
<pre><code class="language-css">-webkit-transform: translate3d(0, 0, 0);</code></pre>
<p>위 속성을 적용 하라는 글들이 많았다. 
깜빡거리는 요소에 추가를 해주니 정상 동작을 확인 할 수 있었다.</p>
<h2 id="속성-설명">속성 설명</h2>
<p><code>transition3d</code>로 하드웨어 가속을 사용하게 변경을 해줘서 깜빡이는 현상을 없애 준다.</p>
<p><code>transition3d</code> 대신 <code>will-change: -webkit-transform</code>을 추가해줘도 정상 동작을 한다.
하지만, 배너를 하나하나 추가 할 때마다 모든 엘리먼트에 <code>will-change</code>가 붙게 될 것이다.
너무 많은 엘리먼트에 <code>will-change</code>를 사용하게 된다면 자원을 대량으로 사용할 수 있으므로 사용하지 않았다.</p>
]]></description>
        </item>
    </channel>
</rss>