<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Coding Archive</title>
    <link>https://archive0313.tistory.com/</link>
    <description>개발 기록장</description>
    <language>ko</language>
    <pubDate>Tue, 23 Jun 2026 17:37:59 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>코등어</managingEditor>
    <image>
      <title>Coding Archive</title>
      <url>https://tistory1.daumcdn.net/tistory/5325219/attach/78e2012f1e20465f93f1fee280e60a21</url>
      <link>https://archive0313.tistory.com</link>
    </image>
    <item>
      <title>[CI/CD] GitHub Actions + Vercel CI/CD 오류 해결기</title>
      <link>https://archive0313.tistory.com/68</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Node.js 버전 문제로 Vercel 배포 실패?!?&amp;nbsp;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2118&quot; data-origin-height=&quot;1588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brzbD4/btsQVOG8cE6/abSexKu7OH1hPHYCGRzKJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brzbD4/btsQVOG8cE6/abSexKu7OH1hPHYCGRzKJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brzbD4/btsQVOG8cE6/abSexKu7OH1hPHYCGRzKJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrzbD4%2FbtsQVOG8cE6%2FabSexKu7OH1hPHYCGRzKJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2118&quot; height=&quot;1588&quot; data-origin-width=&quot;2118&quot; data-origin-height=&quot;1588&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소와 같이 코드를 수정하고 메인 브랜치에 커밋 푸시했는데, 배포가 실패했다는 에러가.. &lt;br /&gt;갑자기 요런 에러가 뜨면서 deploy가 안되는 이슈 발생!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;581&quot; data-start=&quot;566&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;요게 무슨 뜻이냐면?&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;687&quot; data-start=&quot;582&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;642&quot; data-start=&quot;582&quot;&gt;Node.js 18.x는 &lt;b&gt;지원 종료(EOL)&lt;/b&gt; 되어서 더 이상 Vercel에서 빌드할 수 없습니다.&lt;/li&gt;
&lt;li data-end=&quot;687&quot; data-start=&quot;643&quot;&gt;해결 방법은 &lt;b&gt;Node.js 22.x로 업그레이드&lt;/b&gt; 하는 것뿐입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;783&quot; data-start=&quot;689&quot; data-ke-size=&quot;size16&quot;&gt;원인을 확인해보니, 지금 프로젝트는 Node.js 18 환경에서 빌드되고 있었고, Vercel 정책상 18 버전은 더 이상 지원하지 않으므로 빌드가 중단된 것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;121&quot; data-start=&quot;101&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;✅ 수정 방법 (so simple 주의)&lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-end=&quot;731&quot; data-start=&quot;709&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;1️⃣ Vercel 프로젝트 설정&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;731&quot; data-start=&quot;709&quot;&gt;Vercel 대시보드 &amp;rarr; Project Settings &amp;rarr; Node.js Version: 22.x 로 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;  이렇게 하면 Vercel 서버에서 항상 Node.js 22 환경으로 빌드됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-end=&quot;873&quot; data-start=&quot;842&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2️⃣ GitHub Actions 환경도 맞춰주기&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;961&quot; data-start=&quot;874&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;921&quot; data-start=&quot;874&quot;&gt;현재 워크플로우는 Node.js 18 고정 &amp;rarr; Vercel과 동일하게 22로 변경&lt;/li&gt;
&lt;li data-end=&quot;961&quot; data-start=&quot;922&quot;&gt;actions/setup-node를 v4로 업데이트하면 안정성&amp;uarr;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;1000&quot; data-start=&quot;963&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;3️⃣ package.json에 Node 버전 명시 (선택)&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1759125940359&quot; class=&quot;bash custom-cursor-default-hover&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{ 
	&quot;engines&quot;: { 
    	    &quot;node&quot;: &quot;22.x&quot; 
    } 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;  이렇게 하면 팀원 로컬 환경에서도 Node.js 22 사용을 권장할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;205&quot; data-start=&quot;155&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;205&quot; data-start=&quot;155&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  기존 CI/CD Workflow 정리&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot; data-start=&quot;2601&quot; data-end=&quot;2860&quot;&gt;
&lt;li data-start=&quot;2601&quot; data-end=&quot;2699&quot;&gt;&lt;i&gt;Pull Request / Branch Push &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;CI 실행&lt;/b&gt;&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;2646&quot; data-end=&quot;2699&quot;&gt;
&lt;li data-start=&quot;2646&quot; data-end=&quot;2670&quot;&gt;Lint / Test / Build 검증&lt;/li&gt;
&lt;li data-start=&quot;2674&quot; data-end=&quot;2699&quot;&gt;Node.js 22 + Yarn 환경 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-start=&quot;2700&quot; data-end=&quot;2860&quot;&gt;&lt;i&gt;Main&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;Branch&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Push &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;CD 실행&lt;/b&gt;&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;2732&quot; data-end=&quot;2860&quot;&gt;
&lt;li data-start=&quot;2732&quot; data-end=&quot;2760&quot;&gt;vercel pull &amp;rarr; 환경 변수 불러오기&lt;/li&gt;
&lt;li data-start=&quot;2764&quot; data-end=&quot;2809&quot;&gt;vercel build --prod &amp;rarr; Actions runner에서 빌드&lt;/li&gt;
&lt;li data-start=&quot;2813&quot; data-end=&quot;2860&quot;&gt;vercel deploy --prebuilt --prod &amp;rarr; 빌드 결과물 배포&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;205&quot; data-start=&quot;155&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  GitHub Actions 수정(Node.js 22 + Yarn 캐시 추가)&lt;/span&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;ci.yml 기존 ver.&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1759126389999&quot; class=&quot;bash custom-cursor-default-hover&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
  with:
    node-version: '18'
- run: yarn install --frozen-lockfile
- run: yarn build
...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ci.yml 수정 ver.&lt;/p&gt;
&lt;pre id=&quot;code_1759126434646&quot; class=&quot;bash custom-cursor-default-hover&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: CI
on:
  pull_request:
    branches:
      - main
jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'yarn'
      - run: yarn install --frozen-lockfile
      - run: yarn lint

  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'yarn'
      - run: yarn install --frozen-lockfile
      - run: yarn build
        env:
          NEXT_PUBLIC_BASE_URL: ${{ secrets.NEXT_PUBLIC_BASE_URL }}

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'yarn'
      - run: yarn install --frozen-lockfile
      - run: yarn test&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cd.yml 기존 ver.&lt;/p&gt;
&lt;pre id=&quot;code_1759128561245&quot; class=&quot;bash custom-cursor-default-hover&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
Deploy-Production: 
    runs-on: ubuntu-latest 
    steps: 
      - uses: actions/checkout@v2 
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cd.yml 수정 ver.&lt;/p&gt;
&lt;pre id=&quot;code_1759126675136&quot; class=&quot;bash custom-cursor-default-hover&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: CD(Vercel Production Deployment)

env:
  VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
  VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
on:
  push:
    branches:
      - main
jobs:
  Deploy-Production:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'yarn'

      - name: Install Vercel CLI
        run: npm install --global vercel@latest

      - name: Pull Vercel Environment Information
        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}

      - name: Build Project Artifacts
        run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}

      - name: Deploy Project Artifacts to Vercel
        run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;2449&quot; data-start=&quot;2436&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  변경 포인트&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2575&quot; data-start=&quot;2450&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2477&quot; data-start=&quot;2450&quot;&gt;actions/checkout &amp;rarr; v4&lt;/li&gt;
&lt;li data-end=&quot;2507&quot; data-start=&quot;2478&quot;&gt;actions/setup-node &amp;rarr; v4&lt;/li&gt;
&lt;li data-end=&quot;2527&quot; data-start=&quot;2508&quot;&gt;Node.js 18 &amp;rarr; 22&lt;/li&gt;
&lt;li data-end=&quot;2575&quot; data-start=&quot;2528&quot;&gt;cache: 'yarn' 추가 &amp;rarr; 의존성 설치 속도 &amp;uarr;, 빌드 속도 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-start=&quot;2649&quot; data-end=&quot;2658&quot; data-ke-size=&quot;size26&quot;&gt; 이슈 해결!&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2128&quot; data-origin-height=&quot;1216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/deptuM/btsQTj9whnZ/Eeyy6mz64P6VFNkzIvwPB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/deptuM/btsQTj9whnZ/Eeyy6mz64P6VFNkzIvwPB0/img.png&quot; data-alt=&quot;CI/CD 정상작동&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/deptuM/btsQTj9whnZ/Eeyy6mz64P6VFNkzIvwPB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdeptuM%2FbtsQTj9whnZ%2FEeyy6mz64P6VFNkzIvwPB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2128&quot; height=&quot;1216&quot; data-origin-width=&quot;2128&quot; data-origin-height=&quot;1216&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CI/CD 정상작동&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;2674&quot; data-start=&quot;2659&quot; data-ke-size=&quot;size16&quot;&gt;이번 경험을 통해 배운 점은 Vercel에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;지원되는 Node.js 버전 확인은 필수다!&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;280&quot; data-start=&quot;183&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;280&quot; data-start=&quot;183&quot; data-ke-size=&quot;size16&quot;&gt;모쪼록 별 일 아니지만, &lt;b&gt;CI/CD 초보로서 처음 겪은 이슈&lt;/b&gt;라 공유드리고자 글을 작성했습니다.&lt;br /&gt;다른 분들도 혹시나 비슷한 상황을 만나신다면 참고가 되길 바랍니다.  &lt;/p&gt;</description>
      <category>  Programming/Development Environment</category>
      <category>CI/CD</category>
      <category>frontend</category>
      <category>Github Actions</category>
      <category>next.js</category>
      <category>vercel</category>
      <author>코등어</author>
      <guid isPermaLink="true">https://archive0313.tistory.com/68</guid>
      <comments>https://archive0313.tistory.com/68#entry68comment</comments>
      <pubDate>Mon, 29 Sep 2025 16:25:12 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js] Next.js로 HttpOnly 쿠키 + BFF(프록시) 아키텍처로 인증 보안/성능 잡기(번외편) - 쿠키 라이브러리 대신 BFF 아키텍처를 택한 이유</title>
      <link>https://archive0313.tistory.com/66</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1696&quot; data-origin-height=&quot;1011&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsdSot/btsP2638Epu/Tkb0uWO4S7FokQZKfiH8dK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsdSot/btsP2638Epu/Tkb0uWO4S7FokQZKfiH8dK/img.png&quot; data-alt=&quot;쿠키 라이브러리 대신 BFF 아키텍처를 택한 이유 이미지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsdSot/btsP2638Epu/Tkb0uWO4S7FokQZKfiH8dK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdsdSot%2FbtsP2638Epu%2FTkb0uWO4S7FokQZKfiH8dK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1696&quot; height=&quot;1011&quot; data-origin-width=&quot;1696&quot; data-origin-height=&quot;1011&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;쿠키 라이브러리 대신 BFF 아키텍처를 택한 이유 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;956&quot; data-end=&quot;1019&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;956&quot; data-end=&quot;1019&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;956&quot; data-end=&quot;1019&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;이전에 올린 글&quot; href=&quot;https://archive0313.tistory.com/65&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전에 올린 글&lt;/a&gt;에서 &lt;b&gt;Next.js BFF(Backend For Frontend, 프록시) 아키텍처&lt;/b&gt;를 적용해, HttpOnly 쿠키 기반 인증으로 보안과 성능을 개선하는 과정을 소개했습니다.&lt;/p&gt;
&lt;p data-end=&quot;444&quot; data-start=&quot;403&quot; data-ke-size=&quot;size16&quot;&gt;그런데 프록시 구조를 도입하기 전, 이런 의문이 들었습니다.&lt;/p&gt;
&lt;blockquote data-end=&quot;505&quot; data-start=&quot;446&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p data-end=&quot;505&quot; data-start=&quot;448&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;굳이 프록시까지 쓰지 않고, 쿠키 라이브러리 + 인터셉터 구조로 해결할 수 있지 않을까?&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;631&quot; data-start=&quot;507&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;631&quot; data-start=&quot;507&quot; data-ke-size=&quot;size16&quot;&gt;실제로 이전의 프로젝트에서 &lt;b&gt;react-cookie + Axios 인터셉터&lt;/b&gt; 조합으로 인증을 구현한 적이 있었고, 작은 규모의 프로젝트에서 이 방식이 꽤 단순하고 빠르게 적용할 수 있어서 나쁘지 않았습니다.&lt;/p&gt;
&lt;p data-end=&quot;631&quot; data-start=&quot;507&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;631&quot; data-start=&quot;507&quot; data-ke-size=&quot;size16&quot;&gt;하지만, &lt;b&gt;쿠키 라이브러리만으로는 HttpOnly 쿠키의 보안 장점을 살릴 수 없었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;631&quot; data-start=&quot;507&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서는 그 이유와 함께 각 방식을 자세히 비교해 보겠습니다.&lt;/p&gt;
&lt;p data-end=&quot;631&quot; data-start=&quot;507&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;631&quot; data-start=&quot;507&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;631&quot; data-start=&quot;507&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-end=&quot;664&quot; data-start=&quot;625&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-end=&quot;664&quot; data-start=&quot;625&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;i&gt;대부분의&amp;nbsp;&lt;b&gt;프론트엔드 쿠키 라이브러리&lt;/b&gt;는 사실상&amp;nbsp;&lt;b&gt;HttpOnly 쿠키를 다룰 수 없다!&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;HttpOnly 쿠키는 브라우저 JS에서 접근 불가&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;294&quot; data-start=&quot;127&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;185&quot; data-start=&quot;127&quot;&gt;이건 브라우저의 보안 정책(Web Standard)이라, 어떤 JS 라이브러리든 뚫을 수 없습니다.&lt;/li&gt;
&lt;li data-end=&quot;294&quot; data-start=&quot;188&quot;&gt;react-cookie, next-cookie, universal-cookie, js-cookie 전부 공통적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;HttpOnly 쿠키를 읽거나 쓸 수 없습니다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;정리하면, &lt;u&gt;&lt;b&gt;쿠키 라이브러리는 사실상 non-HttpOnly 쿠키(=JS 접근 가능한 쿠키)를 다루는 도구&lt;/b&gt;&lt;/u&gt;일 뿐입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style4&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;! 여기서 잠시 주요 쿠키 라이브러리별 제약사항을 살펴보겠습니다. 중요한 내용은 아니니 결론만 보고 넘어가도 상관없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✔️ 주요 쿠키 라이브러리별 제약사항&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. react-cookie&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript custom-cursor-default-hover&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;import { useCookies } from 'react-cookie';

function MyComponent() {
  const [cookies, setCookie] = useCookies(['token']);
  
  // ❌ HttpOnly 쿠키는 읽을 수 없음
  console.log(cookies.token); // undefined (HttpOnly인 경우)
  
  // ❌ HttpOnly 쿠키는 설정할 수 없음 (클라이언트에서)
  setCookie('token', 'value', { httpOnly: true }); // 무시됨
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;706&quot; data-start=&quot;580&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;734&quot; data-start=&quot;697&quot;&gt;클라이언트에서 document.cookie를 감싸는 래퍼&lt;/li&gt;
&lt;li data-end=&quot;783&quot; data-start=&quot;737&quot;&gt;SSR에서는 req.headers.cookie를 파싱하는 보조 유틸 정도&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. next-cookie / cookies-next&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript custom-cursor-default-hover&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;import { getCookie, setCookie } from 'cookies-next';

// ❌ HttpOnly 쿠키 읽기 불가능
const token = getCookie('httponly-token'); // undefined

// ❌ 클라이언트에서 HttpOnly 쿠키 설정 불가능
setCookie('token', 'value', { httpOnly: true }); // 브라우저에서 무시&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;891&quot; data-start=&quot;730&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;774&quot; data-start=&quot;730&quot;&gt;Next.js의 SSR/서버 컴포넌트에서 쿠키를 편하게 읽고 쓰는 용도.&lt;/li&gt;
&lt;li data-end=&quot;903&quot; data-start=&quot;849&quot;&gt;서버 코드에서는 NextResponse.cookies.set()로 HttpOnly 가능&lt;/li&gt;
&lt;li data-end=&quot;931&quot; data-start=&quot;906&quot;&gt;하지만 클라이언트 JS에서는 역시 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. js-cookie / universal-cookie&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;cs custom-cursor-default-hover&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;import Cookies from 'js-cookie';

// ❌ HttpOnly 쿠키 접근 불가능
Cookies.get('httponly-token'); // undefined

// ❌ HttpOnly 옵션 자체가 지원되지 않음
Cookies.set('token', 'value'); // 항상 non-HttpOnly&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;557&quot; data-end=&quot;985&quot;&gt;
&lt;li data-end=&quot;954&quot; data-start=&quot;932&quot;&gt;순수 클라이언트 쿠키 라이브러리.&lt;/li&gt;
&lt;li data-end=&quot;985&quot; data-start=&quot;957&quot;&gt;역시 HttpOnly 쿠키는 다룰 수 없음.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style4&quot; /&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;992&quot; data-end=&quot;996&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;992&quot; data-end=&quot;996&quot; data-ke-size=&quot;size23&quot;&gt;✅&amp;nbsp; 결론적으로 쿠키 라이브러리는&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;next-cookie, react-cookie도&amp;nbsp;&lt;b&gt;HttpOnly 쿠키 자체를 JS에서 제어할 수는 없습&lt;/b&gt;니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;HttpOnly 쿠키는 서버(Set-Cookie 헤더)에서만 제어 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;쿠키 라이브러리는 어디까지나&amp;nbsp;&lt;b&gt;non-HttpOnly 쿠키용 편의 기능&lt;/b&gt;이라고 보면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-start=&quot;1229&quot; data-end=&quot;1246&quot;&gt;&lt;b&gt;  쿠키 인증 방식 Trade-off 비교&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 쿠키 라이브러리 + 인터셉터 방식&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript custom-cursor-default-hover&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// Axios 인터셉터 예시
import { getCookie } from 'cookies-next';
import axios from 'axios';

axios.interceptors.request.use((config) =&amp;gt; {
  const token = getCookie('token'); // ⚠️ non-HttpOnly만 가능
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. BFF(프록시) + HttpOnly 쿠키 방식&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript custom-cursor-default-hover&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// Next.js API Route
export async function POST(request) {
  // ✅ HttpOnly 쿠키에서 토큰 읽기
  const token = request.cookies.get('auth-token')?.value;
  
  if (!token) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }
  
  // ✅ 백엔드에 인증된 요청 전달
  const response = await fetch('https://api.example.com/data', {
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    }
  });
  
  return NextResponse.json(await response.json());
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 245px;&quot; border=&quot;1&quot; data-end=&quot;645&quot; data-start=&quot;97&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style14&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 37px;&quot;&gt;
&lt;td style=&quot;height: 37px;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;height: 37px;&quot;&gt;&lt;b&gt;쿠키 라이브러리 + 인터셉터 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 37px;&quot;&gt;&lt;b&gt;BFF(프록시) + HttpOnly 쿠키 방식&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 74px;&quot; data-end=&quot;383&quot; data-start=&quot;228&quot;&gt;
&lt;td style=&quot;height: 74px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;237&quot; data-start=&quot;228&quot;&gt;&lt;b&gt;보안&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 74px;&quot; data-end=&quot;304&quot; data-start=&quot;237&quot; data-col-size=&quot;md&quot;&gt;❌ HttpOnly 불가 &amp;rarr; XSS 취약&lt;br /&gt;❌ 토큰이 JS 런타임에 노출&lt;br /&gt;⚠️ CSRF 방어 따로 필요&lt;/td&gt;
&lt;td style=&quot;height: 74px;&quot; data-end=&quot;383&quot; data-start=&quot;304&quot; data-col-size=&quot;md&quot;&gt;✅ HttpOnly 쿠키 사용 가능&lt;br /&gt;✅ 토큰 JS 접근 불가 &amp;rarr; XSS 방어&lt;br /&gt;✅ CSRF 토큰 검증을 서버에서 통합 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 76px;&quot; data-end=&quot;513&quot; data-start=&quot;384&quot;&gt;
&lt;td style=&quot;height: 76px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;403&quot; data-start=&quot;384&quot;&gt;&lt;b&gt;편의성 (개발 난이도)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 76px;&quot; data-end=&quot;466&quot; data-start=&quot;403&quot; data-col-size=&quot;md&quot;&gt;✅ 구현 간단&lt;br /&gt;✅ 빠른 개발/프로토타입 적합&lt;br /&gt;✅ 라이브러리(document.cookie) 기반&lt;/td&gt;
&lt;td style=&quot;height: 76px;&quot; data-end=&quot;513&quot; data-start=&quot;466&quot; data-col-size=&quot;md&quot;&gt;❌ 프록시 서버 필요&lt;br /&gt;❌ 코드 구조 복잡&lt;br /&gt;❌ 초기 개발 비용 &amp;uarr;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 58px;&quot; data-end=&quot;645&quot; data-start=&quot;514&quot;&gt;
&lt;td style=&quot;height: 58px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;533&quot; data-start=&quot;514&quot;&gt;&lt;b&gt;확장성 (장기적 운영)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 58px;&quot; data-end=&quot;582&quot; data-start=&quot;533&quot; data-col-size=&quot;md&quot;&gt;❌ 보안 문제로 대규모 서비스에 부적합&lt;br /&gt;❌ API Gateway 역할 불가능&lt;/td&gt;
&lt;td style=&quot;height: 58px;&quot; data-end=&quot;645&quot; data-start=&quot;582&quot; data-col-size=&quot;md&quot;&gt;✅ 보안 확장성 높음&lt;br /&gt;✅ 로깅/레이트리밋/A/B 테스트 등 제어 가능&lt;br /&gt;✅ 실서비스 표준 패턴&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style4&quot; /&gt;
&lt;p data-end=&quot;1619&quot; data-start=&quot;1499&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1619&quot; data-start=&quot;1499&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1619&quot; data-start=&quot;1499&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1619&quot; data-start=&quot;1499&quot; data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;쿠키 라이브러리&lt;/b&gt; =&amp;gt; 간단하지만 HttpOnly 쿠키를 못 쓰므로 &lt;b&gt;XSS 취약&lt;/b&gt;&lt;/u&gt;합니다.&lt;br /&gt;&lt;u&gt;&lt;b&gt;프록시 방식&lt;/b&gt; =&amp;gt; 코드가 좀 복잡해지지만, &lt;b&gt;보안 업계 표준&lt;/b&gt;이며 장기적으로는 &lt;b&gt;확장성&lt;/b&gt;도 좋습&lt;/u&gt;니다.&lt;/p&gt;
&lt;p data-end=&quot;1619&quot; data-start=&quot;1499&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1619&quot; data-start=&quot;1499&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-end=&quot;1619&quot; data-start=&quot;1499&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1619&quot; data-start=&quot;1499&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1619&quot; data-start=&quot;1499&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;576&quot; data-start=&quot;493&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;&amp;ldquo;혹시 쿠키 라이브러리로 보안 문제를 간단히 해결할 수 있지 않을까?&amp;rdquo;라는&lt;/b&gt; 의문에서 시작해, 직접 시도해 본 결과를 정리했습니다.&lt;/p&gt;
&lt;p data-end=&quot;409&quot; data-start=&quot;190&quot; data-ke-size=&quot;size16&quot;&gt;결론은 단순합니다.&lt;br /&gt;쿠키 라이브러리는 결국 &lt;b&gt;non-HttpOnly 쿠키&lt;/b&gt;만 다룰 수 있고, 보안 취약점을 피할 수 없습니다.&lt;br /&gt;저 역시 이전 프로젝트에서는 &lt;b&gt;react-cookie + 인터셉터 구조&lt;/b&gt;를 활용했지만, 곧 보안상의 한계를 체감하게 되었고, 이번에는 보다 안전한 구조인 &lt;b&gt;BFF(프록시) + HttpOnly 쿠키 아키텍처&lt;/b&gt;로 발전시켜 개발하게 되었습니다.&lt;/p&gt;
&lt;p data-end=&quot;593&quot; data-start=&quot;532&quot; data-ke-size=&quot;size16&quot;&gt;앞으로는 이 경험을 토대로 더욱 안정적이고 확장성 있는 인증/보안 아키텍처를 설계할 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-end=&quot;2500&quot; data-start=&quot;2489&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Programming/Next.js &amp;amp; project</category>
      <category>BFF 아키텍처</category>
      <category>cookie 라이브러리</category>
      <category>HttpOnly Cookie</category>
      <category>next.js</category>
      <author>코등어</author>
      <guid isPermaLink="true">https://archive0313.tistory.com/66</guid>
      <comments>https://archive0313.tistory.com/66#entry66comment</comments>
      <pubDate>Sat, 23 Aug 2025 13:20:05 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js] Next.js로 HttpOnly 쿠키 + BFF(프록시)아키텍처로 인증 보안/성능 잡기 - 클라이언트 사이드 인증의 보안 문제와 HttpOnly 쿠키를 활용한 개선 방법</title>
      <link>https://archive0313.tistory.com/65</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1693&quot; data-origin-height=&quot;1013&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nn7WQ/btsP1X1cJV8/wXvxpdCP8N7bLfLM5Lhywk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nn7WQ/btsP1X1cJV8/wXvxpdCP8N7bLfLM5Lhywk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nn7WQ/btsP1X1cJV8/wXvxpdCP8N7bLfLM5Lhywk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnn7WQ%2FbtsP1X1cJV8%2FwXvxpdCP8N7bLfLM5Lhywk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1693&quot; height=&quot;1013&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1693&quot; data-origin-height=&quot;1013&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Next.js 프로젝트를 리팩토링하는 과정에서 예상치 못한 보안 이슈를 발견하게 되었습니다. 바로 &lt;b&gt;브라우저의 개발자 도구에서 쿠키에 저장된 토큰이 그대로 노출되고 있었다는 점&lt;/b&gt;입니다. 토큰은 사용자 인증과 보안을 위해 가장 중요한 값 중 하나인데, 이 값이 그대로 클라이언트 측에서 확인 가능하다면 XSS(교차 사이트 스크립트 공격)나 토큰 탈취 등의 보안 취약점으로 이어질 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;454&quot; data-start=&quot;276&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이를 개선하기 위해 기존 코드 구조와 인증 방식을 다시 점검하게 되었고, 특히 &lt;b&gt;토큰을 어디에 저장하고, 어떻게 전달할 것인가&lt;/b&gt;라는 주제에 집중했습니다. 프론트엔드와 백엔드 간 통신 구조, 쿠키 옵션 설정(HttpOnly, Secure 등), 프록시 서버의 역할까지 전반적으로 살펴보면서 코드를 수정해 나갔습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;589&quot; data-start=&quot;456&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이번 글에서는 제가 겪은 문제 상황과, 그것을 어떻게 개선했는지에 대한 과정을 정리해 보려고 합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;기존 코드: &lt;u&gt;클라이언트(JS)에서 토큰을 직접 저장/주입&lt;/u&gt; &amp;rarr; &lt;i&gt;&lt;b&gt;HttpOnly 사용 불가, XSS/탈취 위험&lt;/b&gt;&lt;/i&gt;.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;리팩토링: Next.js &lt;b&gt;&lt;i&gt;서버가&lt;/i&gt;&lt;/b&gt; 토큰을 &lt;i&gt;&lt;b&gt;HttpOnly 쿠키로만 관리&lt;/b&gt;&lt;/i&gt;하고, 클라이언트 &lt;b&gt;&lt;i&gt;보호 API는 프록시(BFF)로 호출&lt;/i&gt;&lt;/b&gt;.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt; &amp;nbsp;기존 코드 문제 짚어보기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;1️⃣ XSS 공격에 완전히 노출된 토큰&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1755841927143&quot; class=&quot;typescript custom-cursor-default-hover&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 기존 AuthService.ts
class AuthService {
  private static userAccount: string | null = StorageService.get('userAccount');
  private static token: string | null = StorageService.get('token');

  static login(userAccount: string, token: string): void {
    this.userAccount = userAccount;
    this.token = token;
    StorageService.setStorageAdapter('localStorage');
    StorageService.set('userAccount', userAccount);
    StorageService.setStorageAdapter('cookieStorage');
    StorageService.set('token', token, {
      expires: 3,
      path: '',
    });
  }

  static getToken(): string | null {
    return this.token || StorageService.get('token');
  }
}

// LoginPage에서의 사용
const onSubmit = async (data: ILoginRequest) =&amp;gt; {
  try {
    const response = await login(data);
    const { token, accountname } = response.user;
    if (token &amp;amp;&amp;amp; accountname) {
      AuthService.login(accountname, token); // 클라이언트에서 직접 저장
      goTo('/feed');
    }
  } catch (error) {
    console.error('로그인 에러:', error);
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;JavaScript로 쿠키를 set/get 하면&amp;nbsp;HttpOnly를 사용할 수 없어 XSS가 터지면 토큰 탈취가 가능합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;토큰이 일반 쿠키나 localStorage에 저장되어 JavaScript로 접근이 가능합니다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;아래와 같이 XSS 공격자가 단 한 줄의 JavaScript 코드로 사용자의 인증 토큰을 탈취할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1756025011204&quot; class=&quot;typescript custom-cursor-default-hover&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 악성 스크립트가 쉽게 토큰을 탈취할 수 있음
console.log(document.cookie); // 토큰이 그대로 노출
console.log(localStorage.getItem('token')); // 토큰 직접 접근 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;617&quot; data-start=&quot;604&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;617&quot; data-start=&quot;604&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;2️⃣ CSRF 공격 방어 부족&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;gams custom-cursor-default-hover&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 기존 코드 - SameSite 보호 없음
StorageService.set('token', token, {
  expires: 3,
  path: '', // SameSite, Secure 옵션 없음
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;일반 쿠키는 SameSite 설정이 없으면 모든 도메인에서 자동으로 전송됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;악성 사이트에서 사용자 모르게 요청을 보내면 브라우저가 자동으로 토큰을 포함해서 전송합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;617&quot; data-start=&quot;604&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;617&quot; data-start=&quot;604&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;3️⃣ 복잡한 저장소 관리 로직&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript custom-cursor-default-hover&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// 저장소 타입을 계속 바꾸는 복잡한 로직
StorageService.setStorageAdapter('localStorage');
StorageService.set('userAccount', userAccount);
StorageService.setStorageAdapter('cookieStorage');
StorageService.set('token', token, { expires: 3, path: '' });&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;localStorage와 cookieStorage를 번갈아 사용하는 복잡한 로직으로 인한 버그 가능성이 높습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;직접적인 보안 위험은 아니지만, 복잡성으로 인해 다른 보안 버그가 발생할 가능성이 높습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style4&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;문제 흐름을 정리해 보면&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;AuthService.login()이&amp;nbsp;localStorage + cookieStorage에 토큰을 저장&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;StorageService는 JS에서 쿠키를 직접 set() (HttpOnly 설정 불가)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Axios 인스턴스/인터셉터는 클라이언트에서 토큰을 헤더에 붙여 전송하는 흐름&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;1019&quot; data-start=&quot;956&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;  &lt;/b&gt;지금 구조는 구현은 간단하지만,&lt;b&gt;&lt;span style=&quot;color: #f89009;&quot;&gt; 실서비스 보안 기준(특히 XSS/토큰 보호)에선 취약&lt;/span&gt;&lt;/b&gt;합니다.&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;  Next.js HttpOnly 쿠키를 활용한 보안 개선하기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;1. 개선 목표: 프록시 패턴으로 기존 코드 최대한 보존하며 리팩토링하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;기존 코드의 95%를 그대로 유지하면서 보안만 크게 개선하는 방법 사용하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros custom-cursor-default-hover&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;기존: 클라이언트 &amp;rarr; 외부 API &amp;rarr; 응답 &amp;rarr; 클라이언트에서 쿠키 저장
개선: 클라이언트 &amp;rarr; Next.js API Route &amp;rarr; 외부 API &amp;rarr; 서버에서 HttpOnly 쿠키 저장&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1755842144889&quot; class=&quot;typescript custom-cursor-default-hover&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// 폴더 구조
frontend/
├── app/
│   ├── api/                # ✅ Next.js API Route Handlers (BFF)
│   │   ├── user/
│   │   │   └── login/
│   │   │       └── route.ts    # POST /api/user/login
│   │   └── post/
│   │       └── feed/
│   │           └── route.ts    # GET /api/post/feed
│   ├── (auth)/
│   │   └── login/page.tsx
│   └── feed/page.tsx
│
├── src/
│   ├── api/
│   │   ├── apiRequests/     # ✅ 클라이언트에서 호출하는 함수 모음
│   │   │   ├── auth.ts      # login, signup 등
│   │   │   └── post.ts      # postList 등
│   │   └── index.ts         # axios instance
│   │
│   ├── components/          # 공통 컴포넌트
│   ├── services/            # AuthService, StorageService 등 (클라 전용)
│   └── queries/             # react-query hooks
│
└── .env.local               # BACKEND_BASE_URL 등 환경변수&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-end=&quot;1065&quot; data-start=&quot;1026&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;1065&quot; data-start=&quot;1026&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;2. 개선 방향: Next BFF(프록시) + HttpOnly 쿠키&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1378&quot; data-start=&quot;1077&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1157&quot; data-start=&quot;1077&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;로그인&lt;/b&gt;: Next 서버 라우트가 백엔드 로그인 API를 호출 &amp;rarr; 응답받은 토큰을 &lt;b&gt;HttpOnly 쿠키&lt;/b&gt;로 &lt;b&gt;서버가&lt;/b&gt; 설정.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1263&quot; data-start=&quot;1158&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;보호 API 호출&lt;/b&gt;: 클라이언트는 백엔드로 직접 가지 않고 &lt;b&gt;Next 프록시&lt;/b&gt;로 호출 &amp;rarr; 프록시가 쿠키에서 토큰을 꺼내 &lt;b&gt;Authorization 헤더를 서버에서만 주입&lt;/b&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1343&quot; data-start=&quot;1264&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;세션 확인&lt;/b&gt;: 서버 라우트에서 쿠키 토큰으로 확인 &amp;rarr; 클라에는 &amp;ldquo;인증됨/아님 + 최소 정보&amp;rdquo;만 전달.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1378&quot; data-start=&quot;1344&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;클라이언트는 토큰을 영영 모른다.&lt;/b&gt; (이게 포인트!)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1875&quot; data-start=&quot;1847&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;1875&quot; data-start=&quot;1847&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;3. 단계별 리팩토링 &lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-end=&quot;1875&quot; data-start=&quot;1847&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;클라이언트에서 토큰 다루는 코드 제거 &amp;rarr; Next 라우트/프록시에서만 쿠키/토큰 제어&lt;/span&gt;&lt;/blockquote&gt;
&lt;h4 data-end=&quot;664&quot; data-start=&quot;625&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;1️⃣ 보안 강화된 로그인 API Route 생성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755842669092&quot; class=&quot;typescript custom-cursor-default-hover&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// app/api/auth/login/route.ts (BFF, Next.js 서버에서만 실행)
import { cookies } from 'next/headers'; // (서버 컴포넌트나 Route Handler(app/api)에서만 호출 가능한 서버 컴포넌트 전용 API)
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const body = await request.json();

  // 1. 기존 외부 API 호출
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_BASE_URL}/user/login`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    },
  );

  const data = await response.json();

  if (response.ok &amp;amp;&amp;amp; data.user?.token) {
    // 2.  ️ HttpOnly 쿠키로 보안 저장
    cookies().set('token', data.user.token, {
      httpOnly: true, // XSS 공격 방어
      secure: process.env.NODE_ENV === 'production', // HTTPS에서만 전송
      sameSite: 'strict', // CSRF 공격 방어
      maxAge: 60 * 60 * 24 * 3, // 3일
      path: '/',
    });

    // 3.   클라이언트에는 토큰 없이 안전한 응답
    return NextResponse.json({
      ...data,
      user: {
        ...data.user,
        token: undefined, // 토큰 제거로 클라이언트 노출 방지
      },
    });
  }

  return NextResponse.json(data, { status: response.status });
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;로그인 요청 후 토큰을 HttpOnly + Secure 쿠키에 저장&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1052&quot; data-start=&quot;1025&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;HttpOnly 쿠키를 읽거나 세팅할 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1078&quot; data-start=&quot;1053&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;백엔드 API에 요청을 &lt;b&gt;대리&lt;/b&gt;로 보냄&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;664&quot; data-start=&quot;625&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;2️⃣ Axios 인스턴스 프록시를 기본 baseURL로&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755842866161&quot; class=&quot;typescript custom-cursor-default-hover&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/api/index.ts
import axios, { AxiosInstance } from 'axios';

import { handleRequest, handleRequestError } from './helper/requestHandlers';
import { handleResponse, handleResponseError } from './helper/responseHandlers';

const instance: AxiosInstance = axios.create({
  baseURL: '/api', // Next.js의 API 라우트 사용 (기존: process.env.NEXT_PUBLIC_BASE_URL)
  withCredentials: true, // 쿠키 자동 전송
});

instance.interceptors.request.use(handleRequest, handleRequestError);
instance.interceptors.response.use(handleResponse, handleResponseError);

export default instance;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;664&quot; data-start=&quot;625&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;3️⃣ AuthService/StorageService 정리 및 로그인 페이지&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755843039320&quot; class=&quot;typescript custom-cursor-default-hover&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// services/AuthService.ts
import StorageService from './StorageService';

class AuthService {
  private static userAccount: string | null = StorageService.get('userAccount');

  static login(userAccount: string): void {
    this.userAccount = userAccount;
    StorageService.setStorageAdapter('localStorage');
    StorageService.set('userAccount', userAccount);
  }
...
}

export default AuthService;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;6971&quot; data-start=&quot;6898&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;6927&quot; data-start=&quot;6898&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;토큰을 저장/조회하는 모든 메서드 삭제&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;664&quot; data-start=&quot;625&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;4️⃣ 피드 라우트 생성 (헤더에 토큰 주입)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755843591849&quot; class=&quot;typescript custom-cursor-default-hover&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// app/api/post/feed/route.ts
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const limit = searchParams.get('limit');
  const skip = searchParams.get('skip');

  const token = cookies().get('token')?.value;

  const res = await fetch(
    `${process.env.BACKEND_BASE_URL}/post/feed/?limit=${limit}&amp;amp;skip=${skip}`,
    {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        ...(token ? { Authorization: `Bearer ${token}` } : {}),
      },
      cache: 'no-store', // 항상 최신 데이터
    },
  );

  const data = await res.json();
  return NextResponse.json(data);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2120&quot; data-start=&quot;2063&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;클라이언트 &amp;rarr; BFF 요청 시 &lt;b&gt;쿠키 포함&lt;/b&gt; (withCredentials: true)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2181&quot; data-start=&quot;2121&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;BFF &amp;rarr; 백엔드 요청 시 &lt;b&gt;쿠키에서 token 추출 &amp;rarr; Authorization 헤더&lt;/b&gt;로 변환&lt;b&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-end=&quot;1019&quot; data-start=&quot;956&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;클라이언트는 단순히 /api/... 만 호출하면 되고&lt;/b&gt;,&lt;b&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;보안/토큰 관리는 전부&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Next.js 서버 (app/api)&lt;/b&gt;가 처리합니다.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;h3 data-end=&quot;1955&quot; data-start=&quot;1941&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h2 data-end=&quot;1955&quot; data-start=&quot;1941&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  최종 구조 흐름&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1756029861255&quot; class=&quot;bash custom-cursor-default-hover&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[클라이언트 컴포넌트]
      │
      ▼
로그인 요청 (email, password)
      │
      ▼
Axios &amp;rarr; /api/auth/login
      │
      ▼
[Next.js Route Handler (/api/auth/login)]
  - 백엔드로 로그인 요청 전달
  - 성공 시 JWT 토큰 수신
  - Response 쿠키(token=..., HttpOnly)로 저장
      │
      ▼
브라우저 저장소
  - HttpOnly 쿠키(token) 자동 보관
  - JS에서 직접 접근 불가
      │
      ▼
──────────────────────────────
      이후 API 요청 흐름
──────────────────────────────
      │
      ▼
클라이언트에서 postList(limit, skip) 호출
      │
      ▼
Axios &amp;rarr; /api/post/feed
      │
      ▼
[Next.js Route Handler (/api/post/feed)]
  - cookies() 로 HttpOnly 토큰 꺼냄
  - Authorization: Bearer &amp;lt;token&amp;gt; 헤더 붙임
      │
      ▼
[백엔드 API 서버 호출]
  - 토큰 검증
  - 데이터 반환
      │
      ▼
Next.js Route Handler &amp;rarr; 클라이언트 응답&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;664&quot; data-start=&quot;625&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;664&quot; data-start=&quot;625&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt; ️ 보안 개선 효과&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Before vs After 비교&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmZtlF/btsP454rx7h/GB8K2FoAknGcjHJjgRbm0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmZtlF/btsP454rx7h/GB8K2FoAknGcjHJjgRbm0k/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1690&quot; data-origin-height=&quot;968&quot; data-filename=&quot;스크린샷 2025-08-24 오후 5.30.35.png&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmZtlF/btsP454rx7h/GB8K2FoAknGcjHJjgRbm0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmZtlF%2FbtsP454rx7h%2FGB8K2FoAknGcjHJjgRbm0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1690&quot; height=&quot;968&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dH1BwP/btsP3AqEgf7/tTtqFUt1q2he6c0An3R7T1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dH1BwP/btsP3AqEgf7/tTtqFUt1q2he6c0An3R7T1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1690&quot; data-origin-height=&quot;968&quot; data-filename=&quot;스크린샷 2025-08-24 오후 5.31.54.png&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dH1BwP/btsP3AqEgf7/tTtqFUt1q2he6c0An3R7T1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdH1BwP%2FbtsP3AqEgf7%2FtTtqFUt1q2he6c0An3R7T1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1690&quot; height=&quot;968&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;HttpOnly 쿠키 리팩토링 전/후&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style13&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #6ed3d8; color: #ffffff; width: 12.6745%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;보안 요소&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #6ed3d8; color: #ffffff; width: 40%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;기존 방식&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #6ed3d8; color: #ffffff; width: 47.2093%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;개선된 방식&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef; width: 12.6745%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;XSS 방어&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;❌ JavaScript로 토큰 접근 가능&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.2093%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;✅ HttpOnly로 JavaScript 접근 차단&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef; width: 12.6745%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;CSRF 방어&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9; width: 40%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;❌ SameSite 설정 없음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9; width: 47.2093%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;✅ SameSite=strict로 완벽 차단&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef; width: 12.6745%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;HTTPS 강제&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;❌ HTTP에서도 토큰 전송&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.2093%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;✅ Secure 속성으로 HTTPS만 허용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef; width: 12.6745%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;토큰 노출&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9; width: 40%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;❌ 개발자 도구에서 완전 노출 -&amp;gt; 탈취 가능&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9; width: 47.2093%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;✅ 서버에서만 접근 가능 -&amp;gt; 탈취 불가&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef; width: 12.6745%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;코드 복잡성&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 40%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;❌ 복잡한 저장소 로직&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.2093%;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;✅ 간단하고 명확한 구조&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-start=&quot;625&quot; data-end=&quot;664&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;&lt;br /&gt; ️ 이후 개선 방향&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-end=&quot;664&quot; data-start=&quot;625&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;현재는 각 API 라우트에서 쿠키를 직접 읽고, 요청마다 Authorization 헤더를 붙여주는 방식으로 구현되어 있습니다. 기능적으로는 문제없지만, &lt;b&gt;토큰이 필요한 API가 많아질수록 중복 코드가 늘어난다는 단점&lt;/b&gt;이 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-end=&quot;1019&quot; data-start=&quot;956&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;그래서 다음 단계에서는 &lt;b&gt;범용 프록시 구조&lt;/b&gt;로 리팩터링 할 예정입니다.&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;418&quot; data-start=&quot;375&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;프론트엔드에서는 단순히 /app/api/... 로 요청&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;480&quot; data-start=&quot;419&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;프록시 레이어가 자동으로 쿠키에서 토큰을 읽어 Authorization 헤더를 붙이고 백엔드에 전달&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;539&quot; data-start=&quot;481&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;API 엔드포인트가 수십, 수백 개로 늘어나더라도 &lt;b&gt;프록시 코드 하나만 유지하면 전체가 동작&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-start=&quot;956&quot; data-end=&quot;1019&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;  &amp;nbsp;&lt;/b&gt;&lt;b&gt;중복 코드 제거 + 유지보수성 향상 + 보안 일관성 강화&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;처음에는 &amp;ldquo;토큰이 보이면 안 되겠다&amp;rdquo;는 단순한 문제의식에서 출발했지만, HttpOnly 쿠키와 프록시 라우팅을 적용하면서 보안뿐만 아니라 코드 구조가 훨씬 간결해지고, 결과적으로 개발 흐름도 매끄러워졌습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;467&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;결국 이번 작업으로 단순한 버그 수정이 아니라, 서비스를 더 안전하게 만들고 개발 환경까지 개선할 수 있었습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;467&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;앞으로도 이런 작은 개선들을 꾸준히 쌓아가면서 더 안정적이고 즐겁게 개발할 수 있는 환경을 만들어가고 싶습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;467&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;끝으로 이 글이 비슷한 고민을 하고 계신 분들께 작은 도움이 되길 바랍니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;467&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Programming/Next.js &amp;amp; project</category>
      <category>BFF아키텍처</category>
      <category>cookie 보안</category>
      <category>HttpOnly</category>
      <category>HttyOnly Cookie</category>
      <category>next.js</category>
      <category>프록시 아키텍처</category>
      <author>코등어</author>
      <guid isPermaLink="true">https://archive0313.tistory.com/65</guid>
      <comments>https://archive0313.tistory.com/65#entry65comment</comments>
      <pubDate>Fri, 22 Aug 2025 18:41:14 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js] 게시글 업로드 페이지: 동적으로 element 높이 조정하기(custom hook, throttle)</title>
      <link>https://archive0313.tistory.com/61</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGXnbA/btsKSSjiJIa/yfEWurFkgokTvksmhmPfK1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGXnbA/btsKSSjiJIa/yfEWurFkgokTvksmhmPfK1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGXnbA/btsKSSjiJIa/yfEWurFkgokTvksmhmPfK1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bGXnbA/btsKSSjiJIa/yfEWurFkgokTvksmhmPfK1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;702&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;702&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;게시글 업로드 페이지: 동적으로 element&amp;nbsp;높이&amp;nbsp;조정하기(custom&amp;nbsp;hook,&amp;nbsp;throttle)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;⚠️ 배경&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;게시글 업로드 페이지를 구현하면서, 텍스트를 입력하기 위한 textarea의 높이가 고정되어 있지 않고, 입력된 텍스트의 양에 따라 동적으로 높이가 확장되는 UI가 필요했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. 텍스트 길이에 따른 높이 변화&amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자가 작성하는 텍스트의 양은 고정되어 있지 않으며, 작성 도중에 계속 증가하거나 감소할 수 있습니다. 따라서 textarea의 높이가 텍스트 양에 맞춰 유연하게 조정되어야 사용자 경험(UX)이 개선됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. CSS만으로는 해결되지 않는 스크롤 문제&amp;nbsp;&lt;/b&gt; &amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;min-height와 max-height를 CSS로 설정하면 높이를 일정 범위 내에서 고정할 수 있지만, 텍스트가 증가해도 textarea의 크기가 자동으로 확장되지 않고 텍스트가 잘리는 문제가 발생했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;-&amp;gt; 이 문제를 해결하기 위해, &lt;b&gt;높이를 텍스트의 양에 따라 동적으로 조정&lt;/b&gt;하는 로직을 구현하게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;이 글에서는 &lt;/span&gt;onInput&lt;span style=&quot;text-align: start;&quot;&gt; 이벤트를 활용한 기본적인 구현부터, 커스텀 훅으로 로직을 추상화하고 성능 이슈를 throttle 통해 해결하는 방법까지 상세히 다뤄보겠습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;  구현 방향&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;textarea의 높이를 텍스트 양에 따라 동적으로 조정하는 로직을 구현하기 위해 다음과 같은 방법을 사용했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;텍스트 입력 이벤트 활용 (onInput) : &lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자가 텍스트를 입력하거나 삭제할 때마다 textarea의 높이를 조정하도록 설정.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;scrollHeight 속성 활용 : &lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;textarea.scrollHeight는 텍스트 콘텐츠의 전체 높이를 계산할 수 있는 속성으로 이를&amp;nbsp;활용해 콘텐츠 높이에 맞게 textarea의 높이를 조정.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;style.height 초기화 후 재설정 : &lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존 높이를 초기화(style.height = 'auto')하여 텍스트가 줄어드는 경우에도 적절히 높이를 다시 계산할 수 있도록 처리.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본 구현: onInput를 활용한 동적 높이 조정&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;textarea의 높이를 입력 내용에 맞게 자동으로 조정하려면 on &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;onInput&lt;/span&gt; 이벤트를 사용합니다. &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;onInput은&lt;/span&gt;&amp;nbsp;사용자가 텍스트를 입력할 때마다 이벤트가 트리거되어 scrollHeight를 기반으로 높이를 조정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// UploadForm.tsx
import { useRef } from 'react';

interface IUploadFormProps {
  className?: string;
}

export default function UploadForm({ className }: IUploadFormProps) {
  const textareaRef = useRef&amp;lt;HTMLTextAreaElement&amp;gt;(null);

  const handleTextChange = () =&amp;gt; {
    const textarea = textareaRef.current;
    if (textarea) {
      textarea.style.height = 'auto'; // 높이 초기화
      textarea.style.height = `${textarea.scrollHeight}px`; // 입력 내용에 따라 높이 조정
    }
  };

  return (
    &amp;lt;textarea
      ref={textareaRef}
      onInput={handleTextChange}
      rows={1}
      placeholder=&quot;내용을 입력하세요&quot;
      className=&quot;resize-none w-full&quot;
    /&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;textarea의 높이를 scrollHeight 값으로 설정하여 입력 내용에 따라 높이가 조정됩니다.(textarea.scrollHeight: 콘텐츠 전체 높이)&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;textarea.style.height = 'auto'는 높이를 초기화하여 &lt;span style=&quot;text-align: start;&quot;&gt;기존 높이를 제거하고&amp;nbsp;&lt;/span&gt;높이 조정의 부드러움을 유지합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;최소 높이는 rows={1}로 설정합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; ️ 리팩토링: 커스텀 훅으로 로직 추상화&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;높이 자동 조정을 위한 로직을 별도의 훅(Hook)으로 추상화하 여 다른 컴포넌트의 다양한 DOM 요소에서 재사용할 수 있도록 분리하였습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;useAutoResizeHeight&amp;nbsp;훅은 해당 요소에 대한 참조와 함께 입력 이벤트를 처리하여 높이를 자동으로 조정하는 로직을 캡슐화합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;useAutoResizeHeight 커스텀 훅 제작&lt;/span&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1732270179817&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// useAutoResizeHeight.ts
import { useRef, useCallback } from 'react';

export function useAutoResizeHeight&amp;lt;T extends HTMLElement&amp;gt;() {
  const elementRef = useRef&amp;lt;T&amp;gt;(null);

  const adjustHeight = useCallback(() =&amp;gt; {
    const element = elementRef.current;
    if (element) {
      element.style.height = 'auto'; // 높이 초기화
      element.style.height = `${element.scrollHeight}px`; // 콘텐츠에 맞게 높이 조정
    }
  }, []);

  return { elementRef, resizeToFitContent };
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;제네릭 사용 (T extends HTMLElement): textarea뿐만 아니라 다른 DOM 요소에서도 사용 가능합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;useCallback으로 최적화: 동일한 함수를 여러 번 생성하지 않도록 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;useAutoResizeHeight 훅을 UploadForm에서 활용&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;UploadForm에서 useAutoResizeTextarea 훅을 호출하여 elementRef와 resizeToFitContent을 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732270241545&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UploadForm.tsx
function UploadForm() {
  const { elementRef, resizeToFitContent } = useAutoResizeHeight&amp;lt;HTMLTextAreaElement&amp;gt;();

  return (
    &amp;lt;textarea
      className=&quot;scrollbar-hidden w-full resize-none text-14px focus:outline-none&quot;
      placeholder=&quot;게시글 입력하기...&quot;
      ref={elementRef}
      onInput={resizeToFitContent}
      rows={1}
    /&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개선 후 효과&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;UploadForm에서 UI 로직만 처리하고,&amp;nbsp;&lt;b&gt;비즈니스 로직은 훅으로 분리하여 가독성이 향상&lt;/b&gt;되었습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다른 컴포넌트에서도 useAutoResizeHeight를 호출해 동일한 동작을 구현할 수 있게 되어&amp;nbsp;&lt;b&gt;재사용성이 향상&lt;/b&gt;되었습니다. 특히 DOM 요소의 타입을 제네릭으로 받아 타입 오류를 방지하고, 다양한 UI 컴포넌트에서 활용 가능하도록&amp;nbsp;&lt;b&gt;추상화&lt;/b&gt;할 수 있었습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;useAutoResizeHeight가 독립적으로 동작하므로, 변경이 필요할 때 해당 훅만 수정하면 되기 때문에&amp;nbsp;&lt;b&gt;유지보수성이 향상&lt;/b&gt;되었습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;but, 이 방식은 간단하면서도 유용하지만 입력이 많아질 경우 성능 문제가 발생할 수 있어 성능 최적화할 수 있는 방법이 필요했습니다.&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;✨ 성능 최적화:   throttle 활용&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;onInput&lt;/span&gt;&amp;nbsp;이벤트는 사용자가 입력할 때마다 트리거되기 때문에 입력 속도가 빠르거나 많아지게 되면 성능 문제가 발생할 수 있습니다. 이를 해결하기 위해 throttle을 적용해 호출 빈도를 제한하도록 수정하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;throttle이란?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;throttle은 주기적으로 발생하는 &lt;b&gt;이벤트를 일정 간격으로 제한하여 처리&lt;/b&gt;하는 성능 최적화 기법입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, 이벤트가 아무리 자주 발생하더라도 설정된 시간 간격 내에서는 &lt;b&gt;한 번만 실행&lt;/b&gt;되도록 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;throttle의 동작 원리&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;지속적으로 이벤트가 발생 : 사용자가 빠르게 타이핑하거나 스크롤하는 경우처럼, 이벤트가 매우 짧은 간격으로 반복 발생.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일정한 간격으로 이벤트 실행 : 설정된 시간 간격에 따라 이벤트를 실행하고, 그 사이의 이벤트는 무시.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;-&amp;gt; &lt;b&gt;일정한 주기로 이벤트가 실행되므로, 과도한 연산을 방지하고 성능을 최적화&lt;/b&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;throttle을 적용해야 하는 경우&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;throttle은 &lt;/span&gt;&lt;b&gt;이벤트가 지속적으로 발생하는 상황에서 일정 간격으로만 실행이 필요&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;할 때 적합합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;스크롤 이벤트 : &lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자가 페이지를 스크롤할 때 이벤트가 수백 번 호출될 수 있기 때문에&amp;nbsp;스크롤 위치를 업데이트하거나 특정 위치에 도달했는지 확인하는 로직에 적용.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;윈도우 리사이즈 이벤트 : &lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;브라우저 창 크기가 변경될 때 레이아웃을 업데이트하는 로직에 지나치게 빈번한 호출을 제한하여 성능을 최적화.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;드래그 앤 드롭 : &lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자가 드래그할 때 마우스 위치를 업데이트하는 로직에 적용해 UI 업데이트 빈도를 제한.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;실시간 입력 처리 : &lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;입력 중에 애니메이션이나 화면 업데이트가 필요한 경우(textarea의 높이를 조정하거나, 입력 내용을 실시간으로 보여주는 미리보기)에 적용.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;throttle vs debounce&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;b&gt;throttle&lt;/b&gt;&lt;/b&gt;은&lt;b&gt;이벤트 발생 간격을 제한&lt;/b&gt;, &lt;b&gt;debounce&lt;/b&gt;는 &lt;b&gt;이벤트 실행을 지연&lt;/b&gt;합니다. 두 기법의 차이를 이해하면 적절한 상황에서 활용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 99.8837%; height: 175px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 33px;&quot;&gt;
&lt;td style=&quot;width: 12.7519%; text-align: center; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; text-align: center; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;throttle&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.3799%; text-align: center; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;debounce&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 12.7519%; text-align: center; height: 34px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;동작 방식&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; text-align: center; height: 34px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;지정된 간격마다 이벤트 실행&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.3799%; text-align: center; height: 34px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이벤트 발생 후 지정된 시간 동안 대기 후 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 36px;&quot;&gt;
&lt;td style=&quot;width: 12.7519%; text-align: center; height: 36px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;사용 사례&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; text-align: center; height: 36px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스크롤, 리사이즈, 드래그, 실시간 UI 업데이트&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.3799%; text-align: center; height: 36px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;검색 자동완성, API 요청, 사용자가 입력을 멈췄을 때 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 35px;&quot;&gt;
&lt;td style=&quot;width: 12.7519%; text-align: center; height: 35px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;이벤트 실행&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; text-align: center; height: 35px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;지속적으로 발생하는 이벤트 중 일정 간격으로 실행&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.3799%; text-align: center; height: 35px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;마지막 이벤트 후 한 번만 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 37px;&quot;&gt;
&lt;td style=&quot;width: 12.7519%; text-align: center; height: 37px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;적합한 경우&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 42.8681%; text-align: center; height: 37px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실시간 업데이트가 필요한 경우&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.3799%; text-align: center; height: 37px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이벤트 처리가 끝난 후 작업이 필요할 경우&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Lodash throttle 적용&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;throttle을 적용하여 이벤트가 발생하더라도, 마지막 실행 후 최소 wait 시간이 지나야 다시 실행됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;throttle은 내부 상태를 가지므로, React가 컴포넌트 언마운트 시 이를 클린업해야 메모리 누수를 방지할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 1.&lt;b&gt; Lodash &lt;/b&gt;&lt;b&gt;throttle&lt;/b&gt;&lt;b&gt;의 내부 동작&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Lodash의 throttle은 이벤트 실행을 제한하기 위해 내부 상태를 유지합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;bull; 마지막으로 실행된 시간(lastRan)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;bull; 타이머(timeout): 아직 실행되지 않은 대기 중인 함수&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이러한 상태는 throttle로 생성된 함수가 계속 유지되며 관리합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 2. &lt;b&gt;React 컴포넌트의 생명 주기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &amp;bull; React 컴포넌트가 언마운트될 때, throttle 함수 내부의 타이머(timeout)가 여전히 동작하고 있다면 메모리 누수가 발생할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &amp;bull; 예를 들어 textarea의 입력 이벤트가 발생 중인데 컴포넌트가 언마운트되면, 대기 중이던 throttle 함수가 여전히 실행될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. &lt;b&gt;Cleanup의 필요성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &amp;bull; 컴포넌트 언마운트 시 throttle 함수의 타이머와 상태를 정리해 주어야 메모리 누수와 불필요한 연산을 방지할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &amp;bull; Lodash의 throttle은 cancel() 메서드를 제공하여 타이머와 내부 상태를 정리할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732270313370&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useRef, useCallback } from 'react';
import { throttle } from 'lodash';

export function useAutoResizeHeight&amp;lt;T extends HTMLElement&amp;gt;(throttleTime = 100) {
  const elementRef = useRef&amp;lt;T|null&amp;gt;(null);

  const resizeToFitContent = useCallback(() =&amp;gt; {
    const element = elementRef.current;
    if (element) {
      element.style.height = 'auto'; 
      element.style.height = `${element.scrollHeight}px`; 
    }
  }, []);

  // Throttled function 생성
  const throttledResize = useMemo(() =&amp;gt; throttle(resizeToFitContent, throttleTime), [
    resizeToFitContent,
    throttleTime,
  ]);

  // Cleanup: Throttle 함수 취소
  useEffect(() =&amp;gt; {
    return () =&amp;gt; {
      throttledResize.cancel(); // Throttled 함수 정리
    };
  }, [throttledResize]);

  return { elementRef, throttledResize };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; _&lt;b&gt;throttle 캐싱&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &amp;bull; useMemo로 throttle로 생성된 함수를 캐싱해 매번 새로 생성되지 않도록 설정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &amp;bull; 메모이제이션을 통해 동일한 의존성 배열(deps)을 가지는 동안 기존의 throttle 함수가 재사용됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &amp;bull; 컴포넌트가 언마운트되기 전에는 throttle 함수의 내부 상태(예: 타이머)가 유지됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;_&lt;b&gt;Cleanup 처리&lt;/b&gt;:&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &amp;bull; useEffect Cleanup을 활용해&amp;nbsp;throttledResize.cancel()을 호출하여 언마운트 시 throttle의 타이머(timeout)와 내부 상태를 정리합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &amp;bull; 메모이제이션은 유지하면서도 컴포넌트 생명 주기에 따라 throttle을 완전히 종료하여 메모리 누수를 방지합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;throttle 적용 후 효과&lt;/span&gt;&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;과도한 이벤트 호출이 제한되어&lt;b&gt; 불필요한 DOM 조작을 방지&lt;/b&gt;하여 성능을 보다 효율적으로 개선하였습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자 입력 속도가 빨라도 부드럽게 동작하여 &lt;b&gt;사용자 경험이 향상&lt;/b&gt;되었습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;빠르게 타이핑할 때도 &lt;b&gt;지정된 간격으로만 이벤트를 실행하여 CPU 부하를 줄여&lt;/b&gt; 결과적으로 효율적으로 리소스를 사용하게 되었습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;throttle을 활용하여 복잡한 로직 없이 간단히 최적화 구현할 수 있었습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;매번 코드를 작성할 때마다 느끼는 거지만 단순히 동작하는 코드를 작성하는 것에서 그치지 않고, 성능 최적화와 유지보수성까지 고려한 코드를 작성하는 것에 대한 중요성을 실감하는 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;textarea의 동적 높이 조정은 작은 UX 개선이지만, 커스텀 훅과 최적화를 통해 확장성과 유지보수성을 동시에 갖춘 솔루션을 구현할 수 있었습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Programming/Next.js &amp;amp; project</category>
      <category>lodash</category>
      <category>next.js</category>
      <category>scrollHeight</category>
      <category>Throttle</category>
      <author>코등어</author>
      <guid isPermaLink="true">https://archive0313.tistory.com/61</guid>
      <comments>https://archive0313.tistory.com/61#entry61comment</comments>
      <pubDate>Fri, 22 Nov 2024 18:03:22 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js] Next.js의 Route Groups와 미들웨어를 이용한 인증 및 경로 충돌 문제 해결하기</title>
      <link>https://archive0313.tistory.com/60</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Group 30.png&quot; data-origin-width=&quot;1693&quot; data-origin-height=&quot;1011&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcQzL7/btsKIVlhgIh/u7Z1nS5XLtybhLpVcXVo41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcQzL7/btsKIVlhgIh/u7Z1nS5XLtybhLpVcXVo41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcQzL7/btsKIVlhgIh/u7Z1nS5XLtybhLpVcXVo41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcQzL7%2FbtsKIVlhgIh%2Fu7Z1nS5XLtybhLpVcXVo41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1693&quot; height=&quot;1011&quot; data-filename=&quot;Group 30.png&quot; data-origin-width=&quot;1693&quot; data-origin-height=&quot;1011&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; font-size: 1.44em; letter-spacing: -1px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Next.js Route Groups와 미들웨어를 이용한 인증 및 경로 충돌 문제 해결&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-4o&quot; data-message-id=&quot;0875c4c1-4986-42da-a760-a9c0e3de42fb&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Next.js의 app 디렉토리 구조와 Route Groups(경로 그룹) 기능을 활용하다 보면, 경로를 직관적이고 조직적으로 구성할 수 있지만 동일한 URL 경로로 해석되는 페이지를 두 개 이상 구성하게 되면 경로 충돌 문제가 발생할 수 있습니다. 이번 글에서는 로그인 상태에 따라 다른 페이지를 렌더링하는 과정에서 발생한 문제를 트러블슈팅한 과정과 미들웨어를 활용한 접근 제어 방법을 공유합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  &lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;문제 상황&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;프로젝트의 메인 페이지(/)에서 로그인 여부에 따라 다른 콘텐츠를 보여주고자 했습니다. 로그인 상태에 따라 별도의 경로 그룹을 활용해 다음과 같이 디렉토리를 구성했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1731496481259&quot; class=&quot;bash custom-cursor-default-hover&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;src/ 
├──app/ 
│  ├── (private)/
│  │   └── page.tsx             // 로그인 시 렌더링될 페이지
│  ├── (public)/
│      └── page.tsx             // 비로그인 시 렌더링될 페이지
├── middleware.ts // 로그인 여부를 확인하는 미들웨어&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;(public)/page.tsx&lt;/b&gt;: 비로그인 상태의 사용자에게 기본으로 보여줄 페이지(로그인 유도 콘텐츠)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;(private)/page.tsx&lt;/b&gt;: 로그인된 상태의 사용자에게 기본으로 보여줄 페이지(피드 콘텐츠)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;-&amp;gt; 이렇게 &lt;u&gt;두 개의 경로 그룹을 사용해 &lt;/u&gt;&lt;/span&gt;&lt;u&gt;로그인 전과 후에 서로 다른 페이지를 보여주고자&lt;/u&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;했습니다. &lt;/span&gt;비로그인 사용자가 / 경로로 접속하면 (public)/page.tsx가 렌더링되고, 로그인된 사용자가 접근할 때는 (private)/page.tsx가 렌더링되도록 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;⚠️ &lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;문제 발생: 경로 충돌 오류&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;위와 같은 경로 그룹 설정은 &lt;b&gt;동일한 URL을 가리키는 두 개의 병렬 경로 그룹이 생성되기 때문에&lt;/b&gt;, Next.js에서 아래와 같은 경로 충돌 오류가 발생했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1731496992391&quot; class=&quot;bash custom-cursor-default-hover&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Error: You cannot have two parallel pages that resolve to the same path. 
Please check /(private)/page and /(public)/page.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt; &amp;zwj;♂️ 원인 분석&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;공식 문서를 살펴보니 Next.js에서는 &lt;b&gt;경로 그룹 이름이 URL에 영향을 주지 않고, 단순히 디렉토리 구조를 위한 역할만 합니다&lt;/b&gt;. 즉, (public)/page.tsx와 (private)/page.tsx는 각각 다른 경로 그룹이지만 URL 상에서는 둘 다 동일한 / 경로로 해석됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;[참고: &lt;a style=&quot;color: #333333;&quot; href=&quot;https://nextjs.org/docs/app/building-your-application/routing/route-groups&quot;&gt;https://nextjs.org/docs/app/building-your-application/routing/route-groups]&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Next.js 경로 그룹 관련 사항&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;경로 그룹의 이름은 URL에 영향을 미치지 않음&lt;/b&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;(public)와 (private)는 URL 구조를 변경하지 않고, Next.js 내부에서 경로를 구분(폴더를 조직화하기 위한 역할)하기 위한 역할만 합니다. 따라서, (public)/page.tsx와 (private)/page.tsx는 &lt;b&gt;모두 동일한 / 경로&lt;/b&gt;로 인식됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;중복된 경로 오류 발생(&lt;b&gt;병렬 경로는 동일한 URL을 가질 수 없음&lt;/b&gt;)&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;동일한 경로에 대해 두 개의 페이지가 존재할 경우 Next.js는 &lt;b&gt;경로 충돌을 방지하기 위해 오류를 발생&lt;/b&gt;시킵니다. 경로 그룹을 이용해 디렉토리를 구성해도, 동일한 URL(/)에 두 개의 페이지가 존재할 수 없으므로 오류가 발생하게 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;✅ 해결 방법: 경로 그룹을 구분하고 미들웨어로 접근 제어하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이 문제를 해결하기 위해 미들웨어를 활용하여 특정 경로에 대한 접근을 제어하는 방식을 채택했습니다. 미들웨어를 사용함으로써 로그인 상태에 따라 /feed와 같은 보호 경로 접근을 제어할 수 있었으며, 클라이언트 측 조건부 렌더링이 아닌 서버 측에서 처리함으로써 보안성과 성능 최적화 효과를 동시에 얻을 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;왜 미들웨어를 사용했을까?&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;1. 보안성: 클라이언트 측 조건부 렌더링의 한계&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;만약 layout.tsx 나 page.tsx에서 토큰 값을 가져와 조건부 렌더링을 통해 페이지 접근을 제어하려 한다면, 클라이언트 측에서 조건부로 페이지를 숨기는 방식이 됩니다. 이 방식은 다음과 같은 단점을 가집니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;클라이언트에서 콘텐츠가 잠시라도 로드될 수 있음&lt;/b&gt;: 클라이언트 측 조건부 렌더링은 페이지가 이미 로드된 이후에 조건에 따라 내용을 보여주거나 숨깁니다. 이 과정에서 비로그인 사용자가 보호된 콘텐츠의 일부를 잠시라도 볼 수 있는 가능성이 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;보호가 필요한 콘텐츠를 완전히 차단하기 어려움&lt;/b&gt;: 클라이언트 측에서 콘텐츠를 조건부로 렌더링할 경우, 실제로는 사용자가 보호된 페이지를 요청하고 로드한 후에 해당 콘텐츠가 숨겨지게 됩니다. 이는 보안 측면에서 완전한 접근 제어가 어렵게 만듭니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;반면, &lt;b&gt;미들웨어는 서버 측에서 클라이언트 요청이 처리되기 전에 접근을 제어&lt;/b&gt;하므로, 보호된 페이지에 대한 요청 자체를 차단합니다. &lt;b&gt;토큰이 없는 사용자는 서버 측에서부터 보호된 콘텐츠에 접근할 수 없도록 설정&lt;/b&gt;할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2. 성능 최적화: 불필요한 페이지 로드를 방지&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;클라이언트 측에서 조건부 렌더링을 한다면, &lt;b&gt;비로그인 사용자가 보호된 페이지에 접근할 때도 페이지가 로드&lt;/b&gt;됩니다. 예를 들어 /feed 페이지에 접근할 때, 비로그인 사용자는 콘텐츠 로드 후에야 접근이 제한되었다는 메시지를 보게 될 수 있습니다. 이 방식은 두 가지 성능 문제를 야기합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;불필요한 데이터 요청&lt;/b&gt;: 클라이언트는 서버에서 해당 페이지와 관련된 데이터와 리소스를 받아오게 되어, 불필요한 네트워크 요청과 데이터 전송이 발생합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;초기 로딩 지연&lt;/b&gt;: 페이지를 먼저 로드한 후에 조건부로 콘텐츠를 숨기기 때문에, 실제로 비로그인 사용자가 접근할 수 없는 페이지임에도 불구하고 초기 로딩이 발생하여 UX가 저하됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;미들웨어는&lt;/b&gt; &lt;b&gt;요청 단계에서 바로 리디렉션을 처리&lt;/b&gt;하기 때문에, 비로그인 사용자가 접근할 수 없는 페이지에 대해 불필요한 로딩이 발생하지 않습니다. 그 결과, 비로그인 사용자는 /login과 같은 로그인 페이지로 즉시 리디렉션되어 &lt;b&gt;페이지 로딩과 네트워크 요청을 최적화&lt;/b&gt;할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;3. 일관된 사용자 경험: 빠른 리디렉션 처리&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;미들웨어는 요청이 서버에서 처리되는 단계에서 &lt;b&gt;토큰 유무에 따라 즉시 리디렉션&lt;/b&gt;하므로, 비로그인 사용자가 보호된 페이지에 접근하려 할 때 빠르게 로그인 페이지로 리디렉션됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;페이지 전환의 일관성&lt;/b&gt;: 미들웨어에서 요청을 제어하면, 비로그인 사용자는 보호된 페이지에 접근하려 할 때 직접 /login 페이지로 이동합니다. 이는 사용자가 중간에 다른 페이지나 경고 메시지를 볼 필요 없이 &lt;b&gt;일관된 UX 흐름&lt;/b&gt;을 유지하게 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;즉각적인 피드백 제공&lt;/b&gt;: 클라이언트 측 조건부 렌더링에서는 페이지가 로드된 이후에야 접근 불가 메시지를 볼 수 있는 반면, 미들웨어를 통해 즉시 리디렉션하면 &lt;b&gt;사용자는 빠르게 로그인 필요 상태를 인지&lt;/b&gt;할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;미들웨어를 사용해 서버 측에서 토큰 검사를 진행함으로써, 보안성, 성능 최적화, 그리고 일관된 사용자 경험을 모두 보장할 수 있습니다. 클라이언트 측 조건부 렌더링은 페이지를 숨기는 데 적합하지 않으며, 보호된 콘텐츠에 대한 완전한 접근 제어가 어렵습니다. 반면 미들웨어는 사용자가 콘텐츠에 접근하기 전 요청을 차단하므로, 더 강력하고 효율적인 방식으로 보호된 페이지 접근을 관리할 수 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  최종 구현: 미들웨어로 접근 제어 처리&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;미들웨어에서 특정 경로에 대한 접근 제어를 추가하여 &lt;b&gt;보호 경로에 비로그인 사용자가 접근할 경우 /로 리디렉션&lt;/b&gt;하는 구조로 변경했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;결과적으로, 아래와 같이 디렉토리 구조를 수정했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1731497557704&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;src/ 
├──app/ 
│  ├── (private)/
│  │   └── page.tsx // 로그인되지 않은 사용자에게 보여줄 페이지
│  ├── (public)/
│      ├── feed/
│          └── page.tsx // 로그인된 사용자만 접근 가능한 피드 페이지
├── middleware.ts // 로그인 상태에 따라 접근을 제어하는 미들웨어&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;(public)/page.tsx&lt;/b&gt;: 여전히 로그인하지 않은 사용자를 위한 기본 페이지로 사용합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;(private)/feed/page.tsx&lt;/b&gt;: 로그인한 사용자를 위한 피드 페이지로, /feed 경로로 접근할 수 있습니다. 이 페이지는 로그인된 상태에서만 접근하도록 설정합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;middleware.ts&lt;/b&gt;: /feed 경로에 접근할 때 로그인 상태를 확인하여, 로그인되지 않은 사용자는 / 페이지로 리디렉션하도록 설정합니다.&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;미들웨어를 설정하여 로그인 여부에 따라 /feed, /profile, /post, /search 등 특정 경로에 대한 접근을 제어합니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1731497626664&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('userToken'); // 로그인 상태 확인
  const { pathname } = request.nextUrl;

  // 보호가 필요한 경로들을 정의
  const protectedPaths = ['/feed', '/profile', '/post', '/search'];

  // 로그인되지 않은 사용자가 보호 경로에 접근하면 /로 리디렉션
  if (!token &amp;amp;&amp;amp; protectedPaths.some((path) =&amp;gt; pathname.startsWith(path))) {
    return NextResponse.redirect(new URL('/', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/:path*'], // 모든 경로에 대해 미들웨어 적용 후 조건부로 제어
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;로그인 상태 확인&lt;/b&gt;: userToken 쿠키를 통해 사용자가 로그인 상태인지 확인합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;보호 경로 조건&lt;/b&gt;: 로그인 상태에 따라 접근을 제어해야 하는 경로(/feed, /profile, /post, /search 등)를 하나의 protectedPaths 배열로 관리하고, 비로그인 사용자의 접근을 막습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;리디렉션 처리&lt;/b&gt;: 보호된 경로에 비로그인 사용자가 접근하면 /로 리디렉션하고 로그인된 사용자만 피드 페이지에 접근할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;⭐️ 결론&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Next.js에서 경로 그룹(Route Groups)은 디렉토리 구조를 깔끔하게 정리하는 데 유용하지만, &lt;/span&gt;URL에 영향을 미치지 않으므로 동일한 경로에서 조건부 렌더링이 필요할 때 충돌 문제가 발생할 수 있습니다&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;. 이를 해결하기 위해 미들웨어를 활용하여 &lt;/span&gt;로그인 여부에 따라 특정 경로 접근을 제어&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하는 방식을 도입했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;경로 그룹의 특성 이해&lt;/b&gt;: 경로 그룹은 디렉토리 정리를 위한 것이지, URL 자체에 영향을 주지 않는다는 점을 유의해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;미들웨어를 활용한 접근 제어&lt;/b&gt;: 보호된 경로를 정의하고, 미들웨어를 통해 조건부 접근 제어를 구현함으로써 &lt;b&gt;더 나은 사용자 경험을 제공&lt;/b&gt;할 수 있었습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;로그인 여부에 따라 다른 콘텐츠 제공&lt;/b&gt;: 미들웨어로 인증 상태를 확인하여, 사용자의 로그인 상태에 따라 동적으로 다른 페이지 콘텐츠를 제공할 수 있었습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Programming/Next.js &amp;amp; project</category>
      <category>Authentication</category>
      <category>Middleware</category>
      <category>next.js</category>
      <author>코등어</author>
      <guid isPermaLink="true">https://archive0313.tistory.com/60</guid>
      <comments>https://archive0313.tistory.com/60#entry60comment</comments>
      <pubDate>Wed, 13 Nov 2024 22:00:49 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js] 효율적인 날짜 변환과 시간 표시: 직접 구현 vs. Day.js 활용</title>
      <link>https://archive0313.tistory.com/58</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Group 26.png&quot; data-origin-width=&quot;1693&quot; data-origin-height=&quot;1011&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BwtsT/btsKv7twTw1/wi3300WvCqPiObHZYhLGpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BwtsT/btsKv7twTw1/wi3300WvCqPiObHZYhLGpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BwtsT/btsKv7twTw1/wi3300WvCqPiObHZYhLGpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBwtsT%2FbtsKv7twTw1%2Fwi3300WvCqPiObHZYhLGpK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1693&quot; height=&quot;1011&quot; data-filename=&quot;Group 26.png&quot; data-origin-width=&quot;1693&quot; data-origin-height=&quot;1011&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;날짜 변환과 시간 표시 구현: 커스텀 함수와 Day.js 활용 비교&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;프로젝트를 진행하면서 댓글의 생성 시간을 &quot;몇 분 전&quot;, &quot;몇 시간 전&quot; 등 상대적인 시간으로 표시해야 하는 요구사항이 있었습니다. 처음에는 이 기능을 직접 구현하기 위해 TypeScript로 날짜 변환 함수를 작성했고, 이후에는 더 나은 유지보수성과 코드 간결성을 위해 Day.js 라이브러리를 설치해 적용했습니다. 이번 글에서는 날짜 변환 함수를 직접 작성한 방법과 Day.js를 사용한 이유를 함께 다루겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;1. 날짜 변환 함수 직접 작성하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;먼저, 댓글 생성 시간을 받아 현재 시간과의 차이를 &quot;몇 분 전&quot;, &quot;몇 시간 전&quot;과 같은 형식으로 반환하는 getTimeAgo 함수를 작성했습니다. 이 함수는 밀리초 단위 차이를 계산하여 분, 시간, 일, 주, 월, 연 단위로 변환하여 적절한 문자열을 반환합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;function getTimeAgo(createdAt: string): string {
  const createdDate = new Date(createdAt);
  const now = new Date();

  const diffInMs = now.getTime() - createdDate.getTime(); // 밀리초 단위 차이 계산
  const diffInMinutes = Math.floor(diffInMs / 1000 / 60); // 분 단위로 변환
  const diffInHours = Math.floor(diffInMinutes / 60); // 시간 단위로 변환
  const diffInDays = Math.floor(diffInHours / 24); // 일 단위로 변환
  const diffInWeeks = Math.floor(diffInDays / 7); // 주 단위로 변환
  const diffInMonths = Math.floor(diffInDays / 30); // 월 단위로 변환
  const diffInYears = Math.floor(diffInDays / 365); // 연 단위로 변환

  if (diffInMinutes &amp;lt; 1) return '방금 전';
  if (diffInMinutes &amp;lt; 60) return `${diffInMinutes}분 전`;
  if (diffInHours &amp;lt; 24) return `${diffInHours}시간 전`;
  if (diffInDays &amp;lt; 7) return `${diffInDays}일 전`;
  if (diffInWeeks &amp;lt; 4) return `${diffInWeeks}주 전`;
  if (diffInMonths &amp;lt; 12) return `${diffInMonths}개월 전`;
  return `${diffInYears}년 전`;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사용 예시&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const createdAt = &quot;2023-12-20T06:10:26.803Z&quot;;
console.log(getTimeAgo(createdAt)); // 예: &quot;1년 전&quot;, &quot;2일 전&quot;, &quot;5시간 전&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이 함수는 기본적인 기능을 잘 수행했지만, 다음과 같은 문제점이 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;1. 코드의 복잡성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;상대 시간 계산을 직접 구현하는 것은 많은 조건과 계산을 포함하기 때문에 코드가 비교적 길어지고 복잡해집니다. getTimeAgo 함수는 시간 단위별로 다양한 조건문이 필요하기 때문에 이러한 로직이 늘어나면 코드의 가독성이 떨어지고, 코드 리뷰 및 유지보수 시 실수를 유발할 가능성이 커집니다. 시간 단위별 조건이 추가될 때마다 새로운 로직을 삽입하거나 변경해야 하므로 코드가 비대해질 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;2. 일관성 문제&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;프로젝트 내에서 날짜와 시간을 다루는 로직이 여러 군데에 존재할 경우, 모든 로직이 동일한 계산 방식과 포맷을 유지하기 어려워집니다. 직접 구현한 함수를 사용할 때는 각각의 사용처에서 동일한 함수를 활용하지 않거나 일부 수정된 버전을 사용하는 경우가 생길 수 있습니다. 이는 프로젝트의 일관성을 해칠 수 있으며, 포맷이나 계산 방식이 다르게 표시될 위험을 초래합니다. 또한, 코드 유지보수 중 한 곳에서 로직을 변경했을 때 다른 부분에서 이를 놓칠 가능성도 큽니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이러한 문제점은 프로젝트의 복잡성을 높이고 개발 과정에서 오류를 유발할 수 있으므로, 더 간결하고 일관된 접근법이 필요했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;2. Day.js로 리팩토링 -&amp;gt; 코드 간결화 및 효율성 향상&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이러한 이유로 프로젝트에 Day.js를 도입했습니다. Day.js는 경량의 날짜 처리 라이브러리로(&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;Moment.js보다 무려 33배 가량 더 가벼움&lt;/span&gt;), Moment.js의 단점을 보완하면서도 유사한 API를 제공하는 것이 특징입니다.&amp;nbsp;Moment.js는 풍부한 기능을 제공하지만, 그에 따라 상당히 무겁고 퍼포먼스에 영향을 미칠 수 있습니다. 반면, Day.js는 용량이 작아 로딩 속도와 성능 최적화 측면에서 큰 이점이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Day.js의 가장 큰 장점 중 하나는 상대 시간 계산을 쉽게 구현할 수 있는 relativeTime 플러그인입니다. 이 플러그인을 사용하면 복잡한 조건문 없이 단순히 dayjs(createdAt).fromNow() 한 줄만으로 &quot;몇 분 전&quot;, &quot;몇 시간 전&quot;, &quot;몇 일 전&quot;과 같은 상대적인 시간을 표현할 수 있습니다. 코드의 길이를 크게 줄이고 가독성을 높이면서, 다양한 날짜 포맷이나 상대 시간 계산을 일관되게 유지할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;또한, Day.js는 플러그인 기반 구조로 필요한 기능만 선택적으로 사용할 수 있습니다. 이는 프로젝트에 불필요한 기능이 포함되지 않도록 하여 용량을 최적화할 수 있으며, 프로젝트의 성능과 유지보수성을 향상시킵니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;Day.js 설치&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1730709657514&quot; class=&quot;bash custom-cursor-default-hover&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install dayjs
yarn add dayjs&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;relativeTime 플러그인 적용&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1730709684555&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import dayjs from 'dayjs'; 
import relativeTime from 'dayjs/plugin/relativeTime'; 

dayjs.extend(relativeTime); // 플러그인 적용&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;getTimeAgo 함수 리팩토링&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1730709734277&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function getTimeAgo(createdAt: string): string {
  return dayjs(createdAt).fromNow(); 
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;getTimeAgo 함수 적용&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1730709854254&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function CommentCard({ user, comment, className }: ICommentCardProps) { 
  return ( 
    &amp;lt;article className={`flex gap-3 ${className}`}&amp;gt; 
      &amp;lt;UserImage user={user} size=&quot;36px&quot; /&amp;gt; 
      &amp;lt;div className=&quot;mt-[5px] flex w-full flex-col gap-4&quot;&amp;gt; 
        &amp;lt;div className=&quot;flex gap-6px&quot;&amp;gt; 
          &amp;lt;p className=&quot;text-14px font-medium leading-17px&quot;&amp;gt;{user.username}&amp;lt;/p&amp;gt; 
          &amp;lt;span className=&quot;font-normal text-10px leading-17px text-gray-300&quot;&amp;gt;{`&amp;middot; ${getTimeAgo(comment.createdAt)}`}&amp;lt;/span&amp;gt; 
          &amp;lt;IconButton icon={SMore} className=&quot;ml-auto&quot; label=&quot;더보기 버튼&quot; /&amp;gt; 
        &amp;lt;/div&amp;gt; 
        &amp;lt;p className=&quot;text-12px font-normal leading-17px text-gray-400&quot;&amp;gt;{comment.content}&amp;lt;/p&amp;gt; 
      &amp;lt;/div&amp;gt;
    &amp;lt;/article&amp;gt; 
  ); 
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Day.js를 선택한 이유&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;간결한 코드&lt;/b&gt;: dayjs(createdAt).fromNow() 한 줄로 간단하게 날짜 차이를 계산할 수 있어, 기존의 복잡한 로직보다 훨씬 깔끔하고 효율적입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가독성 향상&lt;/b&gt;: 코드의 길이가 짧아짐으로써 가독성이 크게 향상됩니다. 복잡한 조건문과 계산이 필요하지 않아, 코드의 유지보수가 쉬워졌습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관성 유지&lt;/b&gt;: 프로젝트 전반에서 동일한 날짜 포맷과 상대 시간 계산을 적용할 수 있어, 코드의 일관성을 유지할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경량성&lt;/b&gt;: Moment.js보다 훨씬 작은 용량으로, 프로젝트의 성능에 미치는 영향을 최소화할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;결론 및 느낀 점&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;처음에는 직접 작성한 날짜 변환 함수를 사용하여 댓글 생성 시간을 표시했지만, 코드가 길어지고 유지보수에 부담이 된다는 문제를 느꼈습니다. Day.js를 도입한 후에는 코드의 간결성과 일관성을 크게 개선할 수 있었습니다.&amp;nbsp;특히, relativeTime 플러그인을 통해 상대 시간을 매우 쉽게 구현할 수 있었으며, 라이브러리의 경량성 덕분에 성능에도 큰 영향을 미치지 않았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이번 경험을 통해 필요한 기능을 적절한 외부 라이브러리를 사용해 구현함으로써 코드의 복잡성을 줄이고 효율적으로 개발하는 것 역시 중요하다라는 것을 깨달았습니다. 앞으로도 프로젝트의 필요에 따라 직접 구현과 라이브러리 사용을 적절히 병행하며 더 나은 코드를 작성해 나가고자 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>  Programming/Next.js &amp;amp; project</category>
      <category>dayjs</category>
      <category>next.js</category>
      <author>코등어</author>
      <guid isPermaLink="true">https://archive0313.tistory.com/58</guid>
      <comments>https://archive0313.tistory.com/58#entry58comment</comments>
      <pubDate>Mon, 4 Nov 2024 16:53:57 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js &amp;amp; Tailwind CSS] Tailwind CSS - 동적 값 적용 문제 해결기</title>
      <link>https://archive0313.tistory.com/55</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Group 24 (1).png&quot; data-origin-width=&quot;1693&quot; data-origin-height=&quot;1011&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bF0V52/btsKwptKsNw/MiDOotxo2FV3C2CVQJ8mg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bF0V52/btsKwptKsNw/MiDOotxo2FV3C2CVQJ8mg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bF0V52/btsKwptKsNw/MiDOotxo2FV3C2CVQJ8mg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbF0V52%2FbtsKwptKsNw%2FMiDOotxo2FV3C2CVQJ8mg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1693&quot; height=&quot;1011&quot; data-filename=&quot;Group 24 (1).png&quot; data-origin-width=&quot;1693&quot; data-origin-height=&quot;1011&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Tailwind CSS에서 동적 크기 클래스 문제와 해결 방법&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하던 중 Tailwind CSS를 사용할 때 동적 클래스 적용이 제대로 되지 않는 문제가 발생했습니다. UserImage 컴포넌트에서 image 크기를 h-[${size}]와 같은 형태로 자바스크립트 템플릿 리터럴을 사용하여, Tailwind가 이를 인식하지 못했기 때문입니다. 이번 글에서는 이런 문제의 이유와 해결 방법을 자세히 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;문제 상황 : UserImage 컴포넌트가 렌더링되지 않는 이유&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tailwind CSS는 정적으로 컴파일된 CSS 프레임워크입니다. 즉, 빌드 과정에서 사용된 모든 CSS 클래스를 스캔하고, 최적화된 CSS 파일을 생성합니다. h-[${size}], w-[${size}]와 같이 자바스크립트 문자열 템플릿으로 동적인 값을 전달하면 Tailwind는 이를 알 수 없기 때문에 해당 클래스를 컴파일하지 않습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1730701932187&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 기존 코드: Tailwind는 동적 문자열을 인식하지 못함
className={`h-[${size}] w-[${size}]`}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 코드는 런타임에서 값이 결정되기 때문에 Tailwind가 미리 인식할 수 없습니다. 그 결과, 컴파일된 CSS 파일에는 이 클래스가 포함되지 않으며, 브라우저에서 해당 스타일이 적용되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;해결 방법 1: 인라인 스타일 사용&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인라인 스타일을 사용하여 크기를 동적으로 지정하면 문제를 해결할 수 있습니다. Tailwind가 아닌 CSS로 직접 스타일을 지정하면 자바스크립트 변수로 값을 동적으로 설정할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730702243311&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function UserImage({
  user,
  size = '42px',
  type = 'disabled',
}: IUserProps) {
  const herf = type === 'link' ? `/profile/:${user?.accountname}` : '#';

  return (
    &amp;lt;Link
      href={herf}
      className=&quot;relative inline-block shrink-0 overflow-hidden rounded-full&quot;
      style={{ height: size, width: size }} // 인라인 스타일로 크기 설정
    &amp;gt;
      &amp;lt;Image
        src={user?.image || '/assets/icons/basic-profile-img-.svg'}
        alt={`${user?.username} 프로필 이미지`}
        fill
        className=&quot;object-cover&quot;
      /&amp;gt;
    &amp;lt;/Link&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;className에서 h-[${size}]와 w-[${size}]를 제거하고 대신 style 속성을 사용하여 height와 width를 동적으로 설정했습니다. 이렇게 하면 Tailwind가 아닌 일반 CSS로 동적인 크기를 처리할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;해결 방법 2: 삼항 연산자 사용&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삼항 연산자를 사용하여 정적 클래스를 조건부로 적용하는 방식도 있습니다. 이렇게 하면 Tailwind는 빌드 타임에 모든 조건부 클래스를 인식하고, 최적화된 CSS 파일에 포함시킬 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;export default function UserImage({
  user,
  size = '42px',
  type = 'disabled',
}: IUserProps) {
  const herf = type === 'link' ? `/profile/:${user?.accountname}` : '#';

  return (
    &amp;lt;Link
      href={herf}
      className={`relative inline-block shrink-0 overflow-hidden rounded-full ${
        size === '42px'
          ? 'h-[42px] w-[42px]'
          : size === '50px'
          ? 'h-[50px] w-[50px]'
          : size === '110px'
          ? 'h-[110px] w-[110px]'
          : 'h-[36px] w-[36px]'
      }`}
    &amp;gt;
      &amp;lt;Image
        src={user?.image || '/assets/icons/basic-profile-img-.svg'}
        alt={`${user?.username} 프로필 이미지`}
        fill
        className=&quot;object-cover&quot;
      /&amp;gt;
    &amp;lt;/Link&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에서는 size 값에 따라 클래스 이름을 삼항 연산자로 선택하여 적용했습니다. 각 조건의 결과는 정적인 문자열이기 때문에 Tailwind가 이를 인식하고 컴파일할 수 있습니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;결론: 인라인 스타일 사용&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서 Tailwind CSS에서 동적 클래스 문제를 해결하기 위해 위의 두 방법 중 최종적으로 인라인 스타일 사용 방법을 선택했습니다. 그 이유와 이 과정에서 느낀 점을 결론과 함께 정리하겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;span style=&quot;color: #333333;&quot;&gt;두 가지 방법 중 인라인 스타일 사용을 선택한 이유&lt;/span&gt;&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 유연한 스타일 적용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인라인 스타일을 사용하면 CSS에서 지원하는 모든 크기 값을 자유롭게 적용할 수 있습니다. 프로젝트에서 size 값이 다양하게 변경될 수 있는 상황이 있었고, 고정된 Tailwind 클래스로 조건을 설정하는 것보다 인라인 스타일이 더 유연하게 작동했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 간결하고 유지보수 쉬움&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삼항 연산자 방식을 사용하면 코드가 복잡해지고, size 값의 범위가 늘어날수록 코드의 가독성이 떨어지게 됩니다. 인라인 스타일은 단순히 CSS 속성으로 값을 전달하므로 코드가 더 간결해집니다. 이 방법이 추후에 유지보수에 유리하다고 판단했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 재사용성과 확장성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트가 발전함에 따라 다양한 크기 요구사항이 추가될 수 있는데, 이를 위해 Tailwind 설정을 확장하거나 삼항 연산자를 계속 늘리는 것은 비효율적입니다. 인라인 스타일은 이런 문제를 쉽게 해결해줍니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;span style=&quot;color: #333333;&quot;&gt;결론 및 느낀 점&lt;/span&gt;&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제를 해결하면서 Tailwind CSS의 컴파일 방식과 정적 클래스에 대해를 깊이 이해할 수 있었습니다. Tailwind CSS는 매우 편리하지만, 모든 상황에서 완벽한 해결책은 아닐 수 있다는 점, 특히 런타임에서 동적 스타일 적용이 필요할 때는 Tailwind의 정적 클래스 제한을 넘어서는 방법을 찾아야 한다는 점을 배웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 하며 특정 도구나 프레임워크의 한계를 마주할 때, 다양한 대안과 해결책을 모색하는 과정이 중요하다는 것을 다시 깨달았습니다. 최종적으로 인라인 스타일을 선택함으로써 코드의 유지보수성과 확장성을 높일 수 있었고, 이 과정에서 Tailwind CSS의 정적 컴파일 특성에 대한 이해를 더 깊게 할 수 있었습니다. 이 경험은 앞으로 다른 프로젝트에서도 Tailwind CSS를 더 전략적으로 사용할 수 있는 밑거름이 될 것입니다.&lt;/p&gt;</description>
      <category>  Programming/Next.js &amp;amp; project</category>
      <category>next.js</category>
      <category>tailwindcss</category>
      <category>typescript</category>
      <author>코등어</author>
      <guid isPermaLink="true">https://archive0313.tistory.com/55</guid>
      <comments>https://archive0313.tistory.com/55#entry55comment</comments>
      <pubDate>Sun, 27 Oct 2024 15:45:10 +0900</pubDate>
    </item>
    <item>
      <title>[next.js &amp;amp; jest] Next.js와 TanStack Query(React Query) 테스트 환경에서의 오류</title>
      <link>https://archive0313.tistory.com/54</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1693&quot; data-origin-height=&quot;1011&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqVofi/btsKlwT8aLm/WMYxikAl4yna65p2kBM9p0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqVofi/btsKlwT8aLm/WMYxikAl4yna65p2kBM9p0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqVofi/btsKlwT8aLm/WMYxikAl4yna65p2kBM9p0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqVofi%2FbtsKlwT8aLm%2FWMYxikAl4yna65p2kBM9p0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1693&quot; height=&quot;1011&quot; data-origin-width=&quot;1693&quot; data-origin-height=&quot;1011&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Next.js와 TanStack Query(React Query) 테스트 환경에서 발생한 오류 해결과정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 발생&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로그인 페이지 컴포넌트에서 테스트를 실행할 때 두 가지 주요 오류가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. &lt;b&gt;라우터 오류&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1729912942684&quot; class=&quot;bash custom-cursor-default-hover&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;invariant expected app router to be mounted&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. &lt;b&gt;쿼리 클라이언트 오류&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1729912954585&quot; class=&quot;bash custom-cursor-default-hover&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;No QueryClient set, use QueryClientProvider to set one&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제들은&amp;nbsp;&lt;b&gt;Next.js의 App Router&lt;/b&gt;와&amp;nbsp;&lt;b&gt;React Query의 QueryClient&lt;/b&gt;가 &lt;b&gt;테스트 환경에서 설정되지 않았기 때문에 발생&lt;/b&gt;했습니다. 각각의 문제를 해결하기 위해서 &lt;b&gt;라우터 모킹&lt;/b&gt;&amp;nbsp;및&amp;nbsp;&lt;b&gt;QueryClient 설정&lt;/b&gt;을 추가해야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;라우터 오류 해결: Next.js의 useRouter 모킹&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;오류 메시지&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729913354298&quot; class=&quot;bash custom-cursor-default-hover&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Error: Uncaught [Error: invariant expected app router to be mounted]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Next.js의 페이지 컴포넌트는 라우팅 기능을 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;useRouter&lt;span&gt;&amp;nbsp;&lt;/span&gt;훅을 사용합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;useRouter&lt;span&gt;&amp;nbsp;&lt;/span&gt;훅은 기본적으로 Next.js의&lt;span&gt;&amp;nbsp;&lt;/span&gt;App Router와 연동되어 있으며, 테스트 환경에서는 이 라우팅 시스템이 자동으로 제공되지 않습니다. 따라서 테스트에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;App Router가 없다는 오류가 발생하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결 방법&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;useRouter를 모킹&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;useRouter 훅이 라우팅 정보를 제공하지 못할 때 발생하는 문제를 해결하기 위해,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;라우터를 모킹&lt;/b&gt;하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;가짜 라우터&lt;/b&gt;를 &lt;b&gt;제공&lt;/b&gt;해줄 수 있습니다. Next.js에서는 jest.mock을 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;useRouter&lt;span&gt;&amp;nbsp;&lt;/span&gt;훅을 모킹하여 라우터가 없어도 테스트를 진행할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729914001181&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import '@testing-library/jest-dom'; 
import { render } from '@testing-library/react'; 
import Login from '@/app/(auth)/login/page'; 

// useRouter를 모킹 
jest.mock('next/navigation', () =&amp;gt; ({
  useRouter: jest.fn(),
}));

describe('Login', () =&amp;gt; { 
  it('로그인 헤더 타이틀 렌더링', () =&amp;gt; { 
    const { getByRole } = render(&amp;lt;Login /&amp;gt;); 
    const heading = getByRole('heading', { level: 1 }); // level 옵션을 사용하여 특정 heading level로 대상을 한정(&amp;lt;h1&amp;gt;)
    
    expect(heading).toBeInTheDocument(); 
    expect(heading).toHaveTextContent('로그인'); 
  }); 
});&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;jest.mock('next/navigation')&lt;/b&gt;: next/navigation 모듈을 모킹하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;라우팅 시스템을 가짜로 설정합니다. 이로 인해 테스트 환경에서는 실제 라우터가 없어도 useRouter 훅이 사용될 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;useRouter&lt;/b&gt;: useRouter 훅을 모킹하여 라우팅 동작을 정의하지 않아도 컴포넌트가 정상적으로 렌더링됩니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;쿼리 클라이언트 오류 해결: React Query의 QueryClient 설정&lt;/b&gt;&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;오류 메시지&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729914091004&quot; class=&quot;bash custom-cursor-default-hover&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;No QueryClient set, use QueryClientProvider to set one&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;React Query에서 useMutation이나 useQuery와 같은 훅을 사용하기 위해서는 반드시 QueryClientProvider로&lt;span&gt;&amp;nbsp;&lt;/span&gt;QueryClient를 제공해야 합니다. 테스트 환경에서도&lt;span&gt;&amp;nbsp;&lt;/span&gt;QueryClient가 제공되지 않으면 해당 오류가 발생합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결 방법&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;QueryClientProvider로 컴포넌트 감싸기&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;React Query의 클라이언트인&lt;span&gt;&amp;nbsp;&lt;/span&gt;QueryClient를 테스트에서 사용할 수 있도록 QueryClientProvider로 감싸줍니다&lt;/b&gt;. 이를 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;React Query의 훅들이 제대로 작동하도록 만듭니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729914547260&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import '@testing-library/jest-dom'; 
import { render, screen } from '@testing-library/react'; 
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; // React Query 관련 
import import Login from '@/app/(auth)/login/page'; 

// useRouter를 모킹 
jest.mock('next/navigation', () =&amp;gt; ({
  useRouter: jest.fn(),
}));
  
describe('Login', () =&amp;gt; { 
  it('로그인 헤더 타이틀 렌더링', () =&amp;gt; { 
    const queryClient = new QueryClient(); // QueryClient 생성 
    
    // QueryClientProvider로 감싸서 QueryClient 제공 
    render( 
      &amp;lt;QueryClientProvider client={queryClient}&amp;gt; 
        &amp;lt;Login /&amp;gt; 
      &amp;lt;/QueryClientProvider&amp;gt; ); 
        
    const heading = screen.getByRole('heading', { level: 1 }); 
    expect(heading).toBeInTheDocument(); expect(heading).toHaveTextContent('로그인'); 
    }); 
});&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;QueryClient&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;React Query의 클라이언트로, 데이터 fetching, 캐싱 및 동기화 등을 관리합니다. 이 클라이언트를 생성하여 테스트에서도 사용할 수 있도록 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;QueryClientProvider&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;React Query의 클라이언트 제공자로, 이를 통해 하위 컴포넌트에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;QueryClient에 접근하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;React Query&lt;span&gt;&amp;nbsp;&lt;/span&gt;훅들이 정상적으로 동작할 수 있도록 해줍니다. 테스트에서도 useQuery나 useMutation 같은 훅들이 사용되기 위해서는 QueryClientProvider로 컴포넌트를 감싸야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;라우터 모킹&lt;/b&gt;: 여전히 라우터는 모킹된 상태로, 페이지가 정상적으로 동작하게 해줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;트러블슈팅 과정 느낀점과 결론&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Next.js와 React Query를 사용하는 프로젝트에서, 테스트 환경에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;라우터와&lt;span&gt;&amp;nbsp;&lt;/span&gt;QueryClient&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 시스템을 명시적으로 설정해줘야 합니다. 각각의 모듈을 올바르게 모킹하고, 클라이언트를 제공함으로써 테스트에서 발생하는 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 트러블슈팅 경험을 통해 &lt;span&gt;테스트 환경에서의 라우팅과 상태 관리의 중요성을 깨달았습니다. 특히, 테스트를 위한 모킹(mocking)과&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;테스트 환경에서도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;클라이언트 설정이 필수적이라는 점을 확실히 배웠습니다.&lt;/span&gt;&lt;/span&gt; 앞으로 테스트 코드 작성 시에는 이러한 설정을 놓치지 않도록 더 철저한 테스트 환경을 구성해야겠습니다.&lt;/p&gt;</description>
      <category>  Programming/Next.js &amp;amp; project</category>
      <category>jest</category>
      <category>next.js</category>
      <category>queryClient</category>
      <category>queryprovider</category>
      <category>react-query</category>
      <category>tanstack-query</category>
      <category>useRouter</category>
      <author>코등어</author>
      <guid isPermaLink="true">https://archive0313.tistory.com/54</guid>
      <comments>https://archive0313.tistory.com/54#entry54comment</comments>
      <pubDate>Sat, 26 Oct 2024 13:21:20 +0900</pubDate>
    </item>
    <item>
      <title>[next.js &amp;amp; jest]  jest와 axios-mock-adapter를 활용한 테스트 방법</title>
      <link>https://archive0313.tistory.com/53</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Group 20.png&quot; data-origin-width=&quot;1693&quot; data-origin-height=&quot;1011&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brJAZJ/btsKipBbjNx/CamxyG4N6QfLS3gV9i1i41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brJAZJ/btsKipBbjNx/CamxyG4N6QfLS3gV9i1i41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brJAZJ/btsKipBbjNx/CamxyG4N6QfLS3gV9i1i41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrJAZJ%2FbtsKipBbjNx%2FCamxyG4N6QfLS3gV9i1i41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1693&quot; height=&quot;1011&quot; data-filename=&quot;Group 20.png&quot; data-origin-width=&quot;1693&quot; data-origin-height=&quot;1011&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;회원가입 API 테스트: Jest와 axios-mock-adapter를 활용한 테스트 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 회원가입 API가 정상적으로 작동하는지 확인하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Axios&lt;/b&gt;를 이용한 POST 요청을 테스트하는 내용을 다룹니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;axios-mock-adapter&lt;/b&gt;를 사용하여 네트워크 요청을 모킹(mocking)하여 실제 서버와의 통신 없이도 테스트를 진행하였습니다. 이 글에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;성공적인 회원가입 요청&lt;/b&gt;과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;에러가 발생하는 요청&lt;/b&gt;을 테스트하는 방법과 과정 중에 발생한 에러들에 대해 설명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Axios와 axios-mock-adapter 소개&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Axios&lt;/b&gt;: &lt;u&gt;HTTP 요청을 보내기 위한 JavaScript 라이브러리&lt;/u&gt;입니다. 브라우저와 Node.js 환경 모두에서 동작하며, 간단한 API로 POST, GET, PUT, DELETE 등 다양한 HTTP 요청을 보낼 수 있습니다. 특히 RESTful API와의 상호작용을 위한 개발에 많이 사용됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;axios-mock-adapter&lt;/b&gt;: &lt;u&gt;테스트 환경에서 Axios 요청을 모킹(mocking)하기 위한 라이브러리&lt;/u&gt;입니다. 실제 서버와의 통신 없이도 Axios를 사용하는 API 요청의 동작을 모킹할 수 있습니다. 이를 통해 서버의 상태에 의존하지 않고, 서버 응답을 시뮬레이션하여 다양한 상황을 테스트할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 설치 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axios와 axios-mock-adapter를 설치하려면 다음 명령어를 사용하면 됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729756880281&quot; class=&quot;bash custom-cursor-default-hover&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yarn add axios
yarn add -D axios-mock-adapter&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 axios-mock-adapter는 개발 및 테스트 환경에서만 사용되므로 -D 플래그를 사용해 설치합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3.&lt;span&gt;&amp;nbsp;&lt;/span&gt;테스트할 함수 작성&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;테스트 대상 함수: 회원가입 API 호출&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729756088581&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// signup.ts
import { postRequest } from '@/api/requests'; 
import { ISignUpRequest, ISignupResponse } from '@/api/types/auth'; 

export const signup = async (data: ISignUpRequest) =&amp;gt; { 
  const response = await postRequest&amp;lt;ISignupResponse, ISignUpRequest&amp;gt;('/user', data); 
    
  return response; 
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 함수는 postRequest를 통해 Axios POST 요청을 보내 회원가입을 처리합니다.&lt;/li&gt;
&lt;li&gt;ISignUpRequest: 회원가입 요청에 필요한 데이터를 정의한 타입.&lt;/li&gt;
&lt;li&gt;ISignupResponse: 서버에서 회원가입 요청이 성공했을 때 반환하는 데이터 타입.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4.&lt;span&gt; axios-mock-adapter&lt;/span&gt;를 사용한 테스트 코드&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;테스트 코드&lt;/b&gt;: &lt;b&gt;axios-mock-adapter&lt;/b&gt;를 사용하여 API 요청을 테스트하는 코드로 &lt;b&gt;회원가입 성공&lt;/b&gt;과&amp;nbsp;&lt;b&gt;회원가입 실패&lt;/b&gt;&amp;nbsp;두 가지 상황을 테스트합니다.&lt;/p&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729757151899&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// auth.test.ts
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';

import { signup } from '@/api/apiRequests/auth';
import instance from '@/api/index'; // axios 인스턴스 가져오기
import { ISignUpRequest, ISignupResponse } from '@/api/types/auth';

describe('signup API Test', () =&amp;gt; {
  let mock: MockAdapter;

  // 공통 회원가입 요청 데이터
  const requestData: ISignUpRequest = {
    username: '알파카',
    email: 'alpaca@naver.com',
    password: 'alpaca01!',
    accountname: 'alpaca_01',
    intro: '안녕 난 알파카!',
    image: '',
  };

  // 각 테스트 전에 Mock Adapter 설정
  beforeEach(() =&amp;gt; {
    mock = new MockAdapter(instance); // axios 인스턴스 모킹
  });

  // 각 테스트 후 Mock Adapter 리셋
  afterEach(() =&amp;gt; {
    mock.reset(); // 각 테스트가 끝날 때마다 모킹 초기화
  });

  /* -- 요청 성공 테스트 -- */
  it('회원가입 성공', async () =&amp;gt; {
    const responseData: ISignupResponse = {
      message: '회원가입 성공!',
      user: {
        _id: '1',
        username: '알파카',
        email: 'alpaca@naver.com',
        accountname: 'alpaca_01',
        intro: '안녕 난 알파카!',
        image: '',
      },
    };

    mock.onPost('/user').reply(200, responseData); // /user 경로에 대한 POST 요청 모킹, 200 응답과 함께 mock 데이터를 반환

    const response = await signup(requestData); // 실제 API 호출 함수 signup 실행

    expect(response).toEqual(responseData); // 응답 데이터가 모킹한 데이터와 같은지 확인
  });

  /* -- 요청 실패 테스트 -- */
  it('에러발생!', async () =&amp;gt; {
    mock.onPost('/user').reply(500); // /user 경로에 대한 POST 요청 모킹, 500 에러 응답

    try {
      await signup(requestData); // 실제 API 호출 함수 signup 실행
    } catch (error) {
      if (axios.isAxiosError(error)) { // 에러 상태 코드가 500인지 확인
        expect(error.response?.status).toBe(500);
      } else { // AxiosError가 아닐 경우 처리
        throw new Error('에러 발생!');
      }
    }
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;beforeEach와 afterEach&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;beforeEach&lt;/b&gt;: 각 테스트 전에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;MockAdapter 인스턴스&lt;/b&gt;를 생성하여 Axios 요청을 모킹합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;afterEach&lt;/b&gt;: 각 테스트 후 MockAdapter를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;리셋&lt;/b&gt;하여 테스트 간에 데이터가 섞이지 않도록 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성공적인 회원가입 요청 테스트&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;mock.onPost('/user').reply(200, responseData)&lt;/b&gt;: /user 경로로 POST 요청을 보낼 때,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;200 상태 코드&lt;/b&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;모킹된 성공 응답&lt;/b&gt;(responseData)을 반환합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;expect(response).toEqual(responseData)&lt;/b&gt;: 실제로 반환된 응답이 모킹된 응답과 동일한지 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실패하는 회원가입 요청 테스트&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;mock.onPost('/user').reply(500)&lt;/b&gt;: /user 경로로 POST 요청을 보낼 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;500 에러&lt;/b&gt;를 반환하도록 모킹합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;expect(error.response?.status).toBe(500)&lt;/b&gt;: 요청이 실패했을 때, 반환된 에러의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;상태 코드가 500인지&lt;/b&gt;를 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;5.&lt;span&gt;&amp;nbsp;&lt;/span&gt;트러블슈팅&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;1: &lt;b&gt;AxiosError: Network Error&lt;/b&gt;&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 API 테스트를 진행하는 중, AxiosError: Network Error가 발생하였는데 이는 Axios가 MockAdapter를 통해 설정된 모킹(mocking)을 인식하지 못하고, 실제 네트워크 요청을 시도하기 때문에 발생하는 문제입니다. 이 문제를 해결하려면 Axios 인스턴스가 올바르게 모킹되어 있는지 확인해야 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1-1. &lt;b&gt;문제 원인&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Axios 인스턴스와 MockAdapter가&lt;b&gt; 동일한 인스턴스를 사용하지 않아&lt;/b&gt; &lt;b&gt;문제가 발생&lt;/b&gt;했습니다. Axios는 Mock이 아닌 실제 네트워크 요청을 시도합니다.&lt;/li&gt;
&lt;li&gt;프로젝트에서 axios.create()로 별도의 Axios 인스턴스를 생성하여 사용하는 경우, MockAdapter도 그 &lt;b&gt;같은 인스턴스&lt;/b&gt;를 모킹해야 합니다. 그렇지 않으면 Mock이 적용되지 않고 실제 요청이 발생합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1-2. &lt;b&gt;문제 해결: Axios 인스턴스 통일&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MockAdapter를 사용할 때 Axios 인스턴스를 동일하게 사용하여 문제를 해결하였습니다. 저의 경우, 프로젝트에서 &lt;b&gt;Axios 인스턴스를 별도로 생성했기 때문에 그 인스턴스를 &lt;span data-token-index=&quot;1&quot;&gt;MockAdapter&lt;/span&gt;에 설정&lt;/b&gt;하였습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729758388856&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import axios { Axios } from 'axios';

// Axios 인스턴스 생성
const instance: Axios = axios.create({
  baseURL: process.env.NEXT_PUBLIC_BASE_URL,
  withCredentials: true,
});

export default instance;

/* POST 요청 */
export const postRequest = async &amp;lt;T, D&amp;gt;(
  url: string,
  data?: D,
  config?: AxiosRequestConfig,
): Promise&amp;lt;T&amp;gt; =&amp;gt; {
  const response = await instance.post(url, data, config); // Axios 인스턴스 사용
  return response.data;
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729758496073&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { instance } from '@/api/requests'; // 동일한 인스턴스 사용

describe('signup API Test', () =&amp;gt; {
  let mock: MockAdapter;
 
  beforeEach(() =&amp;gt; {
    mock = new MockAdapter(instance); // 동일한 axios 인스턴스를 모킹
  });
   ...
};​&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게&amp;nbsp;&lt;b&gt;동일한 Axios 인스턴스를 &lt;b&gt;테스트 파일에서&lt;/b&gt;&amp;nbsp;MockAdapter에 설정&lt;/b&gt;해야 실제 네트워크 요청 대신 모킹된 응답을 받을 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;2: &lt;b&gt;AxiosInstance와 Axios의 차이로 인한 타입 충돌&lt;/b&gt;&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 발생한 문제는 &lt;b&gt;타입 충돌&lt;/b&gt;입니다. 이 문제는 AxiosInstance와 Axios 간의 차이로 인해 발생하였는데 직전에 동일한 instance를 설정하였더니, 타입충돌 문제가 발생하였습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1. &lt;b&gt;Axios와 AxiosInstance의 차이&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Axios: &lt;b&gt;Axios 클래스&lt;/b&gt; 자체를 나타내며, axios.create()로 생성된 인스턴스가 아닌 &lt;b&gt;기본 클래스&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;AxiosInstance: axios.create()로 생성된 &lt;b&gt;특정 인스턴스&lt;/b&gt;를 나타내는 타입입니다. 이는 기본 설정을 포함한 구성된 Axios 객체를 나타냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-2. &lt;b&gt;왜 AxiosInstance를 사용하면 해결되는가?&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AxiosInstance는 Axios의 특정 인스턴스에 대한 타입을 명시적으로 정의해주기 때문에, 기본 설정(예: headers)과 관련된 타입 충돌을 방지할 수 있습니다.&lt;/li&gt;
&lt;li&gt;반면 Axios는 Axios 클래스 자체를 나타내기 때문에, 인스턴스에서 필요한 세부 설정 정보가 부족할 수 있으며, 그로 인해 타입 충돌이 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-3. &lt;b&gt;타입 충돌 해결&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AxiosInstance&amp;nbsp;인스턴스를 명시적으로 설정&lt;/b&gt;하여 헤더 정보와 같은 기본 설정들이 명확하게 정의되어 타입 충돌을 해결할 수 있었습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729758966251&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import axios, { AxiosInstance } from 'axios';

// AxiosInstance는 axios.create()로 반환되는 인스턴스의 타입
const instance: AxiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_BASE_URL,
  withCredentials: true,
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 &lt;b&gt;axios.create()로 반환된 인스턴스에 AxiosInstance 타입을 명시적으로 사용하면, Axios의 인스턴스에서 발생할 수 있는 설정과 관련된 타입 충돌을 방지&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;hr class=&quot;custom-cursor-on-hover&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;-&amp;gt; 트러블 슈팅 해결방법&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Axios 인스턴스 문제 해결&lt;/b&gt;: MockAdapter가 동일한 Axios 인스턴스에 대해 설정되지 않으면 네트워크 에러가 발생할 수 있습니다. MockAdapter는 항상 동일한 Axios 인스턴스를 모킹해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;타입 충돌 문제 해결&lt;/b&gt;: AxiosInstance를 사용하면 Axios 인스턴스에 대해 명확한 타입을 정의할 수 있어, 타입 충돌을 예방하고 더 안전한 타입 검사를 수행할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;br /&gt;결론&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;axios와 axios-mock-adapter를 사용해&lt;span&gt;&amp;nbsp;&lt;/span&gt;회원가입 API를 테스트하는 방법을 살펴보았습니다. 처음에는 MockAdapter를 통해 네트워크 요청을 모킹하는 과정에서&amp;nbsp;에러 처리나&amp;nbsp;타입 정의 문제&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;로 인한 어려움을 겪었습니다.&lt;span&gt; 그러나 이러한 문제를 해결하면서 axios와 axios-mock-adapter의 사용법을 더 깊이 이해하게 되었습니다. 이번 경험을 통해 테스트 환경에서 네트워크 요청을 어떻게 시뮬레이션하는지에 대한 지식이 쌓였고,&lt;/span&gt;&lt;/span&gt; 성공적인 응답과 실패한 응답을 각각 처리하는 방법을 배웠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 환경에서의 모킹(Mocking)과&lt;span&gt;&amp;nbsp;&lt;/span&gt;API 테스트는 코드의 안정성을 높이고, 서버가 없는 상황에서도 빠르게 테스트를 진행할 수 있게 해주기 때문에 이후에도 다양한 API 테스트에 이 방법을 적용해볼 계획입니다.&lt;/p&gt;</description>
      <category>  Programming/Next.js &amp;amp; project</category>
      <category>axios</category>
      <category>axios-mock-adapte</category>
      <category>jest</category>
      <category>next.js</category>
      <author>코등어</author>
      <guid isPermaLink="true">https://archive0313.tistory.com/53</guid>
      <comments>https://archive0313.tistory.com/53#entry53comment</comments>
      <pubDate>Thu, 24 Oct 2024 18:06:59 +0900</pubDate>
    </item>
    <item>
      <title>[next.js &amp;amp; jest] Jest 설치 및 구성 가이드: Next.js + TypeScript 환경에서의 Jest 사용법</title>
      <link>https://archive0313.tistory.com/52</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Group 18 (1).png&quot; data-origin-width=&quot;2040&quot; data-origin-height=&quot;1218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/U4NMt/btsKfrFUjY1/f59g26oWkff81NbZP1YraK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/U4NMt/btsKfrFUjY1/f59g26oWkff81NbZP1YraK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/U4NMt/btsKfrFUjY1/f59g26oWkff81NbZP1YraK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FU4NMt%2FbtsKfrFUjY1%2Ff59g26oWkff81NbZP1YraK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2040&quot; height=&quot;1218&quot; data-filename=&quot;Group 18 (1).png&quot; data-origin-width=&quot;2040&quot; data-origin-height=&quot;1218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-4o&quot; data-message-id=&quot;525b521a-3ec1-42d8-a9db-3de1d0dd5a09&quot; data-message-author-role=&quot;assistant&quot;&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Jest 설치 및 구성 가이드: Next.js + TypeScript 환경에서의 Jest 사용법&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는&amp;nbsp;&lt;b&gt;Next.js&lt;/b&gt;와 &lt;b&gt;TypeScript&lt;/b&gt; 프로젝트에서 &lt;b&gt;Jest&lt;/b&gt;를 설치하고 구성하는 방법을 단계별로 설명합니다. Jest는 자바스크립트 테스트 프레임워크로, &lt;b&gt;테스트 코드 작성&lt;/b&gt;과 &lt;b&gt;테스트 실행&lt;/b&gt;을 빠르고 쉽게 할 수 있도록 도와줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. &lt;b&gt;Jest 설치&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Step 1: Jest 패키지 설치&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, Jest와 TypeScript 환경에서 Jest를 사용할 수 있도록 필요한 패키지를 설치해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729597407529&quot; class=&quot;bash custom-cursor-default-hover&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yarn add jest ts-jest jest-environment-jsdom @types/jest @testing-library/react @testing-library/jest-dom @testing-library/dom --dev&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;jest&lt;/b&gt;: Jest의 핵심 라이브러리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ts-jest&lt;/b&gt;: TypeScript를 사용하여 Jest 테스트를 실행할 수 있도록 도와주는 패키지.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@types/jest&lt;/b&gt;: Jest의 타입 정의 파일로, TypeScript 프로젝트에서 Jest의 타입을 인식할 수 있게 해줌.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;jest-environment-jsdom&lt;/b&gt;: DOM 환경에서 Jest 테스트를 실행할 수 있게 해주는 패키지.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@types/jest&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: &lt;/span&gt;Jest의 타입 정의 파일로, TypeScript에서 Jest를 사용할 때 타입 지원을 제공.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@testing-library/react&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: &lt;/span&gt;React 컴포넌트의 UI를 테스트하는 데 사용. 사용자 관점에서 UI를 테스트할 수 있게 도와줌.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@testing-library/jest-dom&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Jest에서 DOM 관련 매처(예: toBeInTheDocument() 등)를 사용할 수 있게 도와줌.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@testing-library/dom&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: &lt;/span&gt;DOM 조작을 테스트할 수 있는 도구를 제공.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. &lt;b&gt;Jest 기본 설정&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Step 2: Jest 설정 파일 생성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js 프로젝트에서 Jest를 설정하려면 jest.config.js 파일과 jest.setup.js 파일을 생성해야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1729598869763&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* jest.setup.js */

import '@testing-library/jest-dom' // React Testing Library에서 사용하는 설정&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729597759586&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* jest.config.cjs */

const nextJest = require('next/jest'); // Next.js 프로젝트에서 Jest를 사용할 수 있도록 도와주는 함수 

const createJestConfig = nextJest({ 
    dir: './', // Next.js 앱의 루트 디렉토리 설정
}); 

const customJestConfig = { 
    testEnvironment: 'jest-environment-jsdom', // DOM 환경에서 테스트 
    setupFilesAfterEnv: ['&amp;lt;rootDir&amp;gt;/jest.setup.js'], // 테스트 환경 설정 파일 
    moduleNameMapper: { 
    	'^@/(.*)$': '&amp;lt;rootDir&amp;gt;/src/$1', // 절대 경로를 설정하기 위한 경로 매핑  
    }, 
    transform: { 
    	'^.+\\.(ts|tsx)$': 'ts-jest', // TypeScript 파일을 Jest가 인식하도록 설정 
    }, 
}; 

module.exports = createJestConfig(customJestConfig);​&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. &lt;b&gt;폴더 구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 내의 테스트 파일들은 일반적으로 __tests__ 디렉토리 안에 .test.tsx 또는 .spec.ts 파일 형식으로 작성합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729599027131&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/project-root 
├── /src 
│ ├── /app 
│ │ └── login.tsx 
│ ├── /components 
│ │ └── Header.tsx 
│ ├── /__tests__ 
│ │ └── login.test.tsx
├── jest.config.js 
├── jest.setup.js 
├── tsconfig.json 
└── package.json&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;__tests__: 테스트 파일들을 모아 놓는 폴더입니다. 이 폴더에 *.test.tsx 또는 *.spec.ts 파일들을 저장할 수 있습니다.&lt;/li&gt;
&lt;li&gt;테스트 파일은 프로덕션 파일과 동일한 경로에 위치할 수도 있고, 별도의 __tests__ 폴더에 저장할 수도 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. &lt;b&gt;TypeScript 설정과 Jest 통합&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Step 3: TypeScript 설정 파일(tsconfig.json) 업데이트&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jest와 TypeScript의 호환성을 위해 tsconfig.json 파일에 Jest와 관련된 설정을 추가합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729599417197&quot; class=&quot;javascript custom-cursor-default-hover&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{ 
    &quot;compilerOptions&quot;: { 
        &quot;types&quot;: [&quot;node&quot;, &quot;jest&quot;, &quot;@testing-library/jest-dom&quot;],
        &quot;lib&quot;: [&quot;dom&quot;, &quot;dom.iterable&quot;, &quot;esnext&quot;], 
        &quot;module&quot;: &quot;esnext&quot;, 
        &quot;moduleResolution&quot;: &quot;bundler&quot;, 
        &quot;jsx&quot;: &quot;preserve&quot;, 
        &quot;skipLibCheck&quot;: true, 
        &quot;esModuleInterop&quot;: true 
        ...
    },
    &quot;include&quot;: [&quot;next-env.d.ts&quot;, &quot;**/*.ts&quot;, &quot;**/*.tsx&quot;, &quot;__tests__/**/*.ts&quot;, &quot;__tests__/**/*.tsx&quot;], 
    &quot;exclude&quot;: [&quot;node_modules&quot;] 
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;types&lt;/b&gt;: Jest와 Node.js의 타입을 포함하여 Jest 환경에서 TypeScript 타입 에러가 발생하지 않도록 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;include&lt;/b&gt;: Jest 테스트 파일(__tests__ 폴더나 *.test.sx, *.test.tsx 파일)을 포함시킵니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. &lt;b&gt;Jest 실행&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Step 4: 테스트 실행&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정이 완료되었으면 package.json에 아래와 같이 Jest 실행 스크립트를 추가합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729600134260&quot; class=&quot;bash custom-cursor-default-hover&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{ 
    &quot;scripts&quot;: { 
        ...
        &quot;test&quot;: &quot;jest&quot;,
        &quot;test:watch&quot;: &quot;jest --watch&quot; // 파일이 변경되면 테스트를 다시 실행
        ...
    } 
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 테스트를 실행할 때는 다음 명령어를 사용합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729600181604&quot; class=&quot;bash custom-cursor-default-hover&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yarn test&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Programming/Next.js &amp;amp; project</category>
      <category>jest</category>
      <category>next.js</category>
      <category>react</category>
      <category>typescript</category>
      <category>yarn</category>
      <author>코등어</author>
      <guid isPermaLink="true">https://archive0313.tistory.com/52</guid>
      <comments>https://archive0313.tistory.com/52#entry52comment</comments>
      <pubDate>Tue, 22 Oct 2024 22:09:08 +0900</pubDate>
    </item>
  </channel>
</rss>