<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ahn_829.log</title>
        <link>https://velog.io/</link>
        <description>개발댕발</description>
        <lastBuildDate>Sat, 27 Dec 2025 09:17:53 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ahn_829.log</title>
            <url>https://velog.velcdn.com/images/ahn-sujin/profile/559221b3-70c3-42b3-8b70-5e51ebf384b6/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ahn_829.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ahn-sujin" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Next.js 보안 취약점 이슈에 대하여...]]></title>
            <link>https://velog.io/@ahn-sujin/Next.js-%EB%B3%B4%EC%95%88-%EC%B7%A8%EC%95%BD%EC%A0%90-%EC%9D%B4%EC%8A%88%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@ahn-sujin/Next.js-%EB%B3%B4%EC%95%88-%EC%B7%A8%EC%95%BD%EC%A0%90-%EC%9D%B4%EC%8A%88%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Sat, 27 Dec 2025 09:17:53 GMT</pubDate>
            <description><![CDATA[<p>어느 평화롭던 토요일 아침, AWS에서 메일을 받게되면서 이야기는 시작됩니다.😇</p>
<p>프론트 서버 한대가 죽었다는 알림이었는데요. 트래픽이 몰리거나 메모리 문제 혹은 DDoS 공격 등으로 인해 간헐적으로 발생하던 문제였기 때문에 메뉴얼대로 처리하고 원인에 대해서는 월요일에 파악하자고 하면 마무리되는 듯 했습니다. 그런데 한 10분 정도 지났을까요? 또 다시 서버 한대가 죽었다는 AWS 메일이 왔습니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/d13f6947-bdc1-469f-bf03-b6de8ea95ab2/image.jpg" alt=""></p>
<p>뭔가 잘못됐다는 것을 느꼈고, 몇일 전 Next.js 보안 이슈가 발표됐다는 것 알게되었습니다. 이번 이슈는 단순 라이브러리 경고 수준이 아닌, 구조적으로 서버 코드 실행까지 이어질 수 있는 취약점이었기 때문에 실제 운영 관점에서 많은 점을 다시 점검하게 된 계기였습니다.</p>
<p>이 글에서는 취약점이 어떤 문제였는지, 서비스에 어떤 영향을 줄 수 있었는지, 어떻게 대응했는지 경험을 바탕으로 정리했습니다.</p>
<hr>
<h2 id="react2shell-이-뭔데">React2Shell 이 뭔데?</h2>
<p><strong>React2Shell(CVE-2025-55182)</strong> 은 React Server Components의 RSC 프로토콜에서 발견된 취약점으로, CVSS 점수가 10.0인 최고 등급의 보안 취약점입니다.
React 19의 react-server 패키지가 RSC Flight 프로토콜 처리 과정에서 <strong>안전하지 않은 역직렬화로 인해 악의적인 HTTP 요청을 통해 서버에 임의의 코드를 실행</strong> 할 수 있는 문제입니다.</p>
<p>이 취약점이 특히 심각한 이유는 다음과 같습니다.</p>
<h4 id="1-기본-설정만으로-취약">1. 기본 설정만으로 취약</h4>
<p><code>create-next-app</code> 으로 만든 기본 프로젝트도 공격 대상이 됩니다. Next.js 15의 App Router는 RSC를 기본으로 사용하는데, 바로 이 구조 자체에 취약점이 있었습니다.</p>
<h4 id="2-인증-없는-원격-코드-실행">2. 인증 없는 원격 코드 실행</h4>
<p>공격자는 인증 없이 단 하나의 조작된 HTTP 요청만으로 서버에서 코드를 실행할 수 있습니다. </p>
<h4 id="3-광범위한-영향">3. 광범위한 영향</h4>
<p>React 사용자의 82%, 클라우드 환경의 39%가 취약한 버전을 사용 중이었습니다. 수많은 서비스가 잠재적인 공격 대상이었던 것입니다.</p>
<h4 id="4-빠른-무기화">4. 빠른 무기화</h4>
<p>취약점 공개 후 불과 몇 시간 만에 실제 공격이 시작되었다고 합니다. 중국 국가 지원 해킹 그룹을 포함한 여러 공각자들이 즉각 움지였고, 저희 서비스 또한 그 표적이 되었던 것입니다.</p>
<br />

<h2 id="문제가-발생한-이유는">문제가 발생한 이유는?</h2>
<h3 id="상황">상황</h3>
<p>RSC를 사용하는 곳에 직렬화하는 부분에서 의도적으로 조작된 URL이 들어오게 되었습니다. 그 URL은 사실상 서비스에서 제공하지 않는 비정상적인 요청이었는데도 불구하고 Next.js에서 정상적인 요청으로 받아들였습니다.</p>
<p>여기서 문제는 Next.js가 <code>next-action</code> 헤더가 포함된 요청을 받으면 개발자가 실제로 서버 액션을 작성했는지 여부와 상관없이 무조건 해당 요청의 데이터를 처리하려고 시도한다는 점이었습니다. 공격자는 이 점을 악용해 <code>next-action</code> 헤어만 붙여서 악성 코드를 서버로 전송할 수 있었습니다. </p>
<h3 id="문제">문제</h3>
<p>그 조작된 요청은 중국 암호화폐 채굴 스크립트를 실행하는 URL이었고, 그 공격으로 인해 CPU 사용률이 급격히 증가하면서 서버가 계속해서 다운되는 현상이 발생했습니다.</p>
<p>공격이 성공하면 서버에서는</p>
<ul>
<li>cpu 사용률 급증</li>
<li>외부 중국 서버로 지속적인 네트워크 연결 시도</li>
<li>헬스체크 실패로 인스턴스 종료</li>
<li>오토 스케일링으로 새 인스턴스 생성 -&gt; 다시 공격 -&gt; 반복</li>
</ul>
<p>처음에는 한두대였지만, 몇 분 간격으로 추가적으로 서버가 다운되면서 상황은 심각해졌습니다.</p>
<h3 id="원인">원인</h3>
<p>React2Shell(CVE-2025-55182)의 핵심 원인은 <strong>안전하지 않은 역직렬화(Unsafe Deserialization)</strong> 문제입니다.</p>
<p>React Server Components는 클라이언트와 서버가 데이터를 주고받을 때 Flight 프로토콜이라는 방식을 사용합니다. 이 과정에서 데이터를 <code>&quot;직렬화(문자열로 변환)&quot; → &quot;전송&quot; → &quot;역직렬화(다시 객체로 복원)&quot;</code> 하는데, 바로 이 역직렬화 과정에서 보안 검증이 부족했습니다.</p>
<ol>
<li>공격자가 겉으로 보기엔 정상적인 데이터를 전송</li>
<li>Next.js 서버가 이 데이터를 받아서 처리하기 시작</li>
<li>숨겨진 특수 속성들이 Javascript의 핵심 기능에 접근</li>
<li>공격자가 서버에서 원하는 코드를 실행</li>
</ol>
<p>Next.js는 받은 데이터가 이상한 속성이 포함되어 있는지, 위험 코드가 실행될 수 있는지 등을 제대로 확인하지 않았습니다. 이 취약점이 특히 위험한 이유는 인증이 불필요하고 기본 설정으로도 취약하며 공격 성공률이 거의 100%에 달했기 때문입니다.</p>
<br />


<h2 id="어떻게-해결했지">어떻게 해결했지?</h2>
<h4 id="1-버전-업데이트">1. 버전 업데이트</h4>
<p>가장 우선적으로 수행한 작업은 Next.js 버전 업데이트였습니다. 또한 Next.js 버전 업데이트와 관련된 다른 라이브러리 버전 또한 체크하여 업데이트 진행했습니다.</p>
<blockquote>
<p><strong>공식 권장 패치 버전</strong>
Next.js 15.0.x → 15.0.7
Next.js 15.1.x → 15.1.11
Next.js 15.2.x → 15.2.8
Next.js 15.3.x → 15.3.8
Next.js 15.4.x → 15.4.10
Next.js 15.5.x → 15.5.9
Next.js 16.0.x → 16.0.10</p>
</blockquote>
<h4 id="2-긴급-재배포">2. 긴급 재배포</h4>
<p>패치 버전으로 업데이트한 후 즉시 프로덕션 환경에 재배포했습니다. 주말이었음에도 불구하고 밤늦게까지 고생한 동료들에게 너무 감사합니다😭</p>
<h4 id="3-보안-키-점검">3. 보안 키 점검</h4>
<p>패치된 버전으로 업데이트를 진행하면서 서비스에서 사용중인 시크릿을 점검했습니다. 공격자가 이미 환경 변수나 시크릿에 접근했을 가능성을 배제할 수 없었기 때문입니다.</p>
<h4 id="4-외부-방화벽-도입">4. 외부 방화벽 도입</h4>
<p>또한 앞으로 이런 일을 예방하기 위해 내년 초 웹 방화벽 도입을 검토 중에 있습니다. 방화벽 도입 시 서비스에 도달하기 전 네트워크 레벨에서 비정상적인 요청 패턴을 차단할 수 있어 프레임워크 취약점이 발견되더라도 한 단계 더 안전한 방어막을 구축할 수 있습니다. </p>
<br />

<h2 id="느낌점">느낌점</h2>
<p>이번 React2Shell 취약점 대응을 겪어보면서 보안에 대한 인식이 많이 달라졌습니다.</p>
<p>평소 Next.js 블로그나 커뮤니티에서 올라오는 버전 업데이트 공지를 무심하게 지나쳤던 것을 많이 반성하게 되었습니다. &quot;major 버전만 올리면 되지 않을까?&quot;, &quot;보안 패치는 급하지 않겠지&quot; 하는 안일한 생각이 얼마나 위험했던 것인지 반성하게 되었습니다.</p>
<p>이번 일을 계기로 Next.js의 보안 이슈 및 소식에 대해 Slack으로 즉시 알림을 받을 수 있도록 설정했습니다. 보안 패치는 공개된 순간부터 공격이 시작될 수 있기 때문에, 커뮤니티의 소식을 빠르게 캐치하는 것이 첫 번째 방어선이라는 것을 깨달았습니다.</p>
<p>또한 AWS 알림의 중요성도 절실히 느꼈습니다. 만약 AWS에서 메일이 오지 않았다면 서버가 다운되는 것을 훨씬 늦게 발견했을 것이고 그만큼 서비스 장애 시간도 길어졌을 것입니다. 토요일 아침이라 모니터링 대시보드를 능동적으로 확인하지 않았던 상황에서, 실시간 알림이 즉각적인 대응을 가능하게 해주었습니다. 서버 상태, 리소스 사용률, 비정상 패턴 등을 실시간으로 모니터링하고 즉각 알림받을 수 있는 시스템 구축이 얼마나 중요한지 실감했습니다.</p>
<p>React2Shell 취약점은 현대 웹 개발 생태계에서 얼마나 빠르게 보안 위협이 확산될 수 있는지를 깨닫게 해주었습니다. 단순히 기술적인 문제를 넘어서, 조직의 보안 대응 체계, 모니터링 시스템, 그리고 개발자의 보안 의식까지 전반적으로 점검하게 만든 계기였습니다.</p>
<p>우리 모두 정기적으로 확인하는 습관을 가지면 좋을 것 같습니다 🙌🏻</p>
<pre><code>// 프로젝트에 설치된 npm 패키지들의 보안 취약점을 검사하는 명령어
pnpm audit
</code></pre><br />

<blockquote>
<p>📚 참고 자료</p>
</blockquote>
<ul>
<li><a href="https://nextjs.org/blog/CVE-2025-66478">Next.js Security Advisory: CVE-2025-66478</a></li>
<li><a href="https://aws.amazon.com/ko/blogs/security/china-nexus-cyber-threat-groups-rapidly-exploit-react2shell-vulnerability-cve-2025-55182/">AWS Security Blog: China-nexus cyber threat groups rapidly exploit React2Shell</a></li>
<li><a href="https://pnpm.io/cli/audit">pnpm audit</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[모노레포에서 TailwindCSS 기반 디자인 시스템을 만들 수 없는 이유]]></title>
            <link>https://velog.io/@ahn-sujin/%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC%EC%97%90%EC%84%9C-TailwindCSS-%EA%B8%B0%EB%B0%98-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%84-%EB%A7%8C%EB%93%A4-%EC%88%98-%EC%97%86%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@ahn-sujin/%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC%EC%97%90%EC%84%9C-TailwindCSS-%EA%B8%B0%EB%B0%98-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%84-%EB%A7%8C%EB%93%A4-%EC%88%98-%EC%97%86%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sun, 23 Nov 2025 08:20:37 GMT</pubDate>
            <description><![CDATA[<p>모노레포 기반으로 여러 프로젝트를 운영하는 과정에서, 공통으로 사용할 디자인 시스템의 필요성이 커져 별도 패키지로 구성하게 되었는데요. 기본적인 토큰(foundation)은 CSS로 구축했고, 컴포넌트는 각 프로젝트에서 사용하는 방식과 동일하게 TailwindCSS 기반의 className으로 스타일링했습니다. </p>
<p>이때 디자인 시스템 패키지에는 TailwindCSS를 설치하지 않고, 각 프로젝트에 설치된 Tailwind가 내부 className을 스캔해 스타일을 생성해줄 것이라 기대했습니다.</p>
<p>하지만 동일한 컴포넌트를 사용하는 두 프로젝트에서 한쪽은 정상적으로 스타일이 적용되었고, 다른 한쪽에서는 Tailwind 클래스가 누락되는 문제가 발생했습니다. 이 예상치 못한 차이를 분석하는 과정에서 여러 원인과 구조적인 제약을 확인하게 되었고, 제가 겪었던 경험에 대해서 정리해보고자 합니다!</p>
<hr>
<h2 id="1-프로젝트-구조">1. 프로젝트 구조</h2>
<p>모노레포는 다음과 같은 형태로 구성되어 있습니다!
(이해를 돕기 위한 참고용 구조입니다.)</p>
<pre><code>├─ apps
│  ├─ project-a
│  ├─ project-b
│  └─ ...
│
└─ packages
   ├─ design-system
   ├─ components
   ├─ hooks
   └─ utils
</code></pre><p>각 프로젝트는 apps 디렉토리 아래에 위치하며, packages에는 여러 프로젝트에서 공통으로 사용할 수 있는 패키지들(components, hooks, utils 등) 이 존재하는 구조입니다.</p>
<p>이 중 디자인 시스템은 다음과 같은 형태로 구성되어 있는데요.
빌드 결과물은 dist로 생성되며, 각 컴포넌트는 반드시 index.ts에서 export 해주어야합니다.</p>
<blockquote>
<h4 id="dist-란">dist 란?</h4>
</blockquote>
<ul>
<li>빌드된 결과물이 담기는 디렉토리를 말합니다.</li>
<li>개발 시 작성하는 토드틑 src 내부에 있지만 이 코드를 그대로 다른 프로젝트에서 사용하기 어렵습니다. (Typescrip, JSX, 내부 경로 등 그대로는 브라우저나 외부 프로젝트가 이해하지 못합니다ㅠ)</li>
<li>그래서 빌드 도구(tsup) 을 통해 JS로 변환하고 번들링한 최종 산출물을 생성하는데, 이 최종 산출물이 저장되는 폴더가 dist입니다!</li>
<li>즉, <strong>외부 프로젝트에서 실제 가져다 쓰는 배포용 코드가 담긴 폴더</strong> 라고 할 수 있습니다</li>
</ul>
<pre><code>packages
└─ design-system
   ├─ dist
   ├─ node_modules
   ├─ src
   │  ├─ component
   │  ├─ contents
   │  ├─ theme
   │  ├─ types
   │  ├─ index.ts
   │  └─ index.css
   ├─ package.json
   ├─ tsconfig.json
   └─ tsup.config.ts

</code></pre><br />

<h2 id="2-문제">2. 문제</h2>
<p>디자인 시스템을 적용하고 발생한 문제는 동일한 컴포넌트를 사용했는데 <strong>프로젝트마다 TailwindCSS 클래스 적용 결과가 다르게 나타난다</strong> 는 점이었습니다.</p>
<p>A 프로젝트에서는 디자인 시스템에서 정의한 Tailwind 클래스가 문제없이 적용되었지만, B 프로젝트에서는 <strong>동일한 컴포넌트임에도 일부 클래스가 누락</strong> 되어 스타일이 깨지는 현상이 발생했습니다.</p>
<table>
<thead>
<tr>
<th>A 프로젝트(적용 O)</th>
<th>B 프로젝트(적용 X)</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/ahn-sujin/post/d11c5ac7-e8cc-4b1a-b718-7c47aec265d0/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/ahn-sujin/post/1d51a817-bb5e-4af7-b5cc-d7e0f0cad30c/image.png" alt=""></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
</tbody></table>
<p>같은 디자인 시스템을 import하여 사용하는데도 불구하고 특정 클래스만 적용되지 않는 상황이 반복되었습니다...😭 </p>
<p>처음에는 Tailwind 설정 문제로 추측했지만 두 프로젝트 모두 동일한 설정을 가지고 있었기 때문에 단순한 설정 이슈로는 보이지 않았습니다. 겉으로 보기에는 환경 구성도 동일했기 때문에 문제의 원인을 바로 파악하기 어려웠습니다.</p>
<br />

<h2 id="3-액션">3. 액션</h2>
<p>문제의 원인을 파악하기 위해 여러 방향에서 하나씩 검증을 진행했습니다. 처음에는 단순한 설정 문제라고 생각했지만 확인 과정이 반복될수록 디자인 시스템 패키지와 tailwind의 동작 방식이 예상과 다르다는 점을 알 수 있었습니다!</p>
<h3 id="첫번째-시도-tailwindconfigjs-경로-확인--❌-">첫번째 시도: tailwind.config.js 경로 확인 ( ❌ )</h3>
<p>GPT의 도움을 받아 가장 처음 수정했던 부분은 tailwind의 content 설정이었습니다. content는 해당 경로에 존재하는 파일을 스캔하여 className을 추출하고 그 결과를 기반으로 최종 CSS를 생성합니다. </p>
<p>따라서, 디자인 시스템 내부 파일이 스캔 대상에서 누락되고 있다면 일부 클래스가 생성되지 않는 문제가 발생할 수 있겠다는 가설이었고, 디자인 시스템 컴포넌트가 있는 경로를 아래와 같이 추가해주었습니다.</p>
<pre><code class="language-javascript">content: [
&#39;./app/**/*.{jsx,tsx,mdx}&#39;, 
&#39;./components/**/*.{jsx,tsx,mdx}&#39;
// 디자인 시스템 경로 추가
&#39;../packages/design-system/components/**/*.{js,ts,jsx,tsx}&#39;
],
...
</code></pre>
<p>하지만 결과는 동일했습니다. 애초에 설정 자체는 두 프로젝트 모두 동일하게 되어있었기 때문에 단순히 content 범위 오류로 발생한 문제는 아니었습니다.</p>
<p>(그리고 모노레포 구조 특성상 app 디렉토리 밖으로 나가면 이론상 프로젝트를 벗어나가는건데 packages에 접근이 가능한지도 정확하지 않았습니다 😇)</p>
<h3 id="두번째-시도-nextjs-버전--❌-">두번째 시도: next.js 버전 ( ❌ )</h3>
<p>다음으로 확인한 부분은 두 프로젝트가 사용하고 있는 Next.js 버전 차이었습니다. 
A프로젝트는 15.3.5, B프로젝트는 15.5.5 버전을 사용하고 있었고 혹시나 두 버전 사이에 TailwindCSS 처리 방식이 달라졌을 가능성을 의심했습니다.</p>
<p>테스트를 위한 신규 프로젝트를 15.3.5 버전으로 설치한 결과 동일하게 디자인 시스템 스타일이 깨지는 문제가 발생하는 것을 확인할 수 있었습니다. 따라서 Next.js 자체의 문제는 아니라는 결론을 내릴 수 있었습니다.</p>
<p>즉, 문제의 핵심은 버전이나 설정 문제가 아닌 tailwind의 스캔 범위 또는 빌드 타이닝에 있다는 방향으로 좁혀졌습니다.</p>
<h3 id="세번째-시도-tailwindcss-빌드-타이밍--✅-">세번째 시도: tailwindCSS 빌드 타이밍 ( ✅ )</h3>
<p>문제의 원인을 보다 명확히 확인하기 위해 TailwindCSS가 실제로 어떤 클래스를 빌드하는지 직접 확인했습니다.
<strong>Next.js는 빌드 시 Tailwind가 스캔한 className을 기반으로 최종 CSS 파일 (layout.css) 을 생성</strong> 하는데, 만약 디자인 시스템 컴포넌트가 스캔되지 않았다면 해당 클래스는 layout.css에 생성되지 않을 것입니다.</p>
<p>이를 확인하기 위해 A 프로젝트와 B 프로젝트 각각의 빌드 결과물을 비교했습니다. 그 결과 다음과 같은 사실을 확인했습니다.</p>
<table>
<thead>
<tr>
<th>A 프로젝트</th>
<th>B 프로젝트</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/ahn-sujin/post/e1f05ade-6c42-4da6-b797-1d6c201e2e9a/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/ahn-sujin/post/02784ca4-30e0-480e-b474-e9fbeef03ff0/image.png" alt=""></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
</tbody></table>
<ul>
<li>A 프로젝트의 layout.css에는 <code>mt-7, text-4xl</code> 등 디자인 시스템에서 사용한 클래스가 모두 존재했습니다.</li>
<li>B 프로젝트의 layout.css에는 일부 <strong>클래스가 아예 존재하지 않았습니다.</strong> (즉, Tailwind가 스캔하지 못했고 CSS 생성 자체가 되지 않은 상태)</li>
</ul>
<p>이 차이를 확인하면서 문제의 원인은 더욱 명확해졌습니다. <strong>디자인 시스템 패키지 내부의 파일이 Tailwind 스캔 대상에 포함되지 않았기 때문에, 프로젝트마다 생성되는 CSS가 달라지고, 그 결과 컴포넌트가 정상적으로 렌더링되지 않는 구조적 문제</strong> 가 발생한 것입니다.</p>
<br />

<h2 id="4-원인과-해결-방법">4. 원인과 해결 방법</h2>
<h3 id="원인">원인</h3>
<p>결론적으로 문제의 핵심은 <strong>TailwindCSS가 스타일을 생성하는 시점에 <code>packages/design-system</code> 내부의 className을 스캔하지 못한다</strong>는 것이었습니다.</p>
<p>TailwindCSS는 content 옵션에 포함된 경로만 기준으로 실제 사용된 className을 스캔해 layout.css 에 필요한 유틸리티 클래스를 생성합니다. 하지만 monorepo 구조에서 <strong>프로젝트 밖의 패키지(packages/design-system)는 content 스캔 범위에 포함되지 않는다</strong> 는 것을 알 수 있었습니다.</p>
<p>정리하자면, </p>
<ul>
<li>디자인 시스템 컴포넌트 내부에서 사용한 Tailwind 클래스는 스캔 대상에 포함되지 않음</li>
<li>빌드된 CSS(layout.css)에 해당 클래스가 생성되지 않음</li>
<li>A 프로젝트 → 우연히 동일한 유틸리티 클래스를 자체적으로 사용하고 있어 정상 노출</li>
<li>B 프로젝트 → 해당 클래스가 프로젝트 내부에서 사용되지 않아 CSS가 생성되지 않았고, 스타일이 누락됨</li>
</ul>
<p>즉, 특정 프로젝트만 깨진 것이 아니라, Tailwind가 아예 해당 클래스를 빌드하지 않았기 때문에 발생한 구조적 문제였습니다.</p>
<h3 id="해결-방법">해결 방법</h3>
<p>지금의 문제를 해결할 수 있는 방법은 3가지로 추릴 수 있었습니다.</p>
<p><strong>1. CSS 사용</strong>
➡️ TailwindCSS 의존도를 낮추고 디자인 시스템 패키지 내부에서 직접 CSS를 정의해 제공하는 방법입니다. 이렇게 하면 각 프로젝트의 Tailwind 스캠 여부와 무관하에 안정적으로 스타일이 적용되기 때문에 가장 확실한 해결책이라고 할 수 있습니다. 다만, CSS 파일 관리에 대해서 팀원들과 충분한 대화가 필요합니다. (컨벤션 등...)</p>
<p><strong>2. tailwind.config.js content 경로 추가</strong>
➡️ Tailwind 가 스타일을 생성하려면 해당 경로에 있는 파일들을 스캔할 수 있어야하기 때문에 가장 논리적인 정석 방법이라고 할 수 있습니다. 하지만, 모노레포 구조에서 어떻게 적용해야하는지 그 방법에 대해서 찾아야하고 프로젝트마다 설정을 관리해야한다는 부담이 있습니다.</p>
<p><strong>3. 디자인 시스템에 tailwindCSS 라이브러리 설치</strong>
➡️ 디자인 시스템이 자체적으로 Tailwind를 기반으로 스타일을 빌드할 수 있어 프로젝트 환경에 의존하지 않아도 된다는 장점이 있지만, 프로젝트에서 사용할 때 결국 Tailwind 라이브러리가 중복으로 로드된다는 점, 스타일이 충돌될 수 있다는 점에서 위험 부담이 있습니다.</p>
<br />

<h2 id="마무리">마무리</h2>
<p>쉽게 해결할 수 있는 문제인 줄 알았는데 꼬박 하루 걸려 원인을 발견할 수 있었습니다 😅 지금 와서 다시 돌이켜보면 처음부터 tailwind의 빌드 구조와 스캔 방식 같은 근본적인 원리를 짚고 들어갔다면 훨씬 빠르게 해결할 수 있지 않았을까 하는 생각이 들었습니다. 그래도 덕분에 평소 당연하게 사용하던 tailwind 가 어떤 식으로 동작하는지 깊에 이해할 수 있었던 좋은 경험이었습니다.</p>
<p>만약 저처럼 모노레포에서 tailwind기반의 디자인 시스템을 구축하며 비슷한 문제를 겪은 분이 있다면 경험이나 해결 과정을 함께 공유해봐요🙌 🏻</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[클라이언트 컴포넌트에서 서버 컴포넌트를 왜 import 할 수 없을까?]]></title>
            <link>https://velog.io/@ahn-sujin/%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%97%90%EC%84%9C-%EC%84%9C%EB%B2%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%A5%BC-%EC%99%9C-import-%ED%95%A0-%EC%88%98-%EC%97%86%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@ahn-sujin/%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%97%90%EC%84%9C-%EC%84%9C%EB%B2%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%A5%BC-%EC%99%9C-import-%ED%95%A0-%EC%88%98-%EC%97%86%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Thu, 09 Oct 2025 10:26:27 GMT</pubDate>
            <description><![CDATA[<p>Next.js 15 (App Router 기준)에서는 RSC(React Server Components) 아키텍처가 기본적으로 적용되어 있습니다. 이 환경에서는 컴포넌트를 서버에서 실행할지, 클라이언트에서 실행할지 구분해야 하죠!</p>
<p>그런데 만약, 클라이언트 컴포넌트안에서 서버 컴포넌트를 import 하면, 다음과 같은 에러가 발생합니다.</p>
<img src="https://github-production-user-asset-6210df.s3.amazonaws.com/67556491/466526159-d93259e4-9d1e-40bc-b4ef-52b2f56ffa97.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20251009%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251009T092825Z&X-Amz-Expires=300&X-Amz-Signature=785fc0048ec57a20c8b46fdf79e0bd631c7bfd19d397d32c2f719aeee310a5ee&X-Amz-SignedHeaders=host" />

<p>평소처럼 개발하던 중 문득, <em>&quot;왜 클라이언트 컴포넌트안에서는 서버 컴포넌트를 import 할 수 없는걸까?&quot;</em> 하는 의문이 생겼고 그 이유에 대해 알아봤습니다.</p>
<hr>
<h2 id="🤔-원인-및-이유">🤔 원인 및 이유</h2>
<h3 id="react-server-components의-렌더링-구조">React Server Components의 렌더링 구조</h3>
<p>React의 RSC 아키텍처는 <strong>단방향 렌더링</strong> 을 따릅니다. </p>
<h4 id="🔄-렌더링-순서">🔄 렌더링 순서</h4>
<p><strong>1. 서버 단계</strong>
    •    React가 서버 컴포넌트를 렌더링
    •    RSC payload 생성 (직렬화된 컴포넌트 트리)
    •    Next.js가 HTML과 함께 클라이언트로 전송</p>
<p><strong>2. 클라이언트 단계</strong>
    •    브라우저에서 HTML 표시
    •    React가 hydration 및 reconciliation(재조정) 수행
    •    클라이언트 컴포넌트 활성화</p>
<p>React는 화면을 만들 때 서버에서 먼저 컴포넌트를 실행하고, 그 다음에 클라이언트에서 이어서 처리하는 구조를 가지고 있습니다.
즉, 클라이언트 컴포넌트는 <strong>서버가 미리 만들어준 결과만 사용</strong>할 수 있고, <strong>서버에서만 실행 가능한 컴포넌트 함수는 직접 실행할 수 없습니다.</strong></p>
<h3 id="실행-환경-차이">실행 환경 차이</h3>
<table>
<thead>
<tr>
<th></th>
<th>서버 컴포넌트</th>
<th>클라이언트 컴포넌트</th>
</tr>
</thead>
<tbody><tr>
<td><strong>실행 위치</strong></td>
<td>Node.js (서버)</td>
<td>브라우저 (클라이언트)</td>
</tr>
<tr>
<td><strong>접근 가능 API</strong></td>
<td>DB, 파일 시스템, 서버 API</td>
<td>DOM, Window, LocalStorage</td>
</tr>
<tr>
<td><strong>빌드 대상</strong></td>
<td>서버 전용 번들</td>
<td>브라우저용 JS 번들</td>
</tr>
<tr>
<td><strong>렌더링 시점</strong></td>
<td>요청 시</td>
<td>hydration 이후</td>
</tr>
</tbody></table>
<p>클라이언트 컴포넌트가 서버 컴포넌트를 import한다는 건
<strong>브라우저 코드 안에서 Node.js 전용 코드를 실행하려는 시도가 되기 때문에</strong>, Next.js가 이를 감지하고 에러가 발생합니다.</p>
<blockquote>
<p><strong>브라우저 코드 안에서 Node.js 코드가 실행되지 않는 이유는?</strong>
➡️ 브라우저와 Node.js는 사용할 수 있는 API가 다르기 떄문입니다. 브라우저에는 파일 시스템, 데이터베이스, Node 전용 모듈이 없어서 서버 코드를 실행할 수 없어요!</p>
</blockquote>
<h3 id="컴포넌트-트리-구조의-제약">컴포넌트 트리 구조의 제약</h3>
<p>React는 다음과 같은 구조로 컴포넌트 트리를 렌더링합니다.</p>
<pre><code class="language-jsx">React.createElement(ClientWrapper, {}, React.createElement(ServerContent))</code></pre>
<p>이 코드의 의미는 <strong>“ServerContent를 먼저 렌더링한 결과를 ClientWrapper의 children으로 전달한다”</strong> 입니다.
즉, 서버 컴포넌트는 이미 서버 단계에서 렌더링을 끝낸 후, 그 결과 (JSX Element)가 클라이언트 컴포넌트에 children으로 전달됩니다.
이때 중요한 점은 클라이언트 컴포넌트는 <strong>&quot;컴포넌트 함수&quot;</strong> 가 아니라, <strong>&quot;렌더링된 결과(React Element)&quot;</strong>를 받는다는 것입니다.</p>
<br />

<h2 id="💡-올바른-컴포넌트-전달-과정">💡 올바른 컴포넌트 전달 과정</h2>
<h3 id="1단계-서버에서-먼저-servercontent-렌더링">1단계: 서버에서 먼저 ServerContent 렌더링</h3>
<pre><code class="language-jsx">// 서버에서 ServerContent가 실행되어 JSX 결과물 생성
const serverResult = {
  type: &#39;div&#39;,
  props: {
    children: &#39;Hello from server!&#39;
  }</code></pre>
<p>서버 컴포넌트는 서버에서 먼저 실행되어 JSX 결과물을 생성합니다.</p>
<h3 id="2단계-렌더링-결과를-rsc-payload로-직렬화">2단계: 렌더링 결과를 RSC payload로 직렬화</h3>
<pre><code class="language-jsx">{
  &quot;type&quot;: &quot;div&quot;,
  &quot;props&quot;: {
    &quot;children&quot;: &quot;Hello from server!&quot;
  }
}</code></pre>
<p>서버에서 렌더링된 결과는 RSC payload로 직렬화됩니다.</p>
<h3 id="3단계-클라이언트로-전달">3단계: 클라이언트로 전달</h3>
<pre><code class="language-jsx">// 클라이언트에서는 이미 렌더링된 결과를 받음

function ClientWrapper({ children }) {
// children은 서버 컴포넌트가 아니라 렌더링된 React 엘리먼트
  console.log(children);
  // { type: &#39;div&#39;, props: { children: &#39;Hello from server!&#39; } }

  return &lt;div&gt;{children}&lt;/div&gt;
}
</code></pre>
<p>클라이언트 컴포넌트에서는 이미 렌더링된 React Element를 받습니다. 즉, 서버에서 만들어진 결과물을 그대로 표시합니다.</p>
<br />

<h2 id="✨-마무리">✨ 마무리</h2>
<p>서버 컴포넌트를 클라이언트로 전달할 때 가장 중요한 점은 <strong>클라이언트는 결과물만 받을 수 있다</strong> 는 것입니다.</p>
<h3 id="❌-직접-import-불가능">❌ 직접 import (불가능)</h3>
<pre><code class="language-jsx">&#39;use client&#39;

import ServerComponent from &#39;./ServerComponent&#39;
// 컴포넌트 함수 자체를 가져오려고 함

export default function ClientComponent() {
  return &lt;ServerComponent /&gt;
  // 클라이언트에서 서버 컴포넌트 함수를 실행하려고 함
}
</code></pre>
<p>클라이언트에서 서버 컴포넌트 함수를 실행하려고 하면, 브라우저가 Node.js 전용 코드를 실행하려는 시도이기 때문에 에러가 발생합니다.</p>
<h3 id="✅-props로-전달-가능">✅ Props로 전달 (가능)</h3>
<pre><code class="language-jsx">// 서버에서
&lt;ClientWrapper&gt;
  &lt;ServerContent /&gt; // 서버에서 실행되어 결과물 생성
&lt;/ClientWrapper&gt;


// 클라이언트에서
&#39;use client&#39;
export default function ClientWrapper({ children }) {
// children은 이미 렌더링된 결과물 (React 엘리먼트)
  return &lt;div&gt;{children}&lt;/div&gt;
}</code></pre>
<p>서버에서 먼저 컴포넌트를 실행하여 렌더링 결과(JSX)를 만들고, 이를 클라이언트 컴포넌트의 props(children)로 전달하면 안전하게 사용할 수 있습니다.</p>
<p>쉽게 비유하자면, <strong>직접 import하는 것은 고객이 주방에 들어가 직접 요리</strong> 하려고 하는 것이고, <strong>props로 전달하는 것은 주방에서 요리를 완성한 후 완성된 음식을 고객에서 가져다 주는 것</strong> 입니다. 즉, 클라이언트 컴포넌트는 &quot;완성된 음식&quot;(렌더링 결과) 은 받을 수 있지만, &quot;레시피&quot;(서버 컴포넌트 함수) 로 직접 요리할 수는 없습니다!</p>
<br />


<blockquote>
<p>평소에는 당연하게 사용했던 개념에 대해서 자세히 알아보고 정리해보면서 Next.js에 대해서 좀 더 깊이 알게된 것 같아요! 새로운 내용을 배우는 것도 중요하지만 내가 알고 있는 내용을 정확하고 깊게 이해하고 넘어가는 것도 중요하다는 것을 다시 한번 깨달았습니다...ㅎㅎ
기본을 알면 나중에 스스로 응용도 할 수 있을 거라고 생각합니다 🙌</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Safari에서만 클립보드 복사를 실패하는 이유]]></title>
            <link>https://velog.io/@ahn-sujin/Safari%EC%97%90%EC%84%9C%EB%A7%8C-%ED%84%B0%EC%A1%8C%EB%8D%98-%EA%B3%B5%EC%9C%A0%ED%95%98%EA%B8%B0-%EB%B2%84%EA%B7%B8-%EC%82%BD%EC%A7%88%EA%B8%B0</link>
            <guid>https://velog.io/@ahn-sujin/Safari%EC%97%90%EC%84%9C%EB%A7%8C-%ED%84%B0%EC%A1%8C%EB%8D%98-%EA%B3%B5%EC%9C%A0%ED%95%98%EA%B8%B0-%EB%B2%84%EA%B7%B8-%EC%82%BD%EC%A7%88%EA%B8%B0</guid>
            <pubDate>Tue, 26 Aug 2025 13:25:09 GMT</pubDate>
            <description><![CDATA[<p>회사에서 공유하기 기능을 구현하던 중, Safari 버그를 만났습니다. 크롬이나 다른 브라우저에서는 멀쩡하게 잘 되는데, Safari만 클릭 첫 번째 시도에서 “실패했습니다”라는 alert이 뜨고, 두 번째 클릭부터는 또 멀쩡하게 잘 복사가 되는 이상한 상황이었습니다.</p>
<p>이때부터 저의 삽질은 시작됐습니다 😇</p>
<hr>
<h2 id="문제-상황">문제 상황</h2>
<p>제가 구현한 기능은 공유하기 버튼을 누르면 <strong>shortURL을 발급받고, 그걸 클립보드에 복사</strong>하는 단순한 기능이었습니다.</p>
<pre><code class="language-tsx">await navigator.clipboard.writeText(text); // 🚨 Safari 첫 클릭에서만 에러!
</code></pre>
<p>혹시 몰라 fallback으로 <code>document.execCommand(&#39;copy&#39;)</code> 도 넣어두긴 했는데, Safari에서는 첫 클릭에서만 writeText 부분이 터지면서 에러 alert이 뜨는 상황이었습니다.</p>
<p>조금 더 전체 흐름을 보자면 이런 식이었습니다.</p>
<pre><code class="language-tsx">
const { data: shortUrl, isLoading } = useGetShortUrl(currentId); // SWR shortURL API 통신

const shareItem = (id: number) =&gt; {
  if (currentId === id &amp;&amp; shortUrl?.url &amp;&amp; !isLoading) {
    handleShare(shortUrl.url);
    return;
  }
  setCurrentId(id);
};

const handleShare = (url: string) =&gt; {
  const shareData = { title: &#39;공유하기 테스트&#39;, url };
  copyToClipboard(shareData.url); // 클립보드 복사
};
</code></pre>
<p>겉보기에는 큰 문제가 없어 보였지만, Safari에서는 첫 클릭시 에러가 발생했습니다.</p>
<br />

<h2 id="삽질-과정">삽질 과정</h2>
<h3 id="1-swr-대신-직접-호출해보기">1. SWR 대신 직접 호출해보기</h3>
<p>처음에는 <em>“혹시 SWR 캐싱 때문에 응답이 늦게 들어와서 그런가?”</em> 싶어서 아예 SWR을 걷어내고, 직접 API를 호출해서 응답을 받은 뒤 클립보드에 넣어봤습니다.</p>
<pre><code class="language-tsx">const handleShareClick = async () =&gt; {
  try {
    // ✅ API 직접 호출
    const res = await fetch(`/api/short-url?postId=${postId}`);
    const data = await res.json();

    // ✅ 응답 후 바로 clipboard 실행
    await navigator.clipboard.writeText(data.url);
    alert(&#39;공유 링크가 복사되었습니다&#39;);
  } catch (err) {
    alert(&#39;복사 실패&#39;);
  }
};</code></pre>
<p>하지만 결과는 똑같이 실패했습니다. 즉, SWR 문제는 아니었습니다.</p>
<h3 id="2-동기-방식으로-강제-실행해보기">2. 동기 방식으로 강제 실행해보기</h3>
<p>Clipboard API가 async 함수라 혹시 Safari가 <em>“비동기라서 제스처 컨텍스트를 잃었다”</em> 고 생각하나 싶어서,
클립보드 복사 부분을 동기 fallback 코드(execCommand)로 강제 실행해봤습니다.</p>
<pre><code class="language-tsx">const copyToClipboard = (text: string) =&gt; {
  if (!text) return;

  try {
    const textarea = document.createElement(&#39;textarea&#39;);
    textarea.value = text;

    textarea.style.position = &#39;absolute&#39;;
    textarea.style.left = &#39;-9999px&#39;;
    document.body.appendChild(textarea);
    textarea.select();

    // ✅ 동기 방식으로 강제 실행
    document.execCommand(&#39;copy&#39;);
    document.body.removeChild(textarea);

    alert(&#39;공유 링크가 복사되었습니다&#39;);
  } catch (err) {
    alert(&#39;복사 실패&#39;);
  }
};
</code></pre>
<p>하지만 이 방식도 첫 클릭 실패는 여전했습니다. 단순히 비동기를 동기로 바꾼다고 해결되는 문제가 아니었습니다.</p>
<h3 id="3-클릭-안에서-끝까지-처리해보기">3. 클릭 안에서 끝까지 처리해보기</h3>
<p>마지막으로, <code>사용자 클릭 → API 호출 → 응답 → clipboard 실행</code> 이 흐름이 문제라면, 클릭 이벤트 안에서 끝까지 실행하면 괜찮지 않을까 싶었습니다.</p>
<pre><code class="language-tsx">const handleShareClick = () =&gt; {
  fetch(`/api/short-url?postId=${postId}`)
    .then(res =&gt; res.json())
    .then(data =&gt; {
      return navigator.clipboard.writeText(data.url);
    })
    .then(() =&gt; {
      alert(&#39;공유 링크가 복사되었습니다&#39;);
    })
    .catch(() =&gt; {
      alert(&#39;복사 실패&#39;);
    });
};;
</code></pre>
<p>하지만 역시 결과는 동일했습니다. Safari 입장에서는 API 호출 자체가 비동기이기 때문에, 이 방법 또한 이미 사용자 제스처 컨텍스트가 사라졌다고 판단한 것이었습니다.</p>
<br />

<h2 id="진짜-원인">진짜 원인</h2>
<p>Safari의 보안 정책상 <code>navigator.clipboard.writeText()</code> 는 다음 조건에서만 허용됩니다.</p>
<blockquote>
<ol>
<li>직접적인 사용자 제스처(클릭/터치) 안에서 호출</li>
<li>동기적 호출 스택 안에서 실행</li>
<li>한 번이라도 await, setTimeout, API 호출 같은 비동기 작업을 거치면 Safari는 “이건 더 이상 사용자 제스처랑 무관하다” 라고 판단 → 복사 거부</li>
</ol>
</blockquote>
<p>즉, 제가 만든 로직은 shortURL을 발급받기 위해 비동기 API 호출을 한 뒤 그 결과를 클립보드에 넣었기 때문에, Safari는 <em>“이건 사용자가 누른 게 아니라 스크립트가 실행한 거야”</em> 라고 보고 첫 클릭을 막아버린 겁니다.</p>
<p>두 번째부터는 shortURL이 이미 캐시/준비되어 있어서 곧바로 실행되니 정상 동작한 것이었고요.</p>
<br />

<h2 id="해결-방법">해결 방법</h2>
<p>저는 결국 <strong>Safari 전용 바텀시트</strong> 를 만들어서 문제를 해결했습니다. 진행 플로우는 이렇게 바뀌었습니다.</p>
<blockquote>
<ol>
<li>공유하기 버튼 클릭 → 바텀시트 열림</li>
<li>바텀시트가 열리면 shortURL을 API로 미리 요청</li>
<li>사용자가 바텀시트에서 링크 복사하기 버튼을 누름</li>
<li>이때 곧바로 <code>navigator.clipboard.writeText()</code> 실행 (사용자 제스처 안에서 호출)</li>
</ol>
</blockquote>
<pre><code class="language-tsx">// 클릭 시 모달 열고 shortURL 미리 요청
const onShareClick = async (itemId: number) =&gt; {
  setShowModal(true);
  setLoading(true);

  try {
    const res = await fetch(`/api/short-url?itemId=${itemId}`);
    const json = await res.json();
    setShortUrl(json.url); // shortURL 준비
  } finally {
    setLoading(false);
  }
};

// 바텀시트에서 &quot;복사하기&quot; 클릭 시
const onCopyClick = () =&gt; {
  if (!shortUrl) {
    alert(&#39;링크 준비 중입니다.&#39;);
    return;
  }

  navigator.clipboard.writeText(shortUrl)
    .then(() =&gt; alert(&#39;링크가 복사되었습니다&#39;))
    .catch(() =&gt; alert(&#39;복사 실패&#39;));
};

</code></pre>
<p>Safari에서는 <strong>클릭 이벤트 핸들러 안에서 즉시 실행</strong> 되는 코드만 허용하기 때문에, onCopyClick 안에서 바로 <code>writeText()</code> 를 호출하는 구조로 바꿔서 버그를 해결할 수 있었습니다.</p>
<br />

<h2 id="마무리">마무리</h2>
<p>결론적으로, Safari에서 발생한 이 버그는 코드 문제가 아니라 브라우저 정책 때문이었습니다.
생각보다 해결책은 단순했습니다. 비동기 작업으로 URL을 받아온 뒤 곧바로 복사하는 게 아니라, <strong>사용자 클릭 시점에서 즉시 복사 동작을 실행</strong> 하도록 구조를 바꾸는 것! </p>
<p>결국 문제의 핵심은 “언제 클립보드 복사 동작을 실행하느냐”였습니다. 비동기 API 호출과 사용자 제스처 사이의 타이밍만 맞춰주면 Safari도 더 이상 문제를 일으키지 않습니다. 삽질은 길었지만, 덕분에 브라우저 호환성에 대해 깊이 이해할 수 있었습니다.😇</p>
<br />

<blockquote>
<p>📚 참고
<a href="https://www.wnsdufdl.com/post/D6faOWUCQqWwxT-1CG61sQ">류쥰열의 기술 블로그 | 사파리에서 클립보드 복사 이슈</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[다시만난 CORS 에러 : pre-flight]]></title>
            <link>https://velog.io/@ahn-sujin/%EB%8B%A4%EC%8B%9C%EB%A7%8C%EB%82%9C-CORS-%EC%97%90%EB%9F%AC-pre-flight</link>
            <guid>https://velog.io/@ahn-sujin/%EB%8B%A4%EC%8B%9C%EB%A7%8C%EB%82%9C-CORS-%EC%97%90%EB%9F%AC-pre-flight</guid>
            <pubDate>Tue, 29 Jul 2025 14:36:15 GMT</pubDate>
            <description><![CDATA[<p>회사에서 AWS CloundFront + S3 를 통해 m3u8 스트리밍 비디오 파일을 불러오는 와중에 CORS 에러가 발생하여 파일을 제대로 불러오지 못하는 일이 발생했습니다. 무엇이 문제였고 어떻게 해결했는지 그 과정에 대해서 정리해보려고 합니다.</p>
<hr>
<h2 id="문제-상황">문제 상황</h2>
<p>회사에서 개발 중인 서비스에서 HLS(HTTP Live Streaming) 방식으로 비디오를 재생하는 기능을 구현하고 있었습니다. 백엔드에서는 AWS S3에 업로드된 m3u8 파일과 ts 세그먼트들을 CloudFront를 통해 배포하고 있었고, 프론트엔드에서는 video.js 라이브러리를 사용해 이를 재생하려고 했습니다.</p>
<p>(이해를 돕기위한 코드로 참고용으로 봐주세요 🙇‍♀️)</p>
<pre><code class="language-tsx">
import { useRef, useEffect } from &#39;react&#39;;
import videojs from &#39;video.js&#39;;
import &#39;video.js/dist/video-js.css&#39;;

interface VideoPlayerProps {
  src: string;
  options?: any;
}

export const VideoPlayer: React.FC&lt;VideoPlayerProps&gt; = ({ src, options }) =&gt; {
  const videoRef = useRef&lt;HTMLVideoElement&gt;(null);
  const playerRef = useRef&lt;any&gt;(null);

  useEffect(() =&gt; {
    if (videoRef.current) {
      playerRef.current = videojs(videoRef.current, {
        ...options,
        sources: [{
          src: src, // CloudFront URL: https://d1234567890.cloudfront.net/videos/sample.m3u8
          type: &#39;application/x-mpegURL&#39;
        }]
      });
    }

    return () =&gt; {
      if (playerRef.current) {
        playerRef.current.dispose();
      }
    };
  }, [src, options]);

  return (
    &lt;video
      ref={videoRef}
      className=&quot;video-js vjs-default-skin&quot;
      controls
      preload=&quot;auto&quot;
      data-setup=&quot;{}&quot;
    /&gt;
  );
};

</code></pre>
<p>그런데 개발 환경에서 테스트해보니 CORS 에러가 발생해 동영상을 제대로 불러오지 못하고 있었습니다.   </p>
<p>처음에는 <em>&quot;단순히 m3u8 파일을 가져오는 건데 왜 CORS 에러가 나지?&quot;</em> 라고 생각했습니다. 이후 원인에 대해 구글링을 해보다 Simple Request 와 Complex Request의 차이가 있다는 것을 알게 되었습니다. </p>
<br />

<h2 id="simple-request의-조건">Simple Request의 조건</h2>
<p>CORS에서 Simple Request는 preflight 없이 <strong>바로 서버로 전송되는 요청</strong>으로, 조건이 생각보다 까다로웠습니다.</p>
<p>Simple Request가 되려면 다음 조건을 모두 만족해야 합니다.</p>
<h4 id="1-http-메서드">1. HTTP 메서드</h4>
<ul>
<li>GET</li>
<li>POST</li>
</ul>
<h4 id="2-허용되는-헤더만-사용">2. 허용되는 헤더만 사용</h4>
<p>자동으로 설정되는 헤더 (User-Agent, Accept 등) 외에 수동으로 설정할 수 있는 헤더는 다음과 같이 제한됩니다.</p>
<ul>
<li>Accept</li>
<li>Accept-Language</li>
<li>Content-Language</li>
<li>Content-Type (단, 특정 값으로 제한)</li>
<li><strong>Range ( ⬅️ 여기서 문제 발생! )</strong></li>
</ul>
<br />

<h2 id="complex-request의-조건">Complex Request의 조건</h2>
<p>Simple Request 조건을 하나라도 벗어나면 Complex Request(복합 요청)가 됩니다.</p>
<h4 id="1-range-헤더-사용">1. Range 헤더 사용</h4>
<p>비디오 스트리밍에서는 브라우저가 자동으로 <code>Range</code> 헤더를 추가합니다. 이는 <strong>비디오 파일의 특정 부분만 요청</strong> 하기 위한 헤더입니다.</p>
<pre><code class="language-http">
GET /videos/sample.m3u8 HTTP/1.1
Host: d1234567890.cloudfront.net
Range: bytes=0-1023
Origin: http://localhost:3000
</code></pre>
<h4 id="2-커스텀-헤더들">2. 커스텀 헤더들</h4>
<p>video.js나 <strong>HLS 플레이어(내가 사용한 것)</strong> 들은 종종 다음과 같은 헤더들을 추가로 사용합니다.</p>
<ul>
<li><code>X-Requested-With</code></li>
<li><code>X-Playback-Session-Id</code></li>
<li>기타 플레이어별 커스텀 헤더들</li>
</ul>
<p>이런 헤더들이 하나라도 있으면 Complex Request가 되어 <strong>preflight가 필요</strong> 합니다.</p>
<br />


<h2 id="pre-flight-란">Pre-flight 란?</h2>
<p>pre-flight는 브라우저가 실제 요청을 보내기전에 <strong>이런 요청을 보내도 괜찮은지</strong> 서버에게 미리 확인하는 과정을 말합니다.</p>
<h4 id="pre-flight-요청-과정">pre-flight 요청 과정</h4>
<ol>
<li>브라우저가 OPTIONS 요청 발송<pre><code class="language-http">OPTIONS /videos/sample.m3u8 HTTP/1.1
Host: d1234567890.cloudfront.net
Origin: http://localhost:3000
Access-Control-Request-Method: GET
Access-Control-Request-Headers: range
</code></pre>
</li>
</ol>
<pre><code>
2. 서버가 허용 정보 응답
```http
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, HEAD, OPTIONS
Access-Control-Allow-Headers: range, content-type
Access-Control-Max-Age: 86400
</code></pre><ol start="3">
<li>브라우저가 실제 요청 발송<pre><code class="language-http">GET /videos/sample.m3u8 HTTP/1.1
Host: d1234567890.cloudfront.net
Origin: http://localhost:3000
Range: bytes=0-1023
</code></pre>
</li>
</ol>
<pre><code>
만약 preflight 단계에서 서버가 적절한 CORS 헤더를 반환하지 않으면, 브라우저는 실제 요청을 보내지 않고 CORS 에러를 발생시키게 되는 것입니다.


&lt;br /&gt;


## 스트리밍 Video 요청시 CORS 에러가 발생했던 이유

#### 1. Range 헤더로 인한 Complex Request
비디오 스트리밍에서는 브라우저가 자동으로 `Range` 헤더를 사용합니다. 이는 **대용량 비디오 파일을 청크 단위로 나눠 받기 위함**인데 이 때문에 simple Request 조건을 벗어나 pre-flight 가 필요하게 됩니다.

```http
# 첫 1MB 요청 (초기 로딩)
Range: bytes=0-1048575

# 다음 1MB 요청 (스트리밍 계속)
Range: bytes=1048576-2097151

# 사용자가 건너뛰기 한 경우
Range: bytes=5242880-6291455</code></pre><p>Range 헤더의 장점은 다음과 같습니다.</p>
<ul>
<li><strong>빠른 초기 로딩</strong>: 전체 파일이 아닌 필요한 부분만 먼저 다운로드</li>
<li><strong>대역폭 절약</strong>: 시청하지 않는 부분은 다운로드하지 않음</li>
<li><strong>사용자 경험 향상</strong>: 즉시 재생 시작, 구간 이동 가능</li>
</ul>
<h4 id="2-cloudfront의-cors-정책-미설정">2. CloudFront의 CORS 정책 미설정</h4>
<p>비디오 스트리밍은 Range 헤더 사용으로 인해 Complex Request가 되며, 이에 따라 브라우저는 사전에 preflight 요청(OPTIONS)을 전송하게 됩니다.</p>
<p>하지만 <strong>CloudFront에서 별도의 응답 헤더 정책(Response Headers Policy) 을 설정하지 않으면</strong>, 이러한 preflight 요청에 대해 적절한 CORS 허용 헤더를 포함하지 않게 됩니다.</p>
<p>즉, 브라우저는 CloudFront에서 오는 응답에 <code>Access-Control-Allow-Origin</code>, <code>Access-Control-Allow-Headers</code>, <code>Access-Control-Allow-Methods</code> 같은 CORS 관련 헤더가 포함되지 않은 것을 확인하고, CORS 에러를 발생시킵니다.</p>
<br />

<h2 id="해결-방법">해결 방법</h2>
<p>해결 방법은 간단했습니다. CloudFront 콘솔에서 응답 헤더 정책을 설정해줌으로써 브라우저가 요청을 정상적으로 처리할 수 있게 만들었습니다.
CloudFront에서 다음과 같은 설정을 통해 문제를 해결할 수 있습니다.</p>
<h4 id="1-cloudfront-콘솔--배포distribution--동작behaviors-로-이동">1. CloudFront 콘솔 &gt; 배포(Distribution) &gt; 동작(Behaviors) 로 이동</h4>
<h4 id="2-해당-동작을-선택-후-편집edit">2. 해당 동작을 선택 후 편집(Edit)</h4>
<h4 id="3-응답-헤더-정책-변경">3. 응답 헤더 정책 변경</h4>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/cfa70545-f8e6-4bd4-9c0a-91ec4259f221/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/7eedf1e2-7229-4688-8c56-1d49a7023892/image.png" alt=""></p>
<p>설정이 완료되면 브라우저가 OPTIONS 요청에 대해 유효한 응답을 받게 되어 CORS 에러 없이 정상적으로 스트리밍 요청이 처리됩니다.</p>
<br />

<blockquote>
<p>📚 <strong>참고</strong></p>
</blockquote>
<ul>
<li><a href="https://repost.aws/pt/questions/QUOa_kv0YrRwKEk38TwplHvA/s-3-%EC%97%90-%EC%97%85%EB%A1%9C%EB%93%9C%EB%90%9C-m-3-u-8-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EA%B0%80%EC%A0%B8%EC%98%AC%EB%95%8C-cors-%EC%98%A4%EB%A5%98%EA%B0%80-%EB%B0%9C%EC%83%9D%ED%95%A9%EB%8B%88%EB%8B%A4">s3에 업로드된 m3u8 파일을 가져올때 cors 오류가 발생합니다</a></li>
<li><a href="https://repost.aws/ko/knowledge-center/no-access-control-allow-origin-error">Cloudfront에서 “요청된 리소스에 대한 &#39;액세스 제어-허용-오리진&#39; 헤더가 없습니다” 오류를 해결하려면 어떻게 해야 하나요?</a></li>
<li><a href="https://velog.io/@exceed96/Front-Preflight">Preflight</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[router.refresh() 와 window.location.reload()의 차이점에 대해서 (with 패럴렐 라우트)]]></title>
            <link>https://velog.io/@ahn-sujin/router.refresh-%EC%99%80-window.location.reload%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90%EC%9D%80</link>
            <guid>https://velog.io/@ahn-sujin/router.refresh-%EC%99%80-window.location.reload%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90%EC%9D%80</guid>
            <pubDate>Sun, 22 Jun 2025 11:46:18 GMT</pubDate>
            <description><![CDATA[<p>최근 Next.js 패럴렐 라우트를 사용해서 모달을 만드는 작업을 하던 중 궁금한 점이 생기게 되었습니다. 패럴렐 라우트로 만든 모달을 닫아야할 때 <code>router.refresh()</code> 와 <code>window.location.reload()</code> 중 어떤 것을 사용하는 것이 적절한지에 대한 것이었습니다.</p>
<p>제가 경험한 바로는 <code>window.location.reload()</code> 를 사용하면 의도한 대로 모든 모달이 닫혔지만, <code>router.refresh()</code> 를 사용하면 URL은 변경되었음에도 불구하고 화면 상의 모달은 여전히 남아있었습니다.</p>
<p>그래서 과연 두개의 차이는 무엇이면 각각 어떤 상황에서 사용해야하는지에 대해 알아보도록 하겠습니다.</p>
<hr>
<h2 id="windowlocationreload의-동작-원리">window.location.reload()의 동작 원리</h2>
<p><code>window.location.reload()</code> 는 브라우저 네이티브 API로 <strong>전체 페이지를 완전히 새로고침</strong>하는 역할을 합니다. 이 메서드가 호출되면 다음과 같은 과정이 진행됩니다.</p>
<pre><code class="language-tsx">const handleHardReload = () =&gt; {
  window.location.reload();
};
</code></pre>
<ol>
<li><strong>브라우저는 서버로부터 현재 페이지의 HTML을 다시 요청</strong>하고 받아옵니다. 이는 처음 페이지에 접속할 때와 동일한 과정입니다.</li>
<li><strong>모든 Javascript 상태가 완전히 초기화</strong>됩니다. 이는 React 컴포넌트 상태, 전역 상태, 그리고 메모리에 저장된 모든 변수들이 초기값으로 돌아가는 것을 의미합니다.</li>
<li><strong>캐시된 데이터가 모두 지워집니다</strong>. 브라우저는 임시로 저장해둔 데이터들이 모두 삭제하고 초기 상태에서 다시 시작하게됩니다.</li>
<li>마지막으로, 컴포넌트가 완전히 새로 구성되고 기존에 렌더링된 모든 컴포넌트들이 언마운트 되며 <strong>새로운 컴포넌트들이 다시 마운트</strong>됩니다.</li>
</ol>
<p>이러한 특성 때문에 <code>window.location.reload()</code> 는 <strong>완전한 초기화</strong>가 필요한 상황에서 확실한 해결책이 되지만, 사용자 경험 측면에서는 <strong>느리고 불편</strong>할 수 있습니다.</p>
<br />

<h2 id="routerrefresh의-동작-원리">router.refresh()의 동작 원리</h2>
<p><code>router.refresh()</code> 는 <strong>Next.js의 App Router</strong>에서 제공하는 메서드로, <strong>소프트 내비게이션(Soft Navigation) 방식</strong>으로 동작합니다.</p>
<blockquote>
<ul>
<li><strong>Hard Navigation</strong>: 브라우저가 완전히 새로운 페이지를 로드 (전체 새로고침)</li>
</ul>
</blockquote>
<ul>
<li><strong>Soft Navigation</strong>: JavaScript로 필요한 컴포넌트만 교체 (부분 업데이트)</li>
</ul>
<pre><code class="language-tsx">&#39;use client&#39;;
import { useRouter } from &#39;next/navigation&#39;;

const RefreshButton = () =&gt; {
  const router = useRouter();

  const handleSoftRefresh = () =&gt; {
    router.refresh();
  };

  return (
    &lt;button onClick={handleSoftRefresh}&gt;
      데이터 새로고침
    &lt;/button&gt;
  );
};
</code></pre>
<p><code>router.refresh()</code> 가 호출되면, <strong>Next.js는 현재 경로에 해당하는 서버 컴포넌트의 데이터만 다시 가져옵니다.</strong> 이때 중요한 점은 <strong>페이지의 구조나 레이아웃은 그대로 유지</strong>한다는 것입니다. 이는 사용자가 입력한 폼 데이터나 스크롤 위치 같은 상태들이 유지된다는 것을 의미합니다.</p>
<p>또한, <strong>클라이언트 상태도 보존</strong>됩니다. useState나 Zustand 같은 상태 관리 라이브러리의 상태들이 그대로 유지되어, 사용자가 설정한 값들이 사라지지 않습니다.</p>
<p>가장 중요한 특징은 <strong>라우팅 구조와 슬롯이 변경되지 않는다는</strong> 점입니다. 특히 패럴렐 라우트의 슬롯들은 기존 상태를 그대로 유지하게 됩니다.</p>
<p>이러한 특성으로 인해 <code>router.refresh()</code>는 사용자 경험을 해치지 않으면서도 <strong>최신 데이터를 가져올 수 있는 효율적인 방법이지만, 완전한 초기화가 필요한 상황에서는 한계</strong>가 있습니다.</p>
<br />

<h2 id="패럴렐-라우트-모달">패럴렐 라우트 모달</h2>
<h3 id="상황">상황</h3>
<p>패럴렐 라우트를 사용하여 모달을 구현할 때는 다음과 같은 구조를 가집니다.</p>
<blockquote>
<p>이해를 돕기 위한 예시 코드로 내용 이해를 위한 참고용으로 봐주세요.</p>
</blockquote>
<pre><code class="language-tsx">// app/layout.tsx
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    &lt;html&gt;
      &lt;body&gt;
        &lt;div&gt;
          {children}
          {modal}
        &lt;/div&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}

// app/page.tsx
export default function HomePage() {
  return (
    &lt;div&gt;
      &lt;h1&gt;메인 페이지&lt;/h1&gt;
      &lt;Link href=&quot;/products/1&quot;&gt;
        상품 모달 열기
      &lt;/Link&gt;
    &lt;/div&gt;
  );
}

// app/@modal/products/[id]/page.tsx
export default function ProductModal({ params }: { params: { id: string } }) {

const handleModalClose = () =&gt; {
  window.history.pushState({}, &#39;&#39;, &#39;/&#39;);
  router.refresh();
};
  return (
    &lt;div&gt;
      &lt;div&gt;
        &lt;h2&gt;상품 {params.id} 상세&lt;/h2&gt;
        &lt;p&gt;상품에 대한 자세한 정보입니다.&lt;/p&gt;
          &lt;button onClick={handleModalClose}&gt;
            닫기
          &lt;/button&gt;

      &lt;/div&gt;
    &lt;/div&gt;
  );
}

// app/@modal/default.tsx
export default function DefaultModal() {
  return null; // 모달이 없을 때는 null 반환
}

</code></pre>
<p>위와 같은 구조에서 이런 구조에서 <code>products/1</code> 경로로 이동하면 @modal 슬롯에 의해 모달이 화면에 표시됩니다. 만약 이때, 사용자가 <strong>닫기 버튼</strong>을 클릭했을 때, URL을 /로 변경하고 <code>router.refresh()</code>를 호출해도 화면상의 모달은 여전히 남아있게 됩니다.</p>
<h3 id="원인">원인</h3>
<pre><code class="language-tsx">const handleModalClose = () =&gt; {
  // 1. URL을 &#39;/&#39;로 변경
  window.history.pushState({}, &#39;&#39;, &#39;/&#39;);
  // 2. router.refresh() 호출
  router.refresh();
};
</code></pre>
<p><code>router.refresh()</code> 를 사용했을 때 패럴렐 라우트 모달이 닫히지 않는 원인은 <strong>Next.js의 패럴렐 라우트 슬롯 관리 방식</strong> 에 있습니다.</p>
<p>패럴렐 라우트에서** @modal 슬롯은 독립적인 렌더링 단위로 취급** 됩니다. <code>router.refresh()</code> 가 호출되면 Next.js는 현재 활성화된 모든 슬롯을 기준으로 <strong>데이터를 다시 가져오지만, 슬롯 자체의 구조는 변경하지 않습니다.</strong></p>
<p>이는 Next.js가 성능 최적화를 위해 설계한 동작으로, <code>router.refresh()</code> 는 필요한 데이터만 새로 가져오고 기존 컴포넌트 구조는 최대한 보존하려고 하기 때문입니다. 구체적으로 설명하면, <strong>URL이 /로 변경되어도 Next.js는 이미 마운트된 @modal 슬롯의 컴포넌트들이 여전히 유효하다고 판단</strong>합니다. <strong><code>router.refresh()</code> 는 단순히 서버에서 새로운 데이터를 가져와서 기존 컴포넌트에 전달할 뿐</strong>, 컴포넌트 자체를 언마운트하지는 않습니다.</p>
<p>반면, <code>window.location.reload()</code> 는 브라우저 레벨에서 완전한 새로고침을 수행하므로, 모든 슬롯과 컴포넌트가 처음부터 다시 생성되어 모달이 확실히 닫히게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/7c98b91d-9d1f-4ca0-9605-82262354ac02/image.gif" alt="잘못된방식"></p>
<h3 id="해결-방안">해결 방안</h3>
<h4 id="routerpush를-사용한-명시적-경로-이동">router.push()를 사용한 명시적 경로 이동</h4>
<p>가장 권장되는 방법은 모달이 없는 경로로 명시적으로 이동하는 것입니다.</p>
<pre><code class="language-tsx">&#39;use client&#39;;
import { useRouter } from &#39;next/navigation&#39;;

const CloseModalButton = () =&gt; {
  const router = useRouter();

  const closeModal = () =&gt; {
    // 모달 슬롯이 매칭되지 않는 경로로 이동
    router.push(&#39;/&#39;);
  };

  return (
    &lt;button 
      onClick={closeModal}
      className=&quot;bg-red-500 text-white px-4 py-2 rounded&quot;
    &gt;
      모달 닫기
    &lt;/button&gt;
  );
};</code></pre>
<h4 id="routerback을-사용한-이전-페이지-이동">router.back()을 사용한 이전 페이지 이동</h4>
<p>사용자가 모달을 열기 전 페이지로 돌아가는 경우에 유용합니다.</p>
<pre><code class="language-tsx">const BackButton = () =&gt; {
  const router = useRouter();

  const goBack = () =&gt; {
    // 브라우저 히스토리에서 이전 페이지로 이동
    router.back();
  };

  return (
    &lt;button onClick={goBack} className=&quot;bg-gray-500 text-white px-4 py-2 rounded&quot;&gt;
      이전으로
    &lt;/button&gt;
  );
};</code></pre>
<p>이 밖에도 복잡한 모달 관리가 필요한 경우 <strong>zustand</strong>를 활용한 상태 관리 방식 및 <strong>인터셉팅 라우트</strong>를 활용한 방법도 고려해 볼 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/e214f33a-bf4a-4a51-aaa7-dad70759d9a0/image.gif" alt=""></p>
<br />

<h2 id="정리">정리</h2>
<p>이번 글을 작성하면서 패럴렐 라우트 모달 관리에 대한 이슈가 저뿐만 아니라 여러 개발자들이 공통적으로 겪고 있는 문제라는 것을 알게 되었습니다. 개발을 하다 보면 모달 관리는 언제나 고민거리였는데, 패럴렐 라우트가 이를 체계적이면서도 심플하게 해결해줄 수 있을 거라는 기대를 했지만 생각보다 쉽지 않더라고요 ... 😅</p>
<p>우선, 블로그를 정리하면서 느꼈던 것은 router.refresh()와 window.location.reload()의 차이점을 명확히 이해하는 것이었습니다.</p>
<p><code>router.refresh()</code> 의 특징:</p>
<ul>
<li>서버 데이터만 새로 가져오는 소프트 리프레시</li>
<li>기존의 레이아웃과 슬롯 구조를 그대로 유지</li>
<li>데이터 갱신이 목적일 때는 효율적이지만, 패럴렐 라우트 모달을 닫는 용도로는 부적합</li>
</ul>
<p><code>window.location.reload()</code> 의 특징:</p>
<ul>
<li>브라우저 전체를 새로고침하여 모든 상태와 컴포넌트를 초기화</li>
<li>모달은 확실히 닫히지만, 사용자 경험 측면에서는 권장되지 않음</li>
</ul>
<p>결론적으로, 패럴렐 라우트 모달을 올바르게 관리하려면 <strong>router.push()</strong> 나 <strong>router.back()</strong> 을 사용하여 적절한 경로로 이동하거나, 상태 관리 라이브러리를 활용한 조건부 렌더링 방식을 고려하는 것이 가장 적절한 해결책입니다.</p>
<p>패럴렐 라우트가 완벽한 해결책은 아니지만, 이러한 동작 원리와 차이점을 이해하고 상황에 맞는 적절한 방법을 선택한다면 Next.js에서 더욱 안정적이고 사용자 친화적인 모달 경험을 제공할 수 있을거라고 생각합니다~!</p>
<br />

<blockquote>
<p>📚 참고
<a href="https://velog.io/@ahn-sujin/Next.js%EC%9D%98-%EA%B3%A0%EC%98%A4%EA%B8%89-%EB%9D%BC%EC%9A%B0%ED%8C%85-%ED%8C%A8%ED%84%B4">- Next.js의 고급 라우팅 패턴</a>
<a href="https://nextjs.org/docs/app/building-your-application/routing/parallel-routes">- Next.js Parallel Routes 공식 문서</a>
<a href="https://github.com/vercel/next.js/discussions/53517">- GitHub Discussion - window.location.reload vs router.reload 차이점</a>
<a href="https://abdulmajid.hashnode.dev/creating-modals-in-nextjs-14-using-parallel-and-intercepting-routes">- Creating Modals in Next.js 14: Using Parallel and Intercepting Routes</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[OOM은 처음이라]]></title>
            <link>https://velog.io/@ahn-sujin/OOM%EC%9D%80-%EC%B2%98%EC%9D%8C%EC%9D%B4%EB%9D%BC</link>
            <guid>https://velog.io/@ahn-sujin/OOM%EC%9D%80-%EC%B2%98%EC%9D%8C%EC%9D%B4%EB%9D%BC</guid>
            <pubDate>Sun, 04 May 2025 13:43:18 GMT</pubDate>
            <description><![CDATA[<p>최근 회사에서 운영하는 서비스에서 OOM 문제가 발생했습니다. 처음 겪는 상황이라 원인을 파악하는데 시간이 걸렸고 현재도 문제를 완전히 해결하기 위해 여러 방법을 시도하고 있습니다. 이 경험을 바탕으로 OOM이란 무엇인지, 어떤 원인으로 발생하는지, 그리고 어떻게 해결할 수 있을지에 대해 정리해보려고 합니다.</p>
<hr>
<br />

<h2 id="oomout-of-memory-이란">OOM(Out of Memory) 이란?</h2>
<p>OOM 에러는 애플리케이션이 사용할 수 있는 메모리의 한계를 초과하려고 할 때 발생하는 에러입니다. 쉽게 말해, <strong>더 이상 필요하지 않은 값들이 여전히 메모리 공간을 차지하고 있어 새로운 데이터를 저장할 공간이 부족해지는 상황</strong>이라고 이해할 수 있습니다.</p>
<p>이러한 문제가 발생하게 되면, 애플리케이션이 강제로 종료되거나, 전체 시스템 성능이 심각하게 저하되는 등의 문제가 발생할 수 있습니다. 특히 서버 환경에서는 OOM이 발생하는 순간 요청을 처리하지 못해 서비스 중단으로 이어질 수 있기 때문에 주의해야합니다.</p>
<h3 id="원시-값과-참조-값의-메모리-관리-방식">원시 값과 참조 값의 메모리 관리 방식</h3>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/db4d0d94-e1ab-4060-b926-3df5e9c987af/image.png" alt=""></p>
<p>자바스크립트에서 값은 크게 원시 값과 참고 값으로 나뉩니다. 이 두 종류의 값은 메모리에 저장되는 방식이 서로 다르며, 이로 인해 메모리 관리에도 차이가 생깁니다.</p>
<p><strong>원시 값</strong>은 <code>문자열,숫자,불리언,null,undefined</code> 와 같은 값들입니다. 이들은 <strong>실행 컨텍스트</strong>가 생성되는 시점에 <strong>스택(stack)</strong> 이라는 메모리 공간에 저장되고, <strong>함수가 종료되면 자동으로 메모리에서 제거</strong> 됩니다. 이처럼 원시 값은 사용이 끝나면 자연스럽게 정리되기 때문에 별도로 메모리를 수거할 필요가 없습니다.</p>
<p>반면, <strong>참조 값</strong> 은 <code>객체,배열,함수</code> 등을 말합니다. 참조 값은 <strong>힙(Heap)</strong> 이라는 공간에 저장되며, 변수에는 해당 값의 실제 값이 아니라 그 값을 가리키는 메모리 주소가 저장됩니다. 힙은 스택과 달리 <strong>다양한 실행 컨텍스트에서 공유되기 때문에 참조 값이 여전히 다른 곳에서 사용되고 있는지 아닌지를 판단해 메모리를 해제</strong> 해야합니다. 이러한 역할을 해주는 것이 바로 <strong>가비지 컬렉션</strong> 입니다.</p>
<blockquote>
<p><strong>🤔 실행 컨텍스트 란?</strong></p>
</blockquote>
<ul>
<li>실행 컨텍스트는 <strong>자바스크립트 코드가 실행될 때 생성되는 실행 환경</strong>을 의미합니다. 즉, 자바스크립트 엔진이 코드를 해석하고 실행하는 동안의 환경을 말하며, 함수 호출이나 코드 블록이 실행될 때 새로운 실행 컨텍스트를 생성합니다.</li>
<li>실행 컨텍스트의 구성 요소는 크게 <strong><code>렉시컬 환경, 변수 객체, this</code></strong> 값이 있습니다.<ul>
<li><strong><code>렉시컬 환경</code></strong> : 변수나 함수가 어디서 선언 되었는지를 기억하고 저장합니다.</li>
<li><strong><code>변수 객체</code></strong> : 변수와 함수 선언이 포함된 객체로 함수 내에서 사용되는 변수들이 저장됩니다.</li>
<li><strong><code>this</code></strong> : 함수가 어디서 실행되었는지에 따라서 동적으로 결정됩니다.</li>
</ul>
</li>
</ul>
<h3 id="가비지-컬렉션">가비지 컬렉션</h3>
<p>가비지 컬렉션은 자바스크립트 엔진이 불필요해진 메모리를 자동으로 해제해주는 기능입니다. 자바스크립트는 메모리를 자동으로 관리하기 때문에 가비지 컬렉션은 메모리 누수를 방지하는데 중요한 역할을 합니다.</p>
<p>가비지 컬렉션은 기본적으로 <strong>도달할 수 없는 값(unreachable value)</strong> 을 찾아서 메모리에서 제거합니다. 예를 들어 어떤 객체를 참조하던 변수가 null로 설정되거나, 함수 실행이 끝나면서 클로저 내부에서 참조하던 객체들이 더 이상 사용되지 않을 경우, 해당 객체는 더 이상 도달할 수 없는 상태가 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/3b7210eb-56b9-4182-844d-60ef957ea6d7/image.gif" alt=""></p>
<p>대표적인 방식은 <strong>Mark-and-Sweep 방식</strong> 입니다. 이 방식은 먼저 전역 객체나 실행 중인 함수에서 시작해서 연결된 객체들을 모두 찾아 ‘도달 가능하다’고 표시한 뒤, 도달할 수 없는 객체들을 찾아내어 제거합니다. 하지만 모든 상황에서 완벽하게 작동하는 것은 아니며, 잘못된 참조나 불필요한 클로저 사용 등으로 인해 <strong>참조가 끊기지 않는 객체가 남아 있는 경우</strong> , 메모리 누수가 발생할 수 있습니다.</p>
<h3 id="oom이-발생하는-원인">OOM이 발생하는 원인</h3>
<p>OOM은 여러 가지 이유로 발생할 수 있지만 일반적으로 아래와 같은 원인이 있습니다.</p>
<ul>
<li><strong>사용하지 않지만 참조가 해제되지 않은 객체</strong> 가 계속해서 메모리를 차지하고 있을 때 발생할 수 있습니다. 예를 들어 클로저 내부에서 참조되고 있거나, 전역 객체에 등록된 값이 제거되지 않은 경우 등이 이에 해당합니다.</li>
<li><strong>대용량 데이터를 한꺼번에 메모리에 로드</strong> 하는 경우에 문제가 발생할 수 있습니다. 예를 들어 JSON데이터 수만 건을 메모리에 올려놓고 처리하거나, 이미지 파일 수백 개를 동시에 로딩하는 경우 OOM 위험이 높아집니다.</li>
<li><strong>무한 루프나 재귀 호출</strong> 이 발생할 경우 메모리 사용량이 급격히 증가합니다.</li>
<li>서버 환경에서는 <strong>로그나 응답 데이터를 메모리에 계속 유지</strong> 하거나, 캐시 데이터를 수동으로 정리하지 않아 누적될 경우 문제가 발생할 수 있습니다.</li>
</ul>
<br />


<h2 id="nextjs-와-oom">Next.js 와 OOM</h2>
<p>Next.js는 서버 기능을 포함하고 있기 때문에 Node.js 런타임에서 돌아가는 서버 측 코드가 존재합니다. <code>getServerSideProps, API Routes, Server Actions</code> 와 같은 기능이 그 예입니다. 따라서, Next.js에서도 서버 측에서 OOM이 발생할 수 있으며 서비스 중단으로 이어질 수 있습니다.</p>
<p>저는 최근 회사에서 Next.js 기반의 서비스에서 OOM 문제를 경험하게 되었습니다. 메모리가 초기화되지 않아 메모리 사용량이 점점 증가하다가 결국 한계치를 초과해 버리는 현상이었습니다. 실무에서 OOM 문제를 마주친건 처음이라 당황스럽기도 했지만, 우선 <strong>Next.js에서 OOM이 발생할 수 있는 원인</strong> 을 찾아봤습니다.</p>
<ul>
<li><strong>불필요하거나 과도한 데이터 캐싱</strong>
  서버에서 응답 결과를 캐시에 저장하되, 이를 해제하지 않으면 메모리가 계속 누적될 수 있습니다.</li>
<li><strong>대용량 데이터의 반복 패칭 또는 유지</strong>
  한 번에 많은 양의 데이터를 패칭하거나, 이를 서버 메모리에 오래 유지할 경우 문제가 됩니다.</li>
<li><strong>해제되지 않는 참조</strong>
  클로저나 전역 변수 등으로 인해 객체 참조가 끊기지 않으면 가비지 컬렉션이 이를 수거하지 못합니다.</li>
<li><strong>의도치 않은 무한 루프 또는 재귀 호출</strong>
  반복되는 로직이 메모리를 점유하면서 해제되지 않아 문제가 될 수 있습니다.</li>
</ul>
<h3 id="nextjs의-캐시가-문제">Next.js의 캐시가 문제?</h3>
<p>이 중에서도 가장 핵심적인 원인으로 판단한 것은 <strong>데이터 패칭 후 캐시에 저장된 데이터가 적절히 초기화되지 않는 문제</strong> 였습니다. </p>
<p>실제로 저희 서비스는 대부분의 페이지가 <strong>서버 컴포넌트</strong> 로 구성되어 있었고, 데이터를 <strong>Server Actions를 통해 데이터를 패칭</strong> 하고 있었습니다. 커머스 서비스의 특성상 무한스크롤 페이지가 많았고, 이미지나 상품 정보처럼 한 페이지에서 불러오는 데이터 양이 많다 보니 메모리 사용량이 빠르게 증가한 것으로 추측했습니다.</p>
<p>문제를 해결하기 위해 문제 해결을 위해 먼저 일부 페이지의 <strong>데이터 패칭 방식을 <code>서버 → 클라이언트</code> 로 전환</strong>했습니다. 클라이언트에서 데이터를 가져올 경우 서버 메모리에 직접 데이터를 유지하지 않기 때문에 일시적으로 메모리 사용량을 줄일 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/fc966e51-94ee-43fa-b400-269b342ad452/image.webp" alt=""></p>
<blockquote>
<h4 id="💭-캐싱도-살리고-메모리도-지킬-수는-없을까">💭 캐싱도 살리고 메모리도 지킬 수는 없을까?</h4>
<p>클라이언트 패칭으로 변경하면서 들었던 생각은 <em>&quot;Next.js의 장점 중 하나가 데이터 캐싱인데 이 캐싱이 오히려 OOM의 원인이 되어 버린다면 과연 Next.js의 이점을 제대로 활용하고 있는 것일까?&quot;</em> 라는 의문이 들었습니다.
그렇다면, <em>&quot;Next.js의 데이터 캐싱 기능을 유지하면서도 OOM 문제를 해결할 수 있는 방법은 없을까?&quot;</em> 라는 생각이 들었고 아직 실무에 적용해보진 않았지만 다음과 같은 방법이 있다고 생각했습니다.</p>
</blockquote>
<ul>
<li><strong>캐시의 만료 시간 설정</strong><ul>
<li><code>fetch()</code>의 <code>next.revalidate</code> 옵션을 이용해 불필요한 데이터의 장기 보관을 피할 수 있습니다. 참고 ➡️ <a href="https://velog.io/@ahn-sujin/Next.jsv15-%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%BA%90%EC%8B%9C">Next.js(v15) 에서 데이터 캐시하기</a></li>
</ul>
</li>
<li><strong>클라이언트 패칭과 서버 패칭 구분</strong><ul>
<li>동적인 데이터와 정적인 데이터에 따라 적절한 패칭 방식을 선택한다.</li>
</ul>
</li>
</ul>
<br />


<h2 id="번외-oom-디버깅-방법">번외) OOM 디버깅 방법</h2>
<p>크롬의 개발자 도구를 통해서 메모리 디버깅을 진행할 수 있습니다. </p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/62945fc8-8658-4224-9310-010f8ba0efef/image.png" alt=""></p>
<p><strong>1. Heap snapshot</strong>
   <img width="310" alt="스크린샷 2025-04-29 오후 10 45 58" src="https://github.com/user-attachments/assets/567207b4-1c72-49e7-84cc-9287371977d5" /></p>
<ul>
<li>현재 페이지의 힙 상태를 기록하고 분석할 수 있으며 성능 개선 전후로 스냅샷을 비교할 수 있습니다.</li>
</ul>
<p><strong>2. Allocation instrumentation timeline</strong>
   <img width="609" alt="스크린샷 2025-04-29 오후 10 37 28" src="https://github.com/user-attachments/assets/edef6b57-f627-4b3c-9573-1e44ccf6ab80" /></p>
<ul>
<li>메모리 누수가 의심되는 <strong>시나리오를 수행하여 메모리 상태를 기록</strong>할 수 있다. 기록이 진행하는 동안 타임라인에 <strong>메모리 할당과 해제가 그래프로 표시</strong>되어 이를 분석하여 메모리 누수를 디버깅할 수 있습니다.</li>
</ul>
<p><strong>3. Allocation sampling</strong></p>
<ul>
<li>메모리 할당을 <strong>함수 단위</strong>로 간단하게 기록할 수 있습니다.</li>
</ul>
<blockquote>
<p>자세한 디버깅 방법은 <a href="https://www.youtube.com/watch?v=P3C7fzMqIYg">FEConf 2023 [B2] SSR 환경(Node.js) 메모리 누수 디버깅 가이드</a> 를 참고해주세요!</p>
</blockquote>
<p>크롬 개발자 도구의 Memory 탭을 활용해 메모리 누수의 원인을 찾아보려 했지만, 개인적으로는 직관적이지 않고 어려웠다고 느꼈습니다. 관련 블로그나 문서를 참고했지만 어떤 항목을 중점적으로 봐야 하는지 명확하지 않았고 각 지표가 무엇을 의미하는지도 쉽게 와닿지 않았던 것 같습니다. (익숙하지 않아서 더 그랬던 것일 수도...😅)</p>
<p>해당 탭에서 제가 파악할 수 있었던 건, 현재 페이지에서 메모리가 얼마나 사용되고 있는지, 메모리가 적절히 해제되고 있는지 여부 정도였던 것 같습니다. 실제 누수의 원인을 정확히 짚어내기에는 한계가 있었고, 프론트엔드 개발자가 실무에서 활용할 수 있는 메모리 디버깅 툴이나 라이브러리를 더 찾아봐야겠다는 생각이 들었습니다.</p>
<br />

<blockquote>
<p>📚 <strong>참고</strong>
<a href="https://deepu.tech/memory-management-in-v8/">Visualizing memory management in V8 Engine</a>
<a href="https://viblo.asia/p/tim-hieu-ve-nextjs-va-build-ung-dung-don-gian-ket-hop-firebase-yMnKM6DgZ7P">Tìm hiểu về NextJS và build ứng dụng đơn giản kết hợp Firebase</a>
<a href="https://velog.io/@davin/Next.js%EC%97%90%EC%84%9C-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%88%84%EC%88%98-%EB%94%94%EB%B2%84%EA%B9%85%ED%95%98%EA%B8%B0">Next.js에서 메모리 누수 디버깅하기</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[3년차 프론트엔드 개발자의 이직 성공기]]></title>
            <link>https://velog.io/@ahn-sujin/3%EB%85%84%EC%B0%A8-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%9D%B4%EC%A7%81-%EC%84%B1%EA%B3%B5%EA%B8%B0</link>
            <guid>https://velog.io/@ahn-sujin/3%EB%85%84%EC%B0%A8-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%9D%B4%EC%A7%81-%EC%84%B1%EA%B3%B5%EA%B8%B0</guid>
            <pubDate>Tue, 22 Apr 2025 14:14:08 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! 오랜만에 회고 블로그로 돌아왔습니다. 마지막 글을 올리고 나서 크다면 큰 변화가 생겼습니다. 바로 제가 이직에 성공했습니다🎉 </p>
<p>작년 10월에 다니던 회사가 경영 악화로 갑작스럽게 문을 닫으면서 퇴사를 하게 됐고, 냅다 채용 시장에 던져졌습니다.ㅋㅋㅋㅋ 그후 약 5-6개월 이직 준비를 하게 됐는데요, <em>&quot;내가 다시 취업할 수 있을까?&quot;</em> 하는 생각에 불안할 때도 많았지만 지금 돌아보면 여유롭던 아침이 살짝 그리워지기도 합니다...ㅎㅎ</p>
<p>힘들었지만 어느 때보다 의미 있었던 이직 준비 과정을 돌아보면서 앞으로의 계획도 함께 정리해보려 합니다!</p>
<hr>
<br />

<h2 id="🤷🏻♀️-왜-퇴사했나요">🤷🏻‍♀️ 왜 퇴사했나요?</h2>
<p>면접에서 꼭 물어보는 질문 중 하나죠ㅎㅎ 저는 경영 악화로 인한 퇴사였기 때문에 설명이 복잡하진 않았습니다. (장점이라면 장점?ㅋㅋㅋ) 회사가 그렇게 하루 아침에 문을 닫게 될 줄은 몰라서 처음엔 그 상황이 웃겼던 것(?) 같습니다ㅋㅋㅋㅋ 이후에 실업급여 신청하고, 회사가 정리된 후에 이력서 쓰고 포폴 준비하면서 <em>&quot;아, 진짜 나 백수됐구나&quot;</em> 하면서 실감 나면서 걱정이 됐던 것 같습니다.</p>
<p>사실 회사를 2년 동안 다니면서 이직 생각을 안 했던 건 아닙니다. 중간중간 면접도 몇 번 봤습니다. 그런데 일을 하면서 이직을 준비하다보니 좋은 회사를 가고 싶은 욕심은 커지는데 시간과 에너지가 부족해서 쉽지 않았습니다. 그런 면에서, 이번에는 오히려 몰입해서 준비할 수 있었던 점이 장점이었던 것 같기도 합니다.</p>
<br />

<h2 id="🏃♀️-어떻게-준비했나요">🏃‍♀️ 어떻게 준비했나요?</h2>
<h3 id="하루-루틴-만들기">하루 루틴 만들기</h3>
<p>저는 완전 J형 인간이라서 퇴사하고 제일 먼저 했던게 무의미하게 시간을 보내는 걸 예방하기 위해 <strong>노션 플래너</strong>를 만드는 것이었습니다. 오전에는 일단 아침에 눈뜨면 무조건 헬스장으로 갔습니다. 운동하는 습관은 제 인생 통틀어 가장 잘한 일이라고 생각하고 진심으로 모든 분들께 추천드립니다! 운동 다녀온 후에 아점을 간단히 먹고 채용 공고를 싹 훑어보고 지원했습니다. 그리고 면접 준비, 코테 준비를 하면서 워밍업을 한 후에 그 날의 계획에 맞게 코어 타임에는 공부를 했습니다. 주로 강의를 듣거나 블로그를 쓰거나 개인 프로젝트를 진행했습니다. 저녁을 먹은 후에 잠깐 쉬었다가 코어 타임에 다 끝내지 못했던 일을 하면서 하루를 마무리했습니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/1e23ba03-35a3-4c24-b212-6e480eff4f90/image.png" alt=""></p>
<p>물론 하루 계획을 다 지키지 못하는 날도 있었지만 중요한 건 꾸준히 하는 거라고 생각했기 때문에 <em>&quot;아 망했네&quot;</em> 라고 생각하기 보다 <em>&quot;오늘 못했으니깐 내일 더 완벽히 하면 되겠다&quot;</em> 라고 긍정적으로 생각하려고 했던 것 같습니다.</p>
<h3 id="이력서는-무조건-여러-번-써보기">이력서는 무조건 여러 번 써보기</h3>
<p>위의 루틴은 거의 올해 1월부터 진행했었고 작년 11월~12월은 이력서 준비 + 피드백 반복의 연속이었습니다. 양식, 구성, 내용 등 뭐 하나 쉽게 느껴지는 게 없었고 정말 많이 시간을 쏟아서 고민했던 것 같습니다. 처음에 인프런과 왓에버에서 이력서 멘토링을 신청해서 피드백을 받으면서 틀을 잡았고, 이후에는 이력서를 넣어보면서 서류 결과에 따라서 또 조금씩 수정했습니다. </p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/e02c8b03-557c-446e-a3dc-4fa8d3af03b4/image.png" alt=""></p>
<p>이력서를 여러 번 수정하면서 느꼈던 것은 결국 모두가 말하는 좋은 이력서는 단순히 무엇을 했는지 나열하기 보다 <strong>무엇을 어떻게 했는지</strong>를 설명해야하고 <strong>어떻게 했는지가 면접관에게 매력적으로 다가와야 한다</strong>는 것이었습니다. 이 말 또한 추상적이고 당연하게 느껴질 수 있지만ㅋㅋㅋ 계속 고치고 고민하다보면 스스로 감을 잡아가는 것 같습니다.</p>
<p>(+ 그리고 추가적으로 회사마다 뽑는 기준이 다 다르기 때문에 절대적으로 좋은 이력서는 없다는 것도 느꼈습니다. A회사에서는 떨어진 이력서가 B,C회사에서는 붙을 수도 있다는 점...)</p>
<h3 id="알고리즘--면접-준비는-평소에-꾸준히">알고리즘 &amp; 면접 준비는 평소에 꾸준히</h3>
<p>이 두 가지(알고리즘, 면접)는 사실 하루 아침에 되는게 아니기 때문에 평소에 준비하는게 중요하다고 생각했습니다. </p>
<p><strong>알고리즘</strong>은 지금도 잘하진 않지만ㅋㅋㅋ 프로그래머스에서 LV0부터 차근차근 시작하면서 기초는 어느 정도 다졌다고 생각합니다. 문제를 읽고 해석할 수 있고, <em>“여기선 이걸 써야겠다.”</em> 판단할 수 있을 정도까진 된 것 같습니다.</p>
<p><strong>면접</strong>은 크게 <strong>기술 질문, 이력서 기반 질문, 인성 질문</strong> 이렇게 나뉘는 것 같습니다. 3가지 유형 모두 평소에 준비해두는 게 중요하다고 느꼈습니다. (그래야 면접 때 어버버하지 않고 자연스럽게 말할 수 있더라구요😅) 기술 질문은 CS부분이 평소 부족하다고 느껴서 인프런의 CS 강의를 들으면서 기초 개념들을 정리했습니다. 프론트엔드 관련 지식(JS,TS,React,Next.js 등...) 은 인터넷에 돌아다니는 대표 질문들을 모두 정리했고 이후에는 매일메일 서비스 구독을 하며 준비했습니다.</p>
<p>기술 질문들을 정리할 때는 무작정 암기하기 보다는 원리를 이해하려고 했습니다. 그러면 억지로 외우려하지 않아도 자연스럽게 기억에 남았던 것 같습니다. </p>
<h3 id="면접-복기">면접 복기</h3>
<p>면접이 끝나면 그냥 멍하니 이불 킥 하면서 시간을 보내기 보다는 어떤 질문들이 나왔고 내가 어떤 것에 질문을 못하고 잘했는지를 정리하는게 중요한 것 같습니다. 실제로 <em>&quot;나 대답 좀 잘한거 같은데?&quot;</em> 싶은데 하나하나 복기해보면 대답 엉망으로 한것 도 있었고 <em>&quot;이게 맞나?</em>&quot; 싶은데 맞을 때도 있었습니다ㅋㅋㅋ (중요한 건 자신감일지도) 이 과정이 다음 면접 때 정말 많은 도움이 되기 때문에 떨어지더라도 얻어가는 게 많았습니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/8933c5c7-d50d-4186-a8b4-1ab5add2bd57/image.png" alt=""></p>
<br />

<h2 id="🏢-그래서-어떤-회사로-갔나요">🏢 그래서 어떤 회사로 갔나요?</h2>
<p>이번에 입사한 회사는 커머스 스타트업입니다.</p>
<p>스타트업이라는 특성상 빠르게 변화하고 끊임없이 도전해야 하는데, 여기에 커머스까지 더해지니 정말 공부할 게 많았습니다...ㅎㅎ 특히 커머스는 서비스 운영 상태가 매출과 직결되기 때문에 예민하고 빠르게 반응해야 한다는 것을 느꼈습니다.</p>
<p>입사 후 가장 인상 깊었던 건, 항상 큰 화면에 <a href="https://www.whatap.io/?utm_source=google&amp;utm_medium=cpa&amp;utm_campaign=da_pmax_kr&amp;utm_term=&amp;utm_campaign=%5B2407%5D+KR_CPA_Pmax&amp;utm_source=adwords&amp;utm_medium=ppc&amp;hsa_acc=6431792263&amp;hsa_cam=21483295088&amp;hsa_grp=&amp;hsa_ad=&amp;hsa_src=x&amp;hsa_tgt=&amp;hsa_kw=&amp;hsa_mt=&amp;hsa_net=adwords&amp;hsa_ver=3&amp;gad_source=1&amp;gbraid=0AAAAADlRry4oW6ODyItETqFZY8bfopAQ6&amp;gclid=Cj0KCQjw_JzABhC2ARIsAPe3ynof-IA7ieu6mwaUJ0AB4fMeF2jlU706OfYozyZZqT7_my5OZ1nSGfoaAjvJEALw_wcB">whatap</a>으로 모두가 볼 수 있도록 실시간 모니터링을 하고 있다는 것이었습니다. 보면서 “<em>지금 이 순간에도 누군가는 이 서비스를 사용하고 있구나”</em> 라는 생각에 신기하기도 했고 실시간으로 서버 상태를 확인할 수 있어서 편리하다고 생각했습니다.</p>
<p>이 회사를 가고 싶다고 생각했던 이유 중 하나는 면접에서 정해진 기술 질문보다는 제 이력서를 기반으로 실제 경험에 대해 궁금해하는 느낌을 받아서 좋았습니다. 또, CTO님의 <em>&quot;커머스에서 다양한 경험을 할 수 있고 그만큼 개발자로 많이 성장할 수 있다&quot;</em> 는 말씀이 인상 깊었습니다. 물론 그 말은 곧 힘든 일도 많을 수 있다는 뜻이겠지만ㅋㅋㅋ 세상에 쉽게 얻어지는 건 없다고 생각해서 지금의 회사를 가게되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/4cfdeb59-bec3-41a7-9261-bb4378d858a4/image.png" alt=""></p>
<br />

<h2 id="✨-앞으로의-계획은-무엇인가요">✨ 앞으로의 계획은 무엇인가요?</h2>
<h4 id="1-빠르게-적응하기">1. 빠르게 적응하기</h4>
<p>현재 회사의 업무 분위기, 개발 환경, 기술 스택에 빠르게 적응하는 게 가장 우선이라고 생각합니다. 예전엔 기능만 구현하면 끝이라고 생각했지만, 이젠 코드의 질과 성능까지 고민하는 개발자가 되고 싶습니다. 이런 이야기를 CTO님과의 1on1 시간에 나눴는데 도움 받을 수 있는 책을 4권이나 주셨습니다 🥹 (부지런히 공부해야겠죠?ㅎㅎ) 지금은 그중 첫 번째로 <a href="https://www.yes24.com/product/goods/102819435">『대규모 시스템 설계 기초』</a> 를 읽고 있습니다.</p>
<h4 id="2-공부를-습관화하기">2. 공부를 습관화하기</h4>
<p>회사에서 배운 것, 새롭게 알게 된 기술들은 개인 Notion, GitHub, 블로그에 정리하려고 합니다. 이번 주에도 기술 블로그를 쓰고 싶었지만 주제를 정하지 못해서 일단 회고 블로그부터 업로드 하기로 했습니다ㅋㅋㅋ 앞으로는 최소 월 1~2회 블로그 포스팅을 목표로 하고 있습니다.</p>
<h4 id="3-개인-블로그-개편">3. 개인 블로그 개편</h4>
<p>작년부터 계획만 세워두고 미뤄왔던 개인 블로그 개편도 올해 꼭 해내고 싶습니다ㅋㅋ
개인적으로 저만의 블로그를 만들고 싶은 로망(?)이 있어서 단순히 글만 올리는 게 아니라, 직접 블로그를 개발하고 CRUD 기능까지 구현해서 운영하는 게 목표입니다!</p>
<p>물론 지금 계획들은 단순하고 당연한 것 들일 수 있지만 회사를 다니면서 더 구체적인 목표들이 생길 거라고 생각합니다.</p>
<br />

<h2 id="👋🏻-마무리">👋🏻 마무리</h2>
<p>마지막으로 제가 준비하면서 힘들 때 chat GPT에게 상담을 했었는데요ㅋㅋㅋ 이게 생각보다 위로가 되더라구요. 혹시나 위로가 필요하신 분들이 계시다면 chat GPT에게 고민을 털어놓는 것도 방법이 될 것 같습니다!</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/58872f53-2cd1-4bde-8dd7-6f83c36e64f0/image.jpeg" alt=""></p>
<p>제가 좋아하는 말 중에 <em><strong>&quot;운을 이기는 것은 꾸준함이다.&quot;</strong></em> 라는 말이 있는데요. 어려운 시기이지만 목표를 이루기 위해 꾸준히 움직인다면 못할 건 없다고 생각합니다! 그러니 우리 모두 멈추지말고 킵고잉합시다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백엔드 없이 통신을 할 수 있다고? - Next.js 의 서버 액션]]></title>
            <link>https://velog.io/@ahn-sujin/Next.js%EC%9D%98-%EC%84%9C%EB%B2%84-%EC%95%A1%EC%85%98%EC%9C%BC%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ahn-sujin/Next.js%EC%9D%98-%EC%84%9C%EB%B2%84-%EC%95%A1%EC%85%98%EC%9C%BC%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 31 Mar 2025 12:00:16 GMT</pubDate>
            <description><![CDATA[<p>기존 Next.js 에서는 API Route를 통해 클라이언트에서 서버와 통신할 수 있었는데요, 이후 <strong>서버 액션</strong>이 도입되면서 더 간결하고 직관적인 방식으로 서버 측 로직을 실행할 수 있게 되었습니다.</p>
<p>이번 글에서는 서버 액션의 구체적인 활용 후기라기 보다는 <strong>서버 액션이 무엇이고 어떻게 동작하는지에 대한 이론적인 부분</strong>에 대해서 정리해보려고 합니다. 서버 액션의 구체적인 활용 방법에 대해서는 다음 글에서 만나요👋🏻</p>
<hr>
<br />


<h2 id="1️⃣-서버-액션이란">1️⃣ 서버 액션이란?</h2>
<p>서버 액션은 <strong>클라이언트에서 호출할 수 있는 서버에서 실행되는 비동기 함수</strong>입니다. </p>
<p>기존에는 DB를 수정하려면 아래와 같은 플로우로 진행됩니다.</p>
<blockquote>
<ol>
<li>백엔드에서 API 엔드 포인트 생성</li>
<li>프론트엔드에서 해당 API를 호출</li>
</ol>
</blockquote>
<p>하지만 서버 액션을 사용하면 <strong>별도의 API 없이 Next.js 서버에서 실행되는 함수를 호출해 직접 DB를 수정</strong>할 수 있습니다.</p>
<br />

<h2 id="2️⃣-서버-액션-사용하기">2️⃣ 서버 액션 사용하기</h2>
<pre><code class="language-jsx">export default function Page() {
    const saveName = async (formData: FormData) =&gt; {    
          &quot;use server&quot;;

        const name = formData.get(&quot;name&quot;);
          console.log(&quot;name&quot;, name);

        // 실제 DB에 저장하려면 여기에 sql 와 같은 서버에서 실행되는 명령어를 추가하면 됩니다.
    }

    return (
        &lt;form action={saveName}&gt;
              &lt;input name=&quot;name&quot; placeholder=&quot;이름을 알려주세요...&quot; /&gt;
            &lt;button type=&quot;submit&quot;&gt;제출&lt;/button&gt;
          &lt;/form&gt;
    )

}
</code></pre>
<p>위 코드는 클라이언트에서 form 태그를 통해 유저의 이름을 서버에 저장하기 위한 간단한 예제 입니다.</p>
<ul>
<li><code>saveName</code> 함수<ul>
<li>함수 최상단에 <code>&quot;use server&quot;</code> 지시자를 통해 서버 액션 함수를 만들어 줍니다.</li>
<li><code>formData.get(&quot;name&quot;)</code> 을 이용해 입력된 데이터를 가져옵니다.</li>
</ul>
</li>
<li><code>&lt;form action={saveName}&gt;</code> <ul>
<li><code>action</code> 속성에 서버 액션을 전달하면 폼 제출 시 서버에서 해당 함수(<code>saveName</code>)가 실행됩니다. </li>
<li>결과적으로 프론트에서 API 요청을 할 필요없이 서버 액션만으로 DB 수정이 가능하게 됩니다.</li>
</ul>
</li>
</ul>
<br />

<h3 id="✔️-서버-액션은-서버에서만-실행된다">✔️ 서버 액션은 서버에서만 실행된다.</h3>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/c6a6b050-992e-43c2-847f-4f6489085377/image.png" alt="console.log"></p>
<p>실제로 위 코드의 폼을 제출하게 되면 서버로 요청이 전송되며, <strong>서버측 콘솔인 vscode 터미널에 console.log 가 출력</strong> 되는 것을 확인할 수 있습니다. 이는 서버 액션이 클라이언트에서 실행되는 것이 아니라 서버에서 실행되고 있음을 증명하는 요소입니다. </p>
<p>이를 통해 서버 액션을 호출하면 Next.js 서버에서 실행되는 함수로 요청이 전달되었고 해당 함수가 정상적으로 실행되었음을 확인할 수 있습니다.</p>
<br /> 

<h3 id="✔️-request-headers의-next-action-해시값">✔️ Request Headers의 Next-Action 해시값</h3>
<p>서버 액션이 실행될 때, 브라우저의 Network 탭에서 요청 정보를 확인하면** Request Headers에 Next-Action 해시값**이 포함된 것을 볼 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/92a45682-86a0-4544-9efd-6b278bb6c83b/image.png" alt="Header"></p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/6770ced9-1a38-4f42-a3d8-67a4e5319675/image.png" alt="Request"></p>
<p>Next.js는 서버 액션을 호출할 때 내부적으로 해당 액션을 실핸하는 API 엔드포인트를 자동으로 생성합니다. 이때, 각 서버 액션 함수는 고유한 해시값(Next-Action)과 연결되며 브라우저가 해당 함수를 실행할 때 Request Header의 Next-Action 필드에 이 해시값을 전달하게 됩니다.</p>
<p>즉, 이 <strong>해시값은 어떤 서버 액션을 실행할지 식별하는 역할</strong>을 하는 것입니다. 이를 통해 Next.js가 서버 액션을 특정 API 엔드 포인트로 변환하여 처리하게 됩니다.</p>
<br />

<h3 id="💬-서버-액션의-동작-과정-정리">💬 서버 액션의 동작 과정 정리</h3>
<p><strong>1. 서버 실행 시, 서버 액션을 해시값과 매핑</strong></p>
<ul>
<li>Next.js가 빌드될 때, <strong>서버 액션 함수마다 고유한 해시값을 생성</strong>하고 특정 API 엔드포인트로 변환합니다. </li>
</ul>
<p><strong>2. 클라이언트에서 서버 액션 호출 (폼 제출)</strong></p>
<ul>
<li>브라우저에서 <code>&lt;form&gt;</code> 을 제출하면 <strong>Next.js는 내부적으로 서버 액션을 실행하는 API 요청</strong>을 보냅니다.</li>
<li>이때, <strong>Request Headers에 Next-Action 해시값이 자동으로 포함</strong>됩니다.</li>
</ul>
<p><strong>3. Next.js 서버에서 요청 처리</strong></p>
<ul>
<li>서버는 요청 헤더에서 Next-Action 값을 확인하고, <strong>해당 해시값과 매핑된 서버 액션을 실행</strong>합니다.</li>
</ul>
<p><strong>4. 서버 액션 실행 및 결과 반환</strong></p>
<ul>
<li>서버 액션이 실행되면서 DB 저장 등 필요한 작업을 수행하게 됩니다.</li>
</ul>
<br />

<h3 id="🚨-주의할-점">🚨 주의할 점</h3>
<p>서버 액션에서 FormData를 사용할 때, formData.get(&quot;key&quot;) 메서드는 <code>FormDataEntryValue | null</code> 타입을 반환합니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/1c28af26-af3f-448c-9d8e-fa034336439b/image.png" alt="예시"></p>
<ul>
<li><code>FormDataEntryValue</code>: 이는 string 또는 File일 수 있습니다. 보통 텍스트 입력 필드에서는 string이 반환됩니다.</li>
<li><code>null</code> : 해당 키에 대응되는 값이 없을 경우 null이 반환됩니다.</li>
</ul>
<p>따라서, <code>formData.get(&quot;name&quot;)</code> 의 결과값을 직접 사용할 때는 반드시 이를 처리해 주어야 합니다. </p>
<ol>
<li>null 값이 올 경우를 대비한 처리</li>
<li>결과값에 대한 string 처리</li>
</ol>
<pre><code class="language-jsx">
const name = formData.get(&quot;name&quot;)?.toString();
</code></pre>
<br />

<h2 id="3️⃣-서버-액션을-사용하는-이유">3️⃣ 서버 액션을 사용하는 이유</h2>
<p>마지막으로 서버 액션을 왜 사용해야하면 어떨 때 사용해야 하는지에 대해 다음과 같이 정리해 보았습니다.</p>
<p><strong>1. 코드가 간결하다.</strong></p>
<ul>
<li>기존 API Route 방식보다 짧고 직관적인 코드 작성이 가능합니다.</li>
</ul>
<blockquote>
<p><strong>기존 API Route 방식</strong> </p>
<ol>
<li>API Route 파일을 따로 생성 
→ pages/api 또는 app/api 경로에 서버 로직을 작성</li>
<li>클라이언트에서 해당 API 엔드포인트를 호출 
→ fetch(&#39;/api/xxx&#39;) 또는 axios.post(&#39;/api/xxx&#39;) 사용</li>
</ol>
</blockquote>
<p><strong>2. 보안성이 높다.</strong></p>
<ul>
<li>서버 액션은 클라이언트에서 실행되지 않고 서버에서만 실행되므로 API 엔드포인트가 노출되지 않습니다. 따라서, API 요청을 가로채거나 변조하는 보안 위협을 줄일 수 있습니다.</li>
</ul>
<p><strong>3. API 없이도 데이터 처리가 가능하다.</strong></p>
<ul>
<li>별도의 API를 만들 필요 없이 Next.js 내부에서 서버 액션을 실행하면 되기 때문에 단순한 CRUD 작업이라면 유지보수에 더 유리할 수 있습니다.</li>
</ul>
<br />

<h2 id="✨-마무리">✨ 마무리</h2>
<p>서버액션에 대해 정리하면서 Next.js에서 기존에 제공하던 API Route보다 훨씬 간편하게 사용할 수 있다는 점이 가장 인상적이었습니다. </p>
<p>API Route의 경우 서버 로직을 따로 작성한 뒤 클라이언트 에서 fetch로 또 해당 로직으로 불러와야했기 때문에 관리가 복잡하게 느껴졌었지만, 서버 액션을 사용하면 하나의 함수로 서버 로직을 관리하고 클라이언트에서 직접 호출할 수 있어 유지보수가 훨씬 쉽게 할 수 있겠다는 생각이 들었습니다. (풀스택 사프 SSAP가능..?😅) </p>
<p>하지만 복잡한 비즈니스 로직이 필요한 경우에는 서버 액션만으로 해결하기 어려운 부분이 반드시 있을거고 백엔드 API 설계를 완전히 대체 할 수는 없겠다는 생각이 들었습니다. 특히 프론트에서 DB를 다루기 위해서 필요한 sql 설정을 하는 것도 리소스가 필요하고 어쩌면 배보다 배꼽이 더 커지는 일이 발생할 수 도 있겠다는 생각이...ㅎㅎ  언제나 그렇듯 프로젝트의 규모와 요구 사항에 따라 사용하는게 가장 중요하겠죠?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js의 고오급 라우팅 패턴]]></title>
            <link>https://velog.io/@ahn-sujin/Next.js%EC%9D%98-%EA%B3%A0%EC%98%A4%EA%B8%89-%EB%9D%BC%EC%9A%B0%ED%8C%85-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@ahn-sujin/Next.js%EC%9D%98-%EA%B3%A0%EC%98%A4%EA%B8%89-%EB%9D%BC%EC%9A%B0%ED%8C%85-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Tue, 18 Mar 2025 12:39:56 GMT</pubDate>
            <description><![CDATA[<p>Next.js 공부를 하다가 새로운 라우팅 패턴에 대해 알게 되었습니다. 이런게 가능하다니? 신기한 마음에 블로그로 정리해 보려고 합니다!</p>
<hr>
<h2 id="1️⃣-패럴렐-라우트">1️⃣ 패럴렐 라우트</h2>
<p>발음하기도 어려운(?) 패럴렐 라우트는 병렬 라우트를 뜻합니다. 즉, <strong>하나의 화면 안에 여러 페이지 컴포넌트를 한번에 렌더링</strong> 시켜주는 패턴입니다. 주로 트위터와 같은 소셜 미디어, 어드민 대시보드 등 복잡한 구조에 사용됩니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/294ce6d9-8808-44b1-bc73-01636035c50f/image.png" alt="예시 이미지"></p>
<h3 id="slot슬롯">Slot(슬롯)</h3>
<p>패럴렐 라우트에는 <strong>슬롯</strong> 이라는 개념이 등장하는데요. 이는 병렬로 렌더링 될 페이지들을 보관하는 폴더 역할을 합니다. 원하는 페이지 폴더 아래 <code>@</code> 를 붙여서 슬롯을 만들어 주면 됩니다. *<em>이때, 생성된 슬롯은 라우트 그룹처럼 URL경로에는 아무런 영향을 주지 않습니다. *</em></p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/ff91510e-1c22-4708-8ef9-5e389e1b0a6d/image.png" alt="슬롯"></p>
<p>슬롯의 특징에 대해서 몇가지 더 살펴보자면,</p>
<h4 id="1-부모-컴포넌트layouttsx에-자동으로-props슬롯에-생성한-컴포넌트로-전달됩니다">1. 부모 컴포넌트(<code>layout.tsx</code>)에 자동으로 props(<code>슬롯에 생성한 컴포넌트</code>)로 전달됩니다.</h4>
<pre><code class="language-jsx">export default function Layout({
  children,
  sidebar,
  feed,
}: {
  children: React.ReactNode;
  sidebar: React.ReactNode;
  feed: React.ReactNode;
}) {
  return (
    &lt;div&gt;
      {feed}
      {sidebar}
      {children}
    &lt;/div&gt;
  );
}
</code></pre>
<h4 id="2-개수-제한이-없습니다">2. 개수 제한이 없습니다.</h4>
<ul>
<li>하나의 페이지 디렉토리 안에 여러 개의 스롯을 만들 수 있습니다.</li>
</ul>
<h4 id="3-슬롯-안에서-새로운-페이지를-추가할-수-있습니다">3. 슬롯 안에서 새로운 페이지를 추가할 수 있습니다.</h4>
<ul>
<li>해당 슬롯 내부에서 페이지 전환이 가능하지만 바로 그 경로로 접근하거나 새로 고침을 하게되면 404페이지를 만나게 됩니다.</li>
</ul>
<br />

<p>3번 특징에 대해 좀더 자세히 알아보겠습니다. </p>
<h3 id="슬롯-안에서-새로운-페이지를-추가한-경우">슬롯 안에서 새로운 페이지를 추가한 경우</h3>
<p><code>@feed</code> 슬롯 안에 <code>setting</code> 이라는 페이지 폴더를 생성한 뒤 <code>layout.tsx</code> 에 해당 페이지로 이동할 수 있는 테스트 경로를 추가했습니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/18379b2b-cfc4-445f-a802-0557b67a8729/image.png" alt=""></p>
<pre><code class="language-jsx">// app/parallel/layout.tsx 

import Link from &quot;next/link&quot;;

export default function Layout({
  children,
  sidebar,
  feed,
}: {
  children: React.ReactNode;
  sidebar: React.ReactNode;
  feed: React.ReactNode;
}) {
  return (
    &lt;div&gt;
      &lt;div&gt;
        &lt;Link href={&quot;/parallel&quot;}&gt;parallel&lt;/Link&gt;
        &amp;nbsp;
        &lt;Link href={&quot;/parallel/setting&quot;}&gt;parallel/setting&lt;/Link&gt;
      &lt;/div&gt;

      &lt;br /&gt;
      {feed}
      {sidebar}
      {children}
    &lt;/div&gt;
  );
}
</code></pre>
<h4 id="1-link-를-통해-진입한-경우">1. <code>&lt;Link&gt;</code> 를 통해 진입한 경우</h4>
<Link/>를 클릭하면 Next.js의 클라이언트 라우팅이 작동하게 되는데 이때, Next.js는 기존 페이지에서 새로운 슬롯(parallel/@feed/setting/page.tsx)을 동적으로 불러와서 렌더링하게 됩니다.

<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/6f773b2a-6c85-409d-95b5-f2e78be3e649/image.gif" alt=""></p>
<p>즉, 현재 <code>layout.tsx</code>가 유지된 상태에서 <strong>패럴렐 라우트 내부만 업데이트</strong> 되기 때문에 정상적으로 동작합니다.</p>
<h4 id="2-초기-진입-새로-고침한-경우">2. 초기 진입 (새로 고침)한 경우</h4>
<p>Next.js는 페이지 진입시 해당 페이지가 독립적인 페이지인지를 확인하는데 슬롯 안에 등록된 페이지는 직접 페이지로 등록하지 않기 때문에 존재하지 않는 경로라고 판단하는 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/b042c049-5274-431a-888b-b8e1fa0b632a/image.gif" alt=""></p>
<p>즉, <strong>페러렐 라우트 내부 페이지들은 기본적으로 단독으로 접근할 수 없고</strong> 반드시 상위 <code>layout.tsx</code> 가 존재해야 정상적으로 렌더링됩니다.</p>
<h4 id="3-해결-방법">3. 해결 방법</h4>
<p>위와 같은 오류를 해결하는 방법은 간단합니다. 페럴렐 라우트가 비어있을 때 기본적으로 보여줄 페이지인 <code>default.tsx</code> 를 만들어 주면 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/f71c9336-8a4a-4963-b80b-48c381285687/image.png" alt=""></p>
<pre><code class="language-jsx">export default function Default() {
  return &lt;div&gt;/parallel/default&lt;/div&gt;;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/a6ef634d-dfed-4554-b7ce-0b59e82ce5c9/image.gif" alt=""></p>
<br />

<h2 id="2️⃣-인터셉팅-라우트">2️⃣ 인터셉팅 라우트</h2>
<p>인터셉팅 라우트는 intercept 의 낚아채다 라는 뜻대로 경로를 낚아채는 것을 말합니다. 즉, 사용자가 동일한 경로로 접속하게 되더라고 특정 <strong>조건에 만족하면 다른 페이지를 렌더링하도록 설정하는 기술</strong> 을 말합니다.</p>
<p>그런데 여기서 말하는 조건은 개발자가 직접 설정할 수 있는 것은 아니고, <strong>Next.js 에서 지정된 조건으로 초기 접속이 아닐 때</strong> 를 말합니다. 예를 들면 <strong>클라이언트 사이드 렌더링 방식</strong> 으로 <code>&lt;Link&gt;</code> 혹은 <code>Router.push()</code> 을 말합니다.</p>
<h3 id="설정-방법">설정 방법</h3>
<ol>
<li>app 폴더 아래 인터셉팅 라우트를 적용시킬 폴더를 만듭니다.</li>
<li>해당 폴더 앞에 <code>(.)</code> 를 붙여줍니다.<ul>
<li><code>()</code> 뒤에 나오는 경로를 인터셉트 하라는 의미입니다.</li>
<li><code>.</code> 은 상태 경로를 나타냅니다. (동일한 경로라는 뜻)</li>
<li><code>(..)</code> 인터셉트를 적용시킬 페이지의 경로가 한 단계 위에 있을 때</li>
<li><code>(..)(..)</code> 인터셉트를 적용시킬 페이지의 경로가 두 단계 위에 있을 때</li>
<li><code>(...)</code> app폴더 바로 아래에 있는 페이지를 인터셉트하겠다는 의미
<a href="https://nextjs.org/docs/app/building-your-application/routing/intercepting-routes">자세한 내용 보러가기</a> </li>
</ul>
</li>
</ol>
<br />

<h2 id="3️⃣-패럴렐-라우트와-인터셉팅-라우트-함께-사용하기">3️⃣ 패럴렐 라우트와 인터셉팅 라우트 함께 사용하기</h2>
<p>이 두 가지 원리를 사용해서 인스타그램의 상세 페이지를 선택했을 때 동작하는 방식을 구현할 수 있습니다.</p>
<p>예시로 인스타그램(pc)에서 내 프로필에서 피드를 선택했을 때 피드 상세가 모달로 노출되지만, 다시 새로 고침을 하게되면 피드 상세 페이지로 보여지고 있습니다. 이와 같은 동작을 패럴렐 라우트와 인터셉팅 라우트를 사용해서 구현해 보도록 하겠습니다. </p>
<h3 id="구현-동작">구현 동작</h3>
<ol>
<li>메인 페이지에서 책을 클릭하면 모달 형태로 책 상세 정보가 나타난다.</li>
<li>새로 고침하면 책 상세 페이지로 이동한다.</li>
</ol>
<h3 id="코드-구조">코드 구조</h3>
<ul>
<li>app/page.tsx → 메인 페이지</li>
<li>app/@modal/(.)book/[id]/page.tsx → 모달을 통한 책 상세 페이지</li>
<li>app/book/[id]/page.tsx → 책 상세 페이지</li>
</ul>
<pre><code class="language-jsx">// app/page.tsx
export default function Home() {
  return (
    &lt;div className={style.container}&gt;
      &lt;section&gt;
        &lt;h3&gt;지금 추천하는 도서&lt;/h3&gt;
        &lt;div&gt;
          {recoBooks.map((book) =&gt; (
              &lt;Link href={`/book/${id}`} className={style.container}&gt;
                  ...
              &lt;/Link&gt;
          ))}
        &lt;/div&gt;
      &lt;/section&gt;
    &lt;/div&gt;
  );
}


// app/@modal/(.)book/[id]/page.tsx
import BookPage from &quot;@/app/book/[id]/page&quot;;
import Modal from &quot;@/components/modal&quot;;

export default function Page(props: any) {
  return (
    &lt;Modal&gt;
      &lt;BookPage {...props} /&gt;
    &lt;/Modal&gt;
  );
}

// app/book/[id]/page.tsx
export default function Page({ params }: { params: { id: string } }) {
  return (
    &lt;div className={style.container}&gt;
      &lt;BookDetail bookId={params.id} /&gt;
      &lt;ReviewEditor bookId={params.id} /&gt;
      &lt;ReviewList bookId={params.id} /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/227d75a6-3b17-4a63-9f3c-e6b39a025d37/image.gif" alt=""></p>
<h3 id="정리">정리</h3>
<p>패럴렐 라우트와 인터셉팅 라우트를 함께 사용하면, 기존 페이지에서 모달을 띄우는 사용자 경험을 제공하면서도 새로 고침 시 개별 상세 페이지로 자연스럽게 이동하는 동작을 구현할 수 있습니다. </p>
<h4 id="1-메인-페이지에서-책을-클릭하면-모달이-열림-인터셉팅-라우트">1. 메인 페이지에서 책을 클릭하면 모달이 열림 (인터셉팅 라우트)</h4>
<ul>
<li>app/@modal/(.)book/[id]/page.tsx가 렌더링됩니다. </li>
<li>이 파일은 app/book/[id]/page.tsx를 불러와 <code>&lt;Modal&gt;</code> 로 감싸서 모달 형태로 표시됩니다. </li>
</ul>
<h4 id="2-새로-고침하면-모달이-사라지고-책-상세-페이지로-이동-패럴렐-라우트">2. 새로 고침하면 모달이 사라지고 책 상세 페이지로 이동 (패럴렐 라우트)</h4>
<ul>
<li>/book/[id]로 직접 접근하는 형태가 되면 app/book/[id]/page.tsx가 단독 렌더링됩니다. 즉, 모달이 아닌 전체 페이지로 전환됩니다.</li>
</ul>
<h4 id="3-modal-패럴렐-라우트와-defaulttsx-역할">3. @modal 패럴렐 라우트와 default.tsx 역할</h4>
<ul>
<li><code>/</code> 로 접근하거나 <code>/book/[id]</code> 에 직접 접근했을 경우를 위해 <code>default.tsx</code> 을 만들어 줘야합니다.</li>
<li>app/@modal/default.tsx는 기본적으로 null을 반환합니다. → 모달이 필요 없을 때 아무것도 렌더링하지 않는 역할을 합니다.</li>
</ul>
<pre><code class="language-jsx">export default function Default() {
  return null;
}
</code></pre>
<br />

<blockquote>
<p>📚 참고
<a href="https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs?attributionToken=kwHwkgoLCLnW5b4GENyzvSoQARokNjdkZTY3ZDgtMDAwMC0yZDZiLTk2ZDEtM2MyODZkNDJjZDQ2KgY4ODc0OTMyOJzWty23t4wtjr6dFdSynRXC8J4Vo4CXIqjlqi3Hy_MXkPeyMJruxjCf1rct8tntMPXZ7TD36MMwOg5kZWZhdWx0X3NlYXJjaEgBWAFgAWgBegJzaQ">한 입 크기로 잘라먹는 Next.js(v15)</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Streaming SSR]]></title>
            <link>https://velog.io/@ahn-sujin/Next.js-%ED%95%98%EC%9D%B4%EB%93%9C%EB%A0%88%EC%9D%B4%EC%85%98-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-Suspense%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ahn-sujin/Next.js-%ED%95%98%EC%9D%B4%EB%93%9C%EB%A0%88%EC%9D%B4%EC%85%98-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-Suspense%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 13 Mar 2025 11:19:26 GMT</pubDate>
            <description><![CDATA[<p>스트리밍은 Next.js13 App Router와 함께 도입되었으며, 이후 Next.js14 에서는 더욱 최적화되어 서버 액션과 함께 서버에서 데이터를 바로 조작하고 스트리밍 할 수 있도록 개선되었습니다. </p>
<p>이번 글에서는 Next.js 15 버전을 공부하면서 새롭게 알게된 스트리밍에 대해서 정리해보려고 합니다!🙂</p>
<hr>
<h2 id="1️⃣-스트리밍이란">1️⃣ 스트리밍이란?</h2>
<p>스트리밍이란 서버에서 클라이언트로 <strong>매우 큰 용량의 데이터를 보낼 줄 때 데이터를 잘게 쪼개서 보내주는 것</strong>을 말합니다.</p>
<h3 id="기존-ssr-vs-스트리밍-ssr">기존 SSR vs 스트리밍 SSR</h3>
<ul>
<li><strong>기존 SSR (Server Side Rendering)</strong> : 서버에서 모든 데이터를 한꺼번에 준비한 후 클라이언트로 전달</li>
<li><strong>스트리밍 SSR (Streaming SSR)</strong> : 준비된 데이터부터 즉시 전송하여 빠르게 화면 구성</li>
</ul>
<p>기존 SSR 방식은 모든 데이터를 서버에서 처리한 후 한번에 클라이언트로 전송하지만, <strong>스트리밍 기반 SSR은 서버에서 준비된 데이터부터 점진적으로 클라이언트에 전송</strong>하기 때문에 전체 데이터가 다 준비되지 않아도 준비된 부분부터 먼저 화면에 표시가 가능합니다.</p>
<p>결과적으로 사용자는 페이지 로딩을 기다리는 시간이 줄고 빠르게 화면을 볼 수 있게됩니다.</p>
<br />

<h2 id="2️⃣-nextjs의-스트리밍">2️⃣ Next.js의 스트리밍</h2>
<p>Next.js에서는 <strong>페이지 스트리밍</strong>과 <strong>컴포넌트 스트리밍</strong>을 지원합니다.먼저 렌더링된 컴포넌트(동기 작업)를 보여주고, 느리게 렌더링 되는 컴포넌트(비동기 작업)는 대체 UI를 보여주게 됩니다.  </p>
<h3 id="페이지-스트리밍">페이지 스트리밍</h3>
<p>Next.js에서는 <strong>페이지 단위에서 스트리밍을 적용</strong>할 수 있습니다.</p>
<p>예를 들어 <code>page.tsx</code>에서 데이터를 불러오는 작업이 오래 걸리는 경우, 해당 디렉토리에 <code>loading.tsx</code> 파일을 함께 정의하면 해당 페이지가 로딩 중일 때 <code>loading.tsx</code> 에서 지정한 로딩 UI를 먼저 보여줄 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/48f5d5b0-2b06-4805-a680-5c9eb380d337/image.png" alt="loading"></p>
<pre><code class="language-jsx">// page.tsx
export default function Page({
  searchParams,
}: {
  searchParams: { q?: string };
}) {
  return (
    &lt;div&gt;
      &lt;div&gt;test&lt;/div&gt;
      &lt;SearchResult q={searchParams.q || &quot;&quot;} /&gt;
    &lt;/div&gt;
  );
}

// loading.tsx
export default function Loading() {
  return &lt;div&gt;로딩 중...&lt;/div&gt;;
}
</code></pre>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/8b0967fb-e8b4-4d70-80c3-6c4e81677419/image.gif" alt=""></p>
<p>위의 코드에서 SearchResult 컴포넌트가 API 호출을 통해 데이터를 받아와야 한다면, 해당 데이터가 준비될 때까지 아무것도 표시되지 않을 수 있습니다. 이를 해결하기 위해 동일한 경로에 <code>loading.tsx</code> 파일을 추가하면, SearchResult가 로딩되는 동안 대체 UI를 표시할 수 있습니다.</p>
<h3 id="주의할-점">주의할 점</h3>
<p>페이지 스트리밍 시에 몇 가지 주의해야할 점이 있습니다.</p>
<ol>
<li><p><code>loading.tsx</code> 는 현재 경로에 있는 page 컴포넌트 뿐만 아니라 마치 layout 처럼 해당 경로 아래에 있는 <strong>모든 비동기 page 컴포넌트를 스트리밍</strong> 되도록 만든다.</p>
</li>
<li><p>스트리밍은 async가 적용된 비동기 페이지에서만 동작합니다.
→ <code>page.tsx</code>가 <strong>서버 컴포넌트</strong>이어야 합니다.</p>
</li>
<li><p><code>loading.tsx</code>는 <code>page.tsx</code>에만 적용되며, <code>layout.tsx</code>나 일반 컴포넌트에서는 사용할 수 없습니다.</p>
</li>
<li><p>쿼리 스트링 변경 시에는 <code>loading.tsx</code>가 트리거되지 않습니다.
→ <strong>URL의 쿼리 스트링만 바뀌는 경우에는 다시 로딩 상태로 돌아가지 않습니다</strong>. </p>
</li>
</ol>
<p>그렇다면 만약 전체 페이지에 대한 스트리밍을 적용하지 않고, 특정 컴포넌트 혹은 클라이언트 컴포넌트에서 스트리밍하고 싶다면 어떻게 해야할까요? 그때 <strong>Suspense를 사용해서 해결</strong>할 수 있습니다.</p>
<br />

<h2 id="3️⃣-suspense란">3️⃣ Suspense란?</h2>
<p>React의 Suspense는 비동기 컴포넌트가 로딩되는 동안 대체 UI를 제공하는 기능입니다. 즉, 비동기 작업이 완료될 때까지 사용자가 기다리지 않도록, <strong>미리 준비된 UI를 보여주고 이후 데이터를 채워 넣을 수 있도록 도와줍니다.</strong></p>
<h3 id="대체-ui-표시">대체 UI 표시</h3>
<p>Suspense는 <strong>fallback 속성</strong>을 통해 비동기 작업이 끝날 때까지 대체 UI를 보여 줄 수 있습니다.</p>
<h3 id="비동기-작업-완료-후-화면-업데이트">비동기 작업 완료 후 화면 업데이트</h3>
<p>비동기 작업이 끝나게 되면 <strong>Suspense로 감싸고 있던 컴포넌트를 렌더링</strong>하여 보여주게 됩니다.</p>
<br />

<h2 id="4️⃣-suspense--스트리밍-ssr">4️⃣ Suspense + 스트리밍 SSR</h2>
<h3 id="컴포넌트-스트리밍">컴포넌트 스트리밍</h3>
<p>페이지 스트리밍에서 부족했던 부분을 다음과 같은 보완할 수 있습니다.</p>
<pre><code class="language-jsx">
export default function Page({
  searchParams,
}: {
  searchParams: { q?: string };
}) {
  return (
    &lt;div&gt;
      &lt;div&gt;test&lt;/div&gt;
      &lt;Suspense
        key={searchParams.q || &quot;&quot;}
        fallback={&lt;BookListSkeleton count={3} /&gt;}
      &gt;
        &lt;SearchResult q={searchParams.q || &quot;&quot;} /&gt;
      &lt;/Suspense&gt;
    &lt;/div&gt;  
  );
}
</code></pre>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/5d008648-6ea9-456d-bf04-68bb37e98299/image.gif" alt=""></p>
<h4 id="1-쿼리-스트링에-따른-ui-갱신">1. 쿼리 스트링에 따른 UI 갱신</h4>
<p>   페이지 스트리밍에서는 쿼리 스트링 변경 시에는 <code>loading.tsx</code>가 트리거되지 않습니다. 하지만 쿼리 스티링이 바뀌면서 새로 데이터를 요청해야하는 경우가 있습니다. <strong>Suspense에서는 key 속성</strong>을 사용하여 컴포넌트가 리렌더링되도록 할 수 있습니다.</p>
<p>   기본적으로 Suspense 컴포넌트는 최초로 한번 내부 컴포넌트 로딩이 완료된 이후로는 내부의 콘텐츠가 변경되어도 로딩 상태로 돌아가지 않습니다. 따라서 <strong>key 값을 동적으로 설정하여 key값이 변할 때마다 새로운 컴포넌트로 인식하게 하여 매번 로딩</strong> 상태를 보여주도록 합니다.</p>
<h4 id="2-전체-페이지가-아닌-일부분-스트리밍-적용">2. 전체 페이지가 아닌 일부분 스트리밍 적용</h4>
<p>Suspense는 페이지 전체가 아닌 <strong>특정 컴포넌트에만 스트리밍을 적용</strong>할 수 있습니다. 페이지 내 여러 개의 컴포넌트 중 일부만 비동기적으로 데이터를 로딩할 때, 그 부분만 대체 UI를 보여줄 수 있습니다.</p>
<p>위 코드에서 <code>&lt;div&gt;test&lt;/div&gt;</code> 영역은 바로 보여지고, <code>&lt;SearchResult /&gt;</code> 영역만 비동기 작업을 수행하는 동안 대체 UI안 BookListSkeleton를 보여주고 있습니다.</p>
<br />


<blockquote>
<p>📚 <strong>참고</strong>
<a href="https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs?attributionToken=lAHwkwoMCNfvyr4GEIin2Y4BEAEaJDY3ZDc2ZDk2LTAwMDAtMjQ5OS1hMDUzLTI0MDU4ODcxMjIxNCoGODg3NDkzMjiQ97Iwt7eMLcLwnhXUsp0Vjr6dFaOAlyKo5aotx8vzF_fowzDy2e0wn9a3LZruxjD12e0wnNa3LToOZGVmYXVsdF9zZWFyY2hIAVgBYAFoAXoCc2k">한 입 크기로 잘라먹는 Next.js(v15)</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[클로저(Closure) - 자바스크립트에서 동작하는 방식]]></title>
            <link>https://velog.io/@ahn-sujin/%ED%81%B4%EB%A1%9C%EC%A0%80closure-%EA%B0%80%EC%9E%A5-%EC%89%BD%EA%B2%8C-%EC%84%A4%EB%AA%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ahn-sujin/%ED%81%B4%EB%A1%9C%EC%A0%80closure-%EA%B0%80%EC%9E%A5-%EC%89%BD%EA%B2%8C-%EC%84%A4%EB%AA%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 03 Mar 2025 08:18:09 GMT</pubDate>
            <description><![CDATA[<p>최근 면접 준비 하면서 자바스크립트의 개념에 대해서 다시 살펴보고 있는데요. 면접 단골 질문이기도 하면서 자바스크립트의 동작 방식을 이해하기 위해서 필수로 알아야할 클로저에 대해서 정리해보려고 합니다~!</p>
<hr>
<h2 id="1️⃣-클로저란">1️⃣ 클로저란?</h2>
<blockquote>
<p>클로저는 주변 상태(어휘적 환경)에 대한 참조와 함께 묶인 함수의 조합니다. 즉, 클로저는 내부 함수에서 외부 함수의 범위에 대한 접근을 제공합니다. Javascript에서 클로저는 함수 생성 시 함수가 생성될 때마다 생성됩니다. <a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures">출처-MDN</a></p>
</blockquote>
<p>사실 우리는 코딩을 하면서 이미 클로저를 자주 사용해왔습니다. 클로저 구조는 다음과 같습니다.</p>
<pre><code class="language-jsx">function outer() {
  let x = 10;
  function inner() {
    console.log(x); 
  }
  return inner;
}

outer(); // 10
</code></pre>
<p>위 코드의 결과가 왜 10이 나올까요? 코드가 다음과 같이 실행되기 때문입니다.</p>
<ul>
<li>outer 함수 안에는 x라는 변수가 있고, inner라는 내부 함수가 x를 사용합니다.</li>
<li>outer 함수가 실행되면 inner 함수를 반환하는데, 이때 inner 함수는 outer 함수의 변수 x를 기억하고 있습니다.</li>
<li>그래서 closureExample()을 호출하면 inner 함수가 실행되어 x를 출력하게 됩니다.</li>
</ul>
<p>위 처럼 클로저란, 함수 안에 정의된 다른 함수가 외부 함수의 변수에 접근할 수 있게 해주는 구조입니다. 그렇다면 자바스크립트에서는 이런 클로저가 왜 가능한 걸까요?</p>
<br />


<h2 id="2️⃣-자바스크립트에서-클로저">2️⃣ 자바스크립트에서 클로저</h2>
<p>자바스크립트에서 클로저가 가능한 이유는 <strong>렉시컬 환경(Lexical Environment)</strong> 과 <strong>실행 컨텍스트(Execution Context)</strong> 덕분입니다. 자바스크립트는 렉시컬 스코프를 따르기 때문에, 함수가 어디에 선언되었는지에 따라 변수의 유효 범위가 결정됩니다. 이를 기반으로 클로저가 형성됩니다.</p>
<blockquote>
<p><strong>렉시컬 스코프란?</strong>
함수의 선언 위치에 따라 변수의 유효 범위(스코프)가 정해지는 것을 말합니다. 즉, 함수 내에서 참조할 수 있는 변수들은 그 함수가 선언된 위치를 기준으로 결정됩니다.</p>
</blockquote>
<h3 id="렉시컬-환경">렉시컬 환경</h3>
<p>렉시컬 환경이란 <strong>변수나 함수가 어디서 정의되었는지를 기억하고 관리</strong>하는 개념입니다. </p>
<p>렉시컬 환경을 구성하는 두가지 요소가 있습니다.</p>
<ol>
<li><strong>환경 레코드</strong>는 변수와 함수들이 실제로 저장되는 저장소를 말합니다. 함수안에서 선언된 변수나 함수들이 여기에 저장되고 변수의 값을 찾을 때 여기서부터 시작해서 찾게됩니다.</li>
<li><strong>상위 환경에 대한 참조</strong>는 해당 변수를 다른 곳에서 찾도록 해주는 역할을 합니다. 예를 들어 함수 안에서 외부 함수의 변수의 접근하려면 상위 환경을 찾아야하는데 이 때 이 참조가 필요하게 됩니다.</li>
</ol>
<h3 id="실행-컨텍스트">실행 컨텍스트</h3>
<p><strong>실행 컨텍스트는 자바스크립트 코드가 실행될 때 생성되는 실행 환경을 의미</strong> 합니다. 즉, 자바스크립트 엔진이 코드를 해석하고 실행하는 동안의 환경을 말하며 함수 호출이나 코드 블록이 실행될 때 새로운 실행 컨텍스트를 생성합니다.</p>
<p>실행 컨텍스트의 구성 요소는 크게 렉시컬 환경, 변수 객체, this 값이 있습니다.</p>
<ol>
<li><strong>렉시컬 환경</strong>은 변수나 함수가 어디서 선언 되었는지를 기억하고 저장합니다.</li>
<li><strong>변수 객체</strong>는 변수와 함수 선언이 포함된 객체로 함수 내에서 사용되는 변수들이 저장됩니다.</li>
<li><strong>this 값</strong>은 함수가 어디서 실행되었는지에 따라서 동적으로 결정됩니다.</li>
</ol>
<p>정리하자면, </p>
<ul>
<li>렉시컬 환경을 통해 변수와 함수 선언이 어디서 정의되었는지를 추적하고 변수의 스코프를 결정합니다. </li>
<li>실행 컨텍스트는 코드 실행 시 생성되는 실행 환경으로 렉시컬 환경을 포함하고 있어 함수가 실행될 때마다 변수에 접근할 수 있도록 도와줍니다.</li>
</ul>
<p>따라서, 렉시컬 환경을 통해 변수와 함수의 선언 위치를 추적하고 이를 바탕으로 실행 컨텍스트 내에서 변수에 접근 할 수 있도록 도와주기 때문에 클로저가 가능하게 됩니다.</p>
<br />


<h2 id="3️⃣-클로저-활용-예시">3️⃣ 클로저 활용 예시</h2>
<p>클로저는 다음과 같은 일을 가능하게 합니다.</p>
<h3 id="데이터의-은닉화">데이터의 은닉화</h3>
<p>데이터의 은닉화는 <strong>외부에서 특정 데이터를 직접 수정하지 못하도록 하고, 그 데이터를 수정하는 메서드만을 제공</strong>하는 방식입니다. 이를 통해 중요한 데이터를 보호하고, 예기치 않은 변경을 방지할 수 있습니다.</p>
<pre><code class="language-jsx">function createCounter() {
  let count = 0; // 외부에서는 접근할 수 없는 변수
  return {
    increment: function() {
      count++;
      console.log(count);
    },
    decrement: function() {
      count--;
      console.log(count);
    },
    getCount: function() {
      return count;
    }
  };
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.decrement(); // 1
console.log(counter.getCount()); // 1
</code></pre>
<p>이 예시에서 count 변수는 createCounter 함수 내부에 정의되어 있으며, 외부에서 직접 접근할 수 없습니다. 대신, 클로저를 통해 반환된 메서드(increment, decrement, getCount)만 사용하여 값을 변경하거나 조회할 수 있습니다. 이렇게 데이터를 은닉하고 외부에서의 직접적인 접근을 차단하는 방식으로 데이터의 은닉화가 이루어집니다.</p>
<h3 id="함수형-프로그래밍">함수형 프로그래밍</h3>
<p>함수형 프로그래밍은 함수를 일급 객체로 취급하며, <strong>함수들이 다른 함수를 반환하거나 인자로 받을 수 있는 특징</strong>이 있습니다. 클로저는 이러한 함수형 프로그래밍에서 유용하게 사용됩니다. 특히, 내부 함수가 외부 상태에 의존하면서 그 상태를 기억하는 데 클로저가 사용됩니다.</p>
<pre><code class="language-jsx">function multiply(factor) {
  return function(number) {
    return number * factor;
  };
}

const multiplyBy2 = multiply(2);
const multiplyBy3 = multiply(3);

console.log(multiplyBy2(5)); // 10
console.log(multiplyBy3(5)); // 15</code></pre>
<p>위 코드에서 multiply 함수는 내부에서 또 다른 함수를 반환합니다. 이때 반환된 함수는 factor 변수에 접근할 수 있는데, 이는 클로저 덕분입니다. 클로저를 사용하면 외부 함수의 인자 값을 기억하는 고차 함수를 쉽게 구현할 수 있으며, 이러한 방식은 함수형 프로그래밍에서 자주 사용됩니다</p>
<h3 id="콜백-함수-및-비동기-작업">콜백 함수 및 비동기 작업</h3>
<p>클로저는 비동기 작업에서 상태를 유지하는 데 유용하게 사용됩니다. 비<strong>동기 작업이 완료된 후 콜백 함수가 실행될 때 외부 함수의 변수에 접근</strong>할 수 있습니다.</p>
<pre><code class="language-jsx">function fetchData(url) {
  let data = null;
  return new Promise((resolve, reject) =&gt; {
    setTimeout(() =&gt; {
      data = `Fetched data from ${url}`;
      resolve(data);
    }, 1000);
  });
}

function displayData() {
  fetchData(&#39;https://api.example.com&#39;).then(data =&gt; {
    console.log(data); // &quot;Fetched data from https://api.example.com&quot;
  });
}

displayData();
</code></pre>
<p>위 코드에서 fetchData 함수는 비동기적으로 데이터를 가져오는 작업을 합니다. 데이터가 가져와지면, then 안의 콜백 함수에서 data를 사용할 수 있습니다. 이때 data는 fetchData 함수 내에서 정의된 변수입니다. <strong>비록 비동기 작업이 완료되었지만, 클로저 덕분에 비동기 작업 후에도 data에 접근</strong>할 수 있는 것입니다. 이처럼 클로저는 비동기 작업에서 중요한 역할을 하며, 작업이 끝난 후에도 데이터를 안전하게 사용할 수 있게 합니다.</p>
<br />

<h2 id="🙌🏻-마무리">🙌🏻 마무리</h2>
<p>사실 클로저는 새로운 개념은 아니고 이미 알고 있고 사용하는 개념이었습니다. 클로저에 대해 정확히 정의하고, 자바스크립트에서 왜 가능한지를 이해함으로써 자바스크립트의 동작 원리를 더욱 깊이 이해할 수 있었습니다. 특히 그동안 통신할 때 사용했던 비동기 처리 방식을 이해하는데 큰 도움이 됐습니다. 원리를 이해하고 사용하는 것의 중요성을 다시 한번 느끼면서 20000... </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[enum을 사용하고 계신가요?]]></title>
            <link>https://velog.io/@ahn-sujin/enum-%EC%9D%84-%EC%95%84%EC%8B%9C%EB%82%98%EC%9A%94</link>
            <guid>https://velog.io/@ahn-sujin/enum-%EC%9D%84-%EC%95%84%EC%8B%9C%EB%82%98%EC%9A%94</guid>
            <pubDate>Mon, 24 Feb 2025 11:57:19 GMT</pubDate>
            <description><![CDATA[<p>타입스크립트를 사용해보셨다면 enum에 대해서 한번쯤 사용해 보셨을텐데요. 혹시 <em>&quot;enum 사용을 지양해야한다&quot;</em> 는 말을 들어보신 적이 있으신가요?  저는 예전 면접에서 처음 들었었는데요. 혹시 저처럼 처음들어 보시는 분들을 위해 enum 사용을 왜 조심히 해야하는 것인지에 대해 정리해 보려고 합니다.</p>
<hr>
<br />

<h2 id="1️⃣-enum-이란">1️⃣ enum 이란?</h2>
<p>enum은 열거형(enumeration)의 줄임말로, <strong>연관된 상수들을 하나의 그룹으로 묶어 표현</strong>하는 TypeScript의 기능입니다. 이를 통해 코드의 가독성과 유지보수성을 높일 수 있습니다.</p>
<pre><code class="language-tsx">const user1 = {
    name: &quot;철수&quot;,
    role: 0 
}

const user2 = {
    name: &quot;영희&quot;,
    role: 1 
}

const user3 = {
    name: &quot;민수&quot;,
    role: 2
}</code></pre>
<p>위의 예시처럼 역할에 숫자 값을 부여했을 때 <code>0,1,2</code>만 보고 어떤 의미인지 바로 알기가 어렵습니다. 이런 경우 enum을 통해 개선할 수 있습니다.</p>
<pre><code class="language-tsx">enum Role {
    ADMIN = 0,
    USER = 1,
    GUEST = 2
}

const user1 = {
    name: &quot;철수&quot;,
    role: Role.ADMIN 
}

const user2 = {
    name: &quot;영희&quot;,
    role: Role.USER  
}

const user3 = {
    name: &quot;민수&quot;,
    role: Role.GUEST 
}
</code></pre>
<p>enum을 활용하여 <strong>역할을 정의</strong>하고 그 <strong>의미를 명확</strong>하게 할 수 있습니다. 또한, <code>0</code> 대신 <code>Role.ADMIN</code> 을 사용함으로써 <strong>코드의 가독성</strong>을 올릴 수 있으며 <strong>오타도 방지</strong>할 수 있습니다.</p>
<h3 id="enum의-자동-할당">enum의 자동 할당</h3>
<p>enum은 값을 명시적으로 할당하지 않으면 자동으로 0부터 시작해서 순차적으로 값을 할당합니다.</p>
<pre><code class="language-tsx">enum Role {
    ADMIN,  // 0
    USER,   // 1
    GUEST   // 2
}
</code></pre>
<p>또한, 특정 값에서 시작하도록 지정할 수도 있습니다.</p>
<pre><code class="language-tsx">enum Role {
    ADMIN = 9,  // 9
    USER = 10,  // 10
    GUEST       // 11
}
</code></pre>
<blockquote>
<p><strong>enum과 union의 차이점</strong></p>
</blockquote>
<ul>
<li>enum은 연관된 상수들을 하나의 그룹을 묶어 관리합니다.<pre><code class="language-tsx">    enum Role {
      ADMIN = 0,
      USER = 1,
      GUEST = 2
  }</code></pre>
</li>
<li>union은 특정 변수나 속성이 여러 개의 정해진 값 중 하나를 가집니다.<pre><code class="language-tsx">  type Role = &quot;ADMIN&quot; | &quot;USER&quot; | &quot;GUEST&quot;;</code></pre>
</li>
<li>즉, <strong>enum</strong>은 하나의 객체처럼 동작하며 <strong>특정 값에 접근</strong>할 수 잇지만 <strong>union</strong>은 값들의 집합을 나타내는 타입이며 <strong>값들의 범위</strong>를 제한하는 역할을 합니다. </li>
</ul>
<br />

<h2 id="2️⃣-enum-의-취약점">2️⃣ enum 의 취약점</h2>
<p>위 처럼 enum은 편리한 기능을 제공하지만, 몇 가지 단점이 존재합니다.</p>
<h3 id="불필요한-런타임-코드-생성">불필요한 런타임 코드 생성</h3>
<p>enum은 컴파일된 Javascript 코드에서 객체로 변환되어 추가적인 코드가 생성됩니다. </p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/cc8db86f-87db-442b-97d9-9e718ae9a63f/image.png" alt="ASIS"></p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/90efb013-9dab-4f0d-a36d-54edbee33a5d/image.png" alt="TOBE"></p>
<p>이러한 추가 코드는 <strong>번들 크기를 증가</strong>시키고 최적화에 불리할 수 있습니다. </p>
<h3 id="three-shaking의-어려움">Three Shaking의 어려움</h3>
<p><strong>Three Shaking</strong>은 <strong>사용하지 않는 코드를 제거</strong>하여 번들 크기를 줄이는 최적화 기법입니다. 그러나 <strong>enum은 실제로 런타임에 객체로 변환</strong>되기 때문에 이를 잘라내기 어렵습니다.</p>
<p><strong>enum의 값은 컴파일된 후 객체 형태로 존재하고, 해당 객체를 참조하는 코드가 존재</strong>할 수 있기 때문에, tree shaking이 제대로 동작하지 않습니다. 이로 인해 사용하지 않는 enum 값들이 최종 번들에 포함되어 최적화가 어려워집니다.</p>
<h3 id="타입-안정성-문제">타입 안정성 문제</h3>
<p>enum은 숫자 값을 기본으로 하기 때문에, <strong>역방향 매핑</strong> 을 지원합니다. 따라서 의도치 않은 값이 할당되는 문제가 발생할 수 있습니다.</p>
<pre><code class="language-tsx">enum Status {
  Loading,
  Success,
  Error,
}

let statusName: string = Status[1]; // &quot;Success&quot;
let statusIndex: number = Status[&quot;Success&quot;]; // 1
</code></pre>
<br />

<h2 id="3️⃣-그렇다면-어떻게-사용해야-할까">3️⃣ 그렇다면 어떻게 사용해야 할까?</h2>
<h3 id="as-const">as const</h3>
<p><a href="https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums">타입스크립트 공식 문서</a>에서는 enum 대신 <code>as const</code> 사용을 지향하고 있습니다.</p>
<p><code>as const</code> 는 TypeScript에서 객체나 배열을 <strong>리터럴 타입</strong>으로 고정할 수 있게 해줍니다. </p>
<blockquote>
<p><strong>리터럴 타입이란?</strong> 
변경 불가능한 값을 나타내는 타입입니다. 예를 들어, <code>&quot;loading&quot;</code>, <code>&quot;success&quot;</code>, <code>&quot;error&quot;</code> 같은 값을 리터럴 타입으로 지정하면, <strong>해당 값만 허용되고 다른 값은 타입 오류</strong>가 발생합니다. 이를 통해 값의 범위를 제한하고 예상치 못한 값을 방지 할 수 있습니다.</p>
</blockquote>
<p>따라서, <code>as const</code> 를 사용하면 값이 변경되지 않도록 고정되고, 타입은 해당 값에 정확히 맞춰지기 때문에 의도치 않은 값 할당을 방지할 수 있습니다.</p>
<pre><code class="language-tsx">const Status = {
  Loading: &quot;loading&quot;,
  Success: &quot;success&quot;,
  Error: &quot;error&quot;,
} as const;

type Status = typeof Status[keyof typeof Status];

let status: Status = &quot;loading&quot;; // 정상
status = &quot;completed&quot;; // 에러 발생
</code></pre>
<p>위 예시에서 Status는 <code>as const</code> 로 리터럴 타입으로 고정되었고, 그 결과 Status 타입은 <code>&quot;loading&quot;</code>, <code>&quot;success&quot;</code>, <code>&quot;error&quot;</code>로 제한됩니다. 이렇게 하면 원하는 기능을 사용하면서 enum에서 발생할 수 있는 여러 문제들을 피할 수 있게 됩니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js(v15) 에서 데이터 캐시하기]]></title>
            <link>https://velog.io/@ahn-sujin/Next.jsv15-%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%BA%90%EC%8B%9C</link>
            <guid>https://velog.io/@ahn-sujin/Next.jsv15-%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%BA%90%EC%8B%9C</guid>
            <pubDate>Fri, 14 Feb 2025 08:32:00 GMT</pubDate>
            <description><![CDATA[<p>고백하자면 저는 next.js13 버전만 사용해봤습니다.😅 14버전이 나왔을 때도 공부해야지 공부해야지 미루기만 하고 최근에서야 앱라우터 와 서버 컴포넌트를 사용하는 중입니다...ㅎㅎ </p>
<p>막상 사용해보니 이렇게 효율적일 수가 없더라구요 진작에 공부해서 써볼껄 이라는 후회가 들었습니다. next.js 15버전을 공부하던 중에 가장 인상 깊었던 것은 캐시 기능이었고 새로워진 next.js는 같은 일 계속 시키는거 정말 싫어하는 구나라고 느꼈습니다.</p>
<p>그래서 오늘은 next.js의 데이터 캐시하는 방법에 대해서 정리해보려고 합니다~!</p>
<hr>
<h2 id="1️⃣-데이터-페칭">1️⃣ 데이터 페칭</h2>
<p>먼저, 데이터 페칭에 대해서 어떻게 이뤄지는지 간략히 알아보도록 하겠습니다.</p>
<h3 id="page-router-버전에서-데이터-페칭">Page Router 버전에서 데이터 페칭</h3>
<ul>
<li>페이지 파일에서 <strong>SSR, SSG, Dynamic SSG</strong>를 통해 서버에서 데이터를 가져와서 page에 props로 데이터를 전달합니다.</li>
<li>모든 컴포넌트가 클라이언트 컴포넌트로 동작합니다. (= 컴포넌트가 서버와 클라이언트에서 모두 실행된다는 것을 의미합니다.)</li>
<li>최상단에서 데이터를 필요로 하는 페이지까지 props로 계속 전달해줘합니다. (props drilling 문제)</li>
</ul>
<h3 id="app-router-버전에서-데이터-페칭">App Router 버전에서 데이터 페칭</h3>
<ul>
<li>서버 컴포넌트에서 <code>async - await</code> 키워드와 <code>fetch</code> 메서드를 활용해서 데이터를 직접 불러오는 데이터 페칭 로직을 만들 수 있습니다.</li>
</ul>
<pre><code class="language-tsx">     export async function Page(props){
         const data = await fetch(&#39;...&#39;);

         return &lt;div&gt;...&lt;/div&gt;
    }</code></pre>
<ul>
<li>하지만 클라이언트 컴포넌트에서는 <code>async - await</code> 키워드 를 사용할 수 없습니다. </li>
<li>데이터가 필요한 곳에서 <strong>직접 불러오는 것</strong>을 권장합니다.</li>
</ul>
<blockquote>
<p>🧐 <strong>async - await 키워드</strong> <strong>사용이  서버 컴포넌트에서는 되지만, 클라이언트 컴포넌트에서는 안되는 이유</strong></p>
</blockquote>
<ul>
<li><strong>서버 컴포넌트</strong>는 서버에서 데이터를 미리 가져와서 HTML을 렌더링할 수 있기 때문입니다. 서버 측에서는 비동기 작업이 완료된 후 렌더링을 처리하므로, 비동기 작업을 문제없이 사용할 수 있습니다.</li>
<li>반면, <strong>클라이언트 컴포넌트</strong>는 브라우저에서 실행되며, 랜더링 시에 비동기 작업을 바로 처리할 수 없기 때문에 제대로 렌더링이 이루어지지 않을 수 있습니다.</li>
</ul>
<blockquote>
<p>☝🏻 <strong>주의사항</strong>
데이터 패칭을 위한 환경 변수를 설정할 때 .env 파일에서 <strong><code>NEXT_PUBLIC</code> 이라는 접두사</strong>를 붙이지 않으면 next 는 자동으로 해당 환경변수를 서버측에서만 접근 가능한 것으로 인식하여 해당 환경변수에 대해 클라언트 컴포넌트에서는 접근이 불가능합니다! </p>
</blockquote>
<br />


<h2 id="2️⃣-데이터-캐시">2️⃣ 데이터 캐시</h2>
<h3 id="nextjs에서-데이터-캐시란">Next.js에서 데이터 캐시란</h3>
<ul>
<li><strong>fetch 메서드</strong>를 활용해 불러운 데이터를 Next 서버에서 보관하는 기능을 말합니다.</li>
<li><strong>영구적</strong>으로 데이터를 보관하거나, 특정 시간을 주기로 <strong>갱신</strong> 시키는 것도 가능합니다.</li>
<li>캐시의 목적은 <strong>불필요한 데이터 요청의 수를 줄여</strong>서 웹 서비스의 <strong>성능</strong>을 개선하는 것이라고 할 수 있습니다.</li>
</ul>
<h3 id="캐시-옵션">캐시 옵션</h3>
<blockquote>
<p>📢 <strong>Next 서버에서 발생하는 데이터 패칭을 로그로 남기는 설정</strong></p>
</blockquote>
<pre><code class="language-tsx">  import type { NextConfig } from &quot;next&quot;;
  const nextConfig: NextConfig = {
        logging: {
            fetches: {
                fullUrl: true,    
            },
        },
    };
  export default nextConfig;</code></pre>
<ul>
<li>next.config.ts 파일을 위와 같으 설정하면 터미널에서 <strong>데이터 패칭에 대한 로그</strong>를 확인할 수 있습니다!
<img src="https://velog.velcdn.com/images/ahn-sujin/post/bc6f5d06-4991-47b0-8d43-97c2a7546bee/image.png" alt=""></li>
</ul>
<pre><code class="language-tsx">const response = await fetch(&#39;~/api&#39;, {cache: &quot;force-cache});</code></pre>
<ul>
<li><code>{cache: &quot;force-cache&quot;}</code><ul>
<li>요청의 결과를 <strong>무조건 캐싱</strong>합니다.</li>
<li>한번 호출 된 이후에는 <strong>다시는 호출되지 않습니다.</strong></li>
</ul>
</li>
<li><code>{cache: &quot;no-store&quot;}</code> <ul>
<li>기본값으로, 데이터 페칭의 결과를 저장하지 않습니다.</li>
<li>캐싱을 아예 하지 않아 매번 데이터를 요청합니다. </li>
</ul>
</li>
<li><code>{next: {revalidate: 3}}</code><ul>
<li>특정 시간을 주기로 캐시를 업데이트합니다.</li>
<li>마치 Page Router의 ISR방식과 비슷하다고 생각하면 됩니다.</li>
</ul>
</li>
<li><code>{next: {tags: [&#39;a&#39;]}}</code><ul>
<li>on-Demand Revalidate</li>
<li>요청이 들어왔을 때 데이터를 최신화하는 방식입니다.</li>
</ul>
</li>
</ul>
<h3 id="예시">예시</h3>
<p>책 리스트 불러오는 API로 예시를 살펴보도록 하겠습니다!</p>
<pre><code class="language-tsx">async function AllBooks() {
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_API_SERVER_URL}/book`,
    { cache: &quot;no-store&quot; } // 이 부분을 원하는 캐시 옵션으로 설정합니다!
  );
  if (!response.ok) {
    return &lt;div&gt;오류가 발생했습니다...&lt;/div&gt;;
  }
  const allBooks: BookData[] = await response.json();

  return (
    &lt;div&gt;
      {allBooks.map((book) =&gt; (
        &lt;BookItem key={book.id} {...book} /&gt;
      ))}
    &lt;/div&gt;
  );
}

export default function Home() {
  return (
    &lt;div&gt;
      &lt;section&gt;
        &lt;h3&gt;등록된 모든 도서&lt;/h3&gt;
        &lt;AllBooks /&gt;
      &lt;/section&gt;
    &lt;/div&gt;
  );
}</code></pre>
<ul>
<li><p><code>{cache: &quot;force-cache&quot;}</code> 로 설정했을 때
<img src="https://velog.velcdn.com/images/ahn-sujin/post/1e912494-a5c0-4d83-9ba2-15f99b4664f0/image.png" alt=""></p>
</li>
<li><p><code>{cache: &quot;no-store&quot;}</code> 로 설정했을 때
<img src="https://velog.velcdn.com/images/ahn-sujin/post/0d37895c-34ec-400d-9ff4-6faea805b4b7/image.png" alt=""></p>
</li>
</ul>
<br />

<h2 id="3️⃣-리퀘스트-메모이제이션">3️⃣ 리퀘스트 메모이제이션</h2>
<p>마지막으로 리퀘스트 메모이제이션이란 <em>&quot;요청을 기억한다.&quot;</em> 라는 뜻으로, <strong>중복된 요청이 발생하지 않도록 캐싱</strong>하는 것을 말합니다. 그렇다면 여기서 <em>&quot;데이터 캐시랑 뭐가 다른거지?&quot;</em> 라는 의문을 생길 수 있습니다. </p>
<h3 id="✋🏻-데이터-캐시-vs-리퀘스트-메모이제이션">✋🏻 데이터 캐시 VS 리퀘스트 메모이제이션</h3>
<ul>
<li><p><strong>데이터 케시</strong>는 <strong>백엔드 서버로부터 불러온 데이터를 거의 영구적으로 보관</strong>하기 위해 사용됩니다. 따라서 서버 가동중에는 영구적으로 보관됩니다.</p>
</li>
<li><p><strong>리퀘스트 메모이제이션</strong>은 <strong>하나의 페이지</strong>를 렌더링 하는 동안에 <strong>중복된 API 요청을 캐싱</strong>하기 위해 존재합니다. 따라서, 랜더링이 종료되면 모든 캐시가 소멸됩니다.</p>
</li>
</ul>
<p>결론적으로 데이터 캐시는 지속적으로 유지되는 반면, 리퀘스트 메모이제이션은 렌더링 동안에만 유지된다고 정리할 수 있습니다.</p>
<h3 id="🤷🏻♀️-리퀘스트-메모이제이션의-등장-배경">🤷🏻‍♀️ 리퀘스트 메모이제이션의 등장 배경</h3>
<p>리퀘스트 메모이제이션이 필요한 이유는 <strong>서버 컴포넌트의 도입</strong> 때문인데요 하나의 페이지에서 컴포넌트가 각각 자신이 필요한 데이터를 페칭하기 때문에 한 페이지에서 <strong>서로 다른 컴포넌트에서 동일한 API를 요청하는 경우가 발생</strong>할 수 있기 때문입니다. </p>
<p>그리고 리퀘스트 메모이제이션은 Next.js에서 자동으로 제공하는 기능으로 별도의 설정은 필요하지 않습니다!</p>
<br />


<blockquote>
<p>📚 <strong>참고</strong>
<a href="https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs">한 입 크기로 잘라먹는 next.js</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Styled-Components 와 TailwindCSS의 차이점]]></title>
            <link>https://velog.io/@ahn-sujin/%EC%A0%9C%EB%A1%9C-%EB%9F%B0%ED%83%80%EC%9E%84-CSS-vs-CSS-in-JS</link>
            <guid>https://velog.io/@ahn-sujin/%EC%A0%9C%EB%A1%9C-%EB%9F%B0%ED%83%80%EC%9E%84-CSS-vs-CSS-in-JS</guid>
            <pubDate>Fri, 07 Feb 2025 09:33:24 GMT</pubDate>
            <description><![CDATA[<p>최근 <strong>제로 런타임 css</strong>라는 개념에 대해서 알게되었는데요. 사실 개념은 알고 있었지만 용어를 몰랐던거였습니다ㅎㅎ css-in-js와 비교해서 알아두면 좋을 것 같아 정리해보려고 합니다!!</p>
<hr>
<h2 id="1️⃣-css-in-js">1️⃣ CSS-in-JS</h2>
<p>CSS-in-JS는 자바스크립트 코드내에서 CSS를 작성하고, 이를 <strong>런타임에 동적으로 생성</strong>하여 DOM에 삽입하는 방식을 말합니다. 대표적인 라이브러리로는 <strong>styled-components와 Emotion</strong> 이 있습니다.</p>
<h3 id="랜더링-과정">랜더링 과정</h3>
<p>CSS-in-JS의 경우 브라우저 랜더링 과정을 간략히 나타내면 다음과 같습니다.</p>
<blockquote>
</blockquote>
<p><strong>1. HTML 파싱 및 DOM 생성</strong>
브라우저는 HTML을 파싱하고 DOM을 생성합니다.
<strong>2. JS 실행</strong>
자바스크립트가 실행되면서 CSS-in-JS 라이브러리가 스타일을 동적으로 생성합니다.
<strong>3. CSS 파싱 및 CSSOM 생성</strong>
자바스크립트가 실행되면서 CSS 코드가 DOM에 삽입되고, 브라우저는 이를 파싱하여 CSSOM을 생성합니다.
<strong>4. 랜더 트리 생성</strong>
CSSOM과 DOM을 결합한 렌더 트리를 생성하고, 화면에 콘테츠가 렌더링 됩니다.</p>
<h3 id="styled-components">Styled-Components</h3>
<pre><code class="language-jsx">import styled from &#39;styled-components&#39;;

const Button = styled.button`
  background-color: ${props =&gt; (props.primary ? &#39;blue&#39; : &#39;gray&#39;)};
  color: white;
  padding: 10px;
  border-radius: 5px;
`;

function App() {
  return &lt;Button primary={true}&gt;Click me&lt;/Button&gt;;
}
</code></pre>
<ul>
<li><strong>동적 스타일링 지원</strong>
자바스크립트의 로직을 활용하여 동적으로 스타일을 계산할 수 있습니다.</li>
<li><strong>컴포넌트 기반</strong>
스타일을 컴포넌트와 함께 관리할 수 있어 코드가 더 모듈화되고 관리하기 쉬워집니다.</li>
<li><strong>자동 고유 클래스 이름 생성</strong>
Styled-components는 각 스타일을 컴포넌트와 연결하고, 이를 고유한 클래스 이름으로 변환하여, 다른 컴포넌트와 스타일 충돌을 방지합니다.</li>
</ul>
<h3 id="문제점">문제점</h3>
<p>CSS-in-JS를 SSR 환경에서 사용한다면, 스타일이 초기 렌더링에 누락되는 문제가 발생할 수 있습니다.</p>
<p>SSR 환경에서는 <strong>서버에서 HTML을 미리 렌더링</strong>해서 클라이언트로 전달하는데요. <strong>CSS-in-JS방식에서는 스타일이 JS코드에서 동적으로 생성</strong>되기 때문에, 서버 측에서 스타일을 미리 계산해서 HTML에 삽입하지 않으면, <strong>클라이언트에서 JS가 실행되기 전까지 스타일이 적용되지 않습니다.</strong> </p>
<p>이로 인해, 클라이언트에서 HTML을 로드한 후, JS가 실행될 때까지 스타일이 적용되지 않은 HTML 페이지가 나타날 수 있습니다.</p>
<blockquote>
<p>해결 방법이 궁금하다면 <strong>블로그</strong>🔽 를 참고해주세요
<a href="https://velog.io/@ahn-sujin/Next.js-CSS-in-JS-%EC%82%AC%EC%9A%A9%EC%8B%9C-HTML%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%8B%9C-%EC%8A%A4%ED%83%80%EC%9D%BC%EC%9D%B4-%EC%A0%81%EC%9A%A9%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C">[Next.js] CSS in JS 사용시 HTML렌더링 시 스타일이 적용되지 않는 문제</a></p>
</blockquote>
<br />

<h2 id="2️⃣-제로-런타임-css">2️⃣ 제로 런타임 CSS</h2>
<p>제로 런타임 CSS는 스타일을 <strong>미리 빌드 타임에 생성</strong>하여, 런타임에 자바스크립트가 실행되기 전에 스타일이 완전히 준비된 상태로 제공되는 방식입니다. 대표적으로 <strong>Sass, CSS Modules, Vanilla Extract, TailwindCSS</strong> 제로 런타임 CSS에 해당합니다.</p>
<h3 id="랜더링-과정-1">랜더링 과정</h3>
<p>제로 런타임 CSS의 경우 브라우저 랜더링 과정을 간략히 나타내면 다음과 같습니다.</p>
<blockquote>
</blockquote>
<p><strong>1. HTML 파싱 및 DOM 생성</strong>
브라우저는 HTML을 파싱하고 DOM을 생성합니다.
<strong>2. CSS 파일 로드 및 CSSOM 생성</strong>
빌드 타임에 컴파일된 CSS 파일을 브라우저가 로드하고, 이를 바탕으로 CSSOM을 생성합니다.
<strong>3. 렌더 트리 생성</strong>
DOM과 CSSOM을 결합하여 렌더 트리를 생성합니다.
<strong>4. 레이아웃 계산 및 페인팅</strong>
렌더 트리를 바탕으로 레이아웃을 계산하고, 각 요소를 화면에 그립니다.</p>
<h3 id="tailwind-css">Tailwind CSS</h3>
<pre><code class="language-tsx">// TailwindCSS 사용 예시 

&lt;button className=&quot;bg-blue-500 text-white p-[10px] rounded-[5px]&quot;&gt;
  Click me
&lt;/button&gt;</code></pre>
<ul>
<li><strong>빌드 타임 스타일 생성</strong>
CSS를 빌드 타임에 미리 생성하여, 런타임에서 추가적인 스타일 계산이 필요하지 않습니다.(JS 실행 없이도 브라우저가 CSS를 바로 적용 가능)</li>
<li><strong>유틸리티 클래스 기반 스타일링</strong>
미리 정의된 유틸리티 클래스를 조합하여 스타일을 적용할 수 있어 빠른 개발이 가능합니다. </li>
<li><strong>최적화된 번들 크기</strong>
사용하지 않는 CSS를 빌드 타임에 제거하기 때문에 최종 CSS크기를 최소화할 수 있습니다.(렌더링 성능 최적화)</li>
</ul>
<h3 id="문제점-1">문제점</h3>
<p>동적 스타일링 관리하기가 어렵고, 조건이나 스타일이 복잡해지면 코드가 점점 길어져 가독성에도 불편함을 줄 수 있습니다.</p>
<p>(실제 사용 코드 예시🔽)
<img src="https://velog.velcdn.com/images/ahn-sujin/post/d246c74a-1c5c-4410-82b1-8ed9776b49a6/image.png" alt="예시"></p>
<br />

<h2 id="3️⃣-결론">3️⃣ 결론</h2>
<h3 id="css-in-js와-제로-런타임-css의-차이점-정리">CSS-in-JS와 제로 런타임 CSS의 차이점 정리</h3>
<p> 특징 | CSS-in-JS | 제로 런타임 CSS |
|:---|:---|:---|
| 스타일 준비 시점 | 런타임에 자바스크립트 실행 시 동적으로 스타일 생성 | 빌드타임에 스타일을 미리 계산하고 정적 CSS로 제공 |
| 성능 | 초기 렌더링 시 스타일 적용이 늦어질 수 있으며, 런타임에 스타일 계산이 필요 | 빠른 초기 렌더링으로 스타일이 즉시 적용됨 |
| 유연성 | 자바스크립트 변수나 로직을 사용한 동적 스타일링 가능 | 동적 스타일링에 제한이 있음 |
| 스타일 관리 |     컴포넌트와 스타일을 함께 관리 가능 | CSS 파일과 스타일을 별도로 관리하고 최적화 가능 |
| 장점 | 컴포넌트화된 스타일링, 동적 스타일링 가능 | 빠른 렌더링, 스타일 최적화 가능, 전통적인 CSS 방식 호환 |
| 단점 | 성능 저하, FOUC 가능성, 런타임 의존 | 동적 스타일링에 유연성 부족, 스타일 코드 중복 가능성 |</p>
<p>두개의 차이를 간단히 아래와 같이 정리할 수 있을 것 같습니다.</p>
<blockquote>
<p><strong>CSS-in-JS</strong>는 동적이고 유연한 스타일링을 제공하지만, 초기 렌더링 성능에서 다소 불리할 수 있다.
<strong>제로 런타임 CSS</strong>는 빠른 렌더링을 가능하게 하지만, 동적 스타일링에는 한계가 있다. </p>
</blockquote>
<p>둘중에 뭐가 더 좋다 라기 보다는 각 방식의 장단점을 고려해서 사용하는 프로젝트와 요구사항에 맞는 방법을 선택하는게 좋을 것 같습니다! 👍🏻</p>
<br />

<blockquote>
<p><strong>참고</strong>
<a href="https://ttaerrim.tistory.com/64">[블로그] Zero RunTime CSS-in-JS에 대해 알아보자</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[<img>말고 Next/Image 를 사용해야하는 이유]]></title>
            <link>https://velog.io/@ahn-sujin/img%EB%A7%90%EA%B3%A0-NextImage-%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@ahn-sujin/img%EB%A7%90%EA%B3%A0-NextImage-%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Tue, 21 Jan 2025 12:48:06 GMT</pubDate>
            <description><![CDATA[<p>Next.js에서 이미지를 불러올 때 대부분<code>&lt;img&gt;</code> 태그가 아닌 Next/Image 컴포넌트를 사용합니다. 보기에는 다른 점이 없는데 왜 굳이 귀찮게 Next/Image 컴포넌트를 사용해야 할까요? 이번 글에서는 <code>&lt;img&gt;</code>와 Next/Image의 차이점, 그리고 Next/Image를 사용하며 느낀 장단점에 대해 정리해 보겠습니다.</p>
<hr>
<p>먼저 Next/Image가 웹 성능 최적화를 위해 사용된다는 점은 알고 계실 텐데요, 그렇다면 <strong>웹 성능 최적화란</strong> 무엇인지, 그리고 <strong>이미지가 웹 성능과 왜 연관이 있는</strong>지 간단히 짚고 넘어가겠습니다!</p>
<h2 id="웹-성능-최적화란">웹 성능 최적화란?</h2>
<p>웹 성능 최적화는 사용자가 웹사이트에 방문했을 때 최대한 빠르고 원활하게 페이지를 로드하고 인터렉션이 자연스럽게 개선하는 과정을 말합니다. 속도가 빨라야 사용자가 서비스를 이용하는데 불편함이 없을 것고 사용자가 많아야 SEO에도 더 좋겠죠?</p>
<h3 id="이미지-최적화">이미지 최적화</h3>
<p>웹 성능 최적화를 위해서는 이미지 최적화를 뺴놓을 수 없습니다. 이유는 아래와 같습니다.</p>
<h4 id="1-이미지-차지-비중">1. 이미지 차지 비중</h4>
<ul>
<li>일반적으로 웹 페이지의 50~80% 용량이 이미지입니다.</li>
<li>이미지 최적화를 통해 페이지 크기를 줄이면 로딩 속도를 획기적으로 개선할 수 있습니다.</li>
</ul>
<h4 id="2-네트워크-비용-절감">2. 네트워크 비용 절감</h4>
<ul>
<li>최적화된 이미지는 더 작은 크기로 전송되므로, 사용자 데이터 소모를 줄이고 네트워크 비용을 절감할 수 있습니다.</li>
</ul>
<h4 id="3-레이아웃-안정성-확보">3. 레이아웃 안정성 확보</h4>
<ul>
<li>최적화되지 않은 이미지는 로드 중 레이아웃 이동(CLS)을 유발해 사용자 경험을 저하시킬 수 있습니다. 하지만, 최적화 과정에서 이미지 크기를 미리 정의하면 이런 문제를 방지할 수 있습니다.</li>
</ul>
<h4 id="4-최적화된-포맷-제공">4. 최적화된 포맷 제공</h4>
<ul>
<li>WebP와 같은 최신 이미지 표맷은 동일한 화질을 유지하면서도 파일 크기를 줄일 수 있습니다. 일반적인 JPEG나 PNG 대비 25~34% 더 작은 크기를 제공합니다!</li>
</ul>
<h3 id="img-의-한계"><code>&lt;img&gt;</code> 의 한계</h3>
<h4 id="1-이미지-최적화를-수동으로-처리해야-함">1. 이미지 최적화를 수동으로 처리해야 함</h4>
<ul>
<li><code>&lt;img&gt;</code>는 브라우저가 제공한 이미지를 그대로 로드합니다.</li>
<li>화면 크기와 해상도에 따라 다른 이미지를 제공하려면 개발자가 직접 미디어 쿼리를 작성하거나 이미지를 별도로 준비해야 합니다.</li>
</ul>
<h4 id="2-lazy-loading을-기본적으로-지원하지-않음">2. Lazy Loading을 기본적으로 지원하지 않음</h4>
<ul>
<li><code>loading=&quot;lazy&quot;</code> 속성을 추가하면 브라우저에서 Lazy Loading이 가능하지만, 모든 브라우저에서 지원하지 않거나 추가 설정이 필요합니다.</li>
</ul>
<h4 id="3-포맷-변환-기능-없음">3. 포맷 변환 기능 없음</h4>
<ul>
<li>WebP, AVIF 등 최신 포맷으로 변환하려면 별도의 이미지 처리 도구나 CDN 서비스를 사용해야 합니다.</li>
</ul>
<h4 id="4-cls누적-레이아웃-이동-문제">4. CLS(누적 레이아웃 이동) 문제</h4>
<ul>
<li><code>&lt;img&gt;</code>는 이미지 로드 전에 크기 정보를 제공하지 않아, 로딩 도중 레이아웃 이동이 발생할 수 있습니다.</li>
</ul>
<br />

<h2 id="nextimage-컴포넌트">Next/Image 컴포넌트</h2>
<p>Next/Image는 성능 최적화의 핵심적인 역할을 합니다. <a href="https://nextjs.org/docs/app/building-your-application/optimizing/images">Next.js 공식 문서</a>에서는 Next/Image에 대해서 다음과 같이 소개하고 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/182e81f8-54ac-4cd4-b4dc-3e93461881c9/image.png" alt="next.js_공식문서1"></p>
<p>Next.js Image 구성 요소는 HTML <code>&lt;img&gt;</code> 요소를 확장하여 자동 이미지 최적화 기능을 제공합니다:</p>
<ul>
<li><strong>사이즈 최적화</strong>: 각 디바이스에 맞는 크기의 이미지를 자동으로 제공하며, WebP와 AVIF와 같은 최신 이미지 포맷을 사용하여 성능을 최적화합니다.</li>
<li><strong>시각적 안정성</strong>: 이미지를 로드할 때 레이아웃 이동을 자동으로 방지하여 화면이 안정적으로 표시됩니다.</li>
<li><strong>빠른 페이지 로딩</strong>: 이미지는 뷰포트에 들어올 때만 로드되며, 기본적으로 브라우저의 Lazy Loading을 사용합니다. 또한, 블러 효과로 로드가 완료될 때까지 잠깐의 placeholder 이미지를 표시할 수 있습니다.</li>
<li><strong>유연성</strong>: 원격 서버에 저장된 이미지도 필요에 따라 크기를 조정할 수 있어, 다양한 상황에 맞는 유연한 이미지 처리가 가능합니다.</li>
</ul>
<p>Next/Image에서 제공하는 대표 기능은 다음과 같습니다.</p>
<h3 id="lazy-loading">Lazy Loading</h3>
<p>Lazy Loading은 사용자가 이미지를 보게 될 때까지 이미지를 로드하지 않고, <strong>스크롤을 통해 이미지가 뷰포트에 들어올 때에만 이미지를 로드</strong>하는 기술입니다. 이렇게 하면 페이지가 처음 로드될 때 <strong>불필요한 이미지 로딩을 피할 수 있어 초기 로딩 속도를 대폭 향상</strong>시킬 수 있습니다.</p>
<p>만약, 중요한 이미지 일부에 lazy loading을 적용하고 싶지 않은 경우에는 해당 기능을 끌 수도 있습니다. Image 컴포넌트의 priority라는 prop을 true로 설정하거나, loading prop에 “eager” 값을 설정하면 됩니다. (priorty 값을 설정하는 것이 더 권장되는 방식입니다.)</p>
<h3 id="이미지-사이즈-최적화">이미지 사이즈 최적화</h3>
<p>이미지 사이즈 최적화는 사용하는 <strong>화면 크기와 디바이스의 해상도에 맞게 이미지를 적절히 조정하여 성능을 개선</strong>하는 기술입니다. 이는 파일 크기를 줄여 로딩 속도를 개선하고, 다양한 디바이스에서 최적화된 이미지를 제공합니다.</p>
<p><strong><code>layout</code> 속성(intrinsic, responsive, fill)</strong>을 통해 이미지 크기를 반응형으로 설정할 수 있습니다. 이 방식은 페이지 크기나 화면 크기에 맞춰 이미지 크기를 자동으로 조정합니다.</p>
<p>Next.js는 <strong>WebP와 같은 최신 이미지 포맷을 자동으로 제공</strong>하여 더 작은 파일 크기로 전송합니다. WebP는 동일한 품질의 이미지에 대해 JPEG보다 30% 이상 더 작은 파일 크기를 제공합니다.</p>
<h3 id="placeholder-제공">Placeholder 제공</h3>
<p>Next/Image는 <strong>이미지를 로드할 때 blur-up 효과를 제공</strong>합니다. 이미지가 로드되는 동안, 저해상도 이미지가 먼저 표시되고, 이후 고해상도 이미지가 로드되면 자연스럽게 교체됩니다. 또한, 이미지 로딩이 끝나기 전에도 화면에 흰 공간이나 빈 곳이 아닌, 로딩 중인 이미지를 표시함으로써 <strong>누적 레이아웃 이동(CLS)</strong>을 방지할 수 있습니다.</p>
<p><code>placeholder=&quot;blur&quot;</code> 속성을 사용하면 이 기능을 활성화할 수 있습니다.</p>
<br />



<h2 id="내가-느낀-nextimage-장단점">내가 느낀 Next/Image 장단점</h2>
<h3 id="장점">장점</h3>
<p>자동으로 최적화된 이미지를 제공하여, <strong>추가적인 설정이나 라이브러리 없이도 성능을 향상</strong>시킬 수 있습니다. 웹 성능을 높이기 위한 여러 가지 기능들이 기본적으로 내장되어 있어, 직접 최적화 옵션을 고민하거나 설정할 필요가 없어서 작업할 때 편리했던 것 같습니다. </p>
<h3 id="단점">단점</h3>
<p>개인적으로 저는 항상 사용할 때마다 속성에 대한 설정이 헷갈렸습니다...ㅎㅎ</p>
<p><code>&lt;img&gt;</code> 같은 경우는 직접 사이즈를 넣어주면 그만인데 Next/Image 는 설정해줘야할 속성들이 있고 그것이 어떤 것들인지에 대해도 다 알고있어야 했습니다.</p>
<p>특히 가장 헷갈렸던 것이 <strong>layout 속성</strong>이었습니다. layout 속성에는 fixed, intrinsic, responsive, fill과 같은 옵션이 있는데, 잘못된 값을 설정해서 이미지가 의도하지 않게 왜곡되거나 크기가 너무 커지는 문제를 겪기도 했습니다😅</p>
<p>그리고 layout 속성에 따라서 width와 height 속성의 의미도 달라진다는 점도 새롭게 알게 되었습니다.</p>
<ul>
<li><strong>intrinsic</strong>은 원본 이미지 비율을 유지하면서 크기 조정.</li>
<li><strong>fixed</strong>는 설정한 크기로 정확히 표시.</li>
<li><strong>responsive</strong>는 width와 height로 비율을 정의하고, 화면 크기에 맞게 크기를 조정.</li>
<li><strong>fill</strong>은 width와 height가 설정되지 않고, 부모 컨테이너의 크기에 맞춰 이미지가 채워짐.</li>
</ul>
<h2 id="결론">결론</h2>
<p>쓰다 보니 장점보다 단점이 길어졌는데, 사실 단점이라기보다는 잘 몰라서 쓰기 어려웠던 개인적인 헤프닝이었던 것 같습니다ㅎㅎ</p>
<p>평소에 Next/Image를 사용하면서 <code>&lt;img&gt;</code>를 쓰지 않고 굳이 Next/Image를 써야 하는 이유와 성능 최적화에서의 차이가 얼마나 큰지 궁금했었는데, 이렇게 글로 정리하면서 그 궁금증을 조금 해소할 수 있었던 것 같습니다~!</p>
<p>다음 작업에서는 무작정 사용하는 것이 아닌, 성능 차이를 비교하면서 직접 눈으로 확인하고 진행해 봐야겠다는 생각이 듭니다.</p>
<blockquote>
<p>📚 <strong>참고</strong>
<a href="https://fe-developers.kakaoent.com/2022/220714-next-image/">Next/Image를 활용한 이미지 최적화</a>
<a href="https://oliveyoung.tech/2023-06-09/nextjs-image-optimization/">NEXT.JS의 이미지 최적화는 어떻게 동작하는가?</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[스토리북 mdx 파일 렌더링 오류를 해결해봅시다 ]]></title>
            <link>https://velog.io/@ahn-sujin/%EC%8A%A4%ED%86%A0%EB%A6%AC%EB%B6%81-mdx-%ED%8C%8C%EC%9D%BC-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%98%A4%EB%A5%98%EB%A5%BC-%ED%95%B4%EA%B2%B0%ED%95%B4%EB%B4%85%EC%8B%9C%EB%8B%A4</link>
            <guid>https://velog.io/@ahn-sujin/%EC%8A%A4%ED%86%A0%EB%A6%AC%EB%B6%81-mdx-%ED%8C%8C%EC%9D%BC-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%98%A4%EB%A5%98%EB%A5%BC-%ED%95%B4%EA%B2%B0%ED%95%B4%EB%B4%85%EC%8B%9C%EB%8B%A4</guid>
            <pubDate>Mon, 16 Dec 2024 11:50:21 GMT</pubDate>
            <description><![CDATA[<p>이번 포스팅에서는 스토리북으로 디자인 시스템을 구축하던 중에 발생한 오류를 공유해 보려고 합니다. 다소 삽질했지만...ㅎㅎ 결국 답은 공식 문서에서 찾았습니다. 혹시 같은 문제를 겪고 계신 분들에게 도움이 되길 바랍니다! 😊</p>
<hr>
<h2 id="문제">문제</h2>
<p>스토리북 초기 세팅을 마치고 본격적인 컴포넌트 작업에 앞서, Foundation(Colors, Fonts, 여백 등)을 설명하는 문서를 작성하고 싶었습니다. 피그마에 이미 관련 자료가 정리되어 있지만, 개발과 디자인 모두가 디자인 시스템 관련 내용을 한눈에 확인할 수 있는 문서를 스토리북에 통합하면 훨씬 효율적일 거라고 판단했습니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/27c65bbf-2dcd-4eb7-9761-0a27c8a3bb4a/image.png" alt=""></p>
<p>그래서 MDX 파일을 활용해 문서를 작성하려고 했습니다. 그런데 파일을 작성 후 실행하자마자 아래와 같은 오류를 만났습니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/b9c56867-d700-4c5a-a2bc-8a567dc98bac/image.png" alt="오류"></p>
<blockquote>
<p>Error: Unable to index ./src/stories/Color.stories.mdx:</p>
</blockquote>
<p>스토리북에서 Color.stories.mdx 파일을 읽지 못한다는 오류였고, 랜더링에 실패했습니다. 몇 시간 동안 헤매며 다양한 설정을 점검했지만, 결국 문제는 <strong>파일 확장자와 스토리북의 역할 분리</strong>에 대한 이해 부족에서 비롯되었습니다. 😅</p>
<br />

<h2 id="원인-및-해결-방법">원인 및 해결 방법</h2>
<h3 id="원인-분석">원인 분석</h3>
<p>최신 스토리북(8.0 이상)에서는 <code>.mdx</code>와 <code>.stories.mdx</code> 파일의 역할이 명확히 분리되었습니다:</p>
<ul>
<li><code>.mdx</code> 파일: <strong>독립적인 문서 페이지</strong>용으로 사용. <strong>컴포넌트와 관계없는 문서화</strong>를 위해 작성.</li>
<li><code>.stories.mdx</code> 파일: <strong>스토리와 문서를 함께 정의</strong>하기 위한 파일. 하지만 <strong>최신 스토리북에서는 기본 지원이 중단</strong>됨.</li>
<li><code>.stories.ts</code> 파일: <strong>컴포넌트의 스토리를 정의</strong>하는 데 사용. CSF(Component Story Format) 기반.</li>
</ul>
<p>제가 작성한 <code>Color.stories.mdx</code> 파일은 최신 스토리북에서는 적합하지 않은 구조였고, 파일 역할과 설정이 일치하지 않아 오류가 발생한 것이었습니다.</p>
<h3 id="해결-방법">해결 방법</h3>
<p>최종적으로 mdx 파일을 제대로 랜더링하기 위해서 아래와 같은 작업을 수행했습니다.</p>
<p><strong>1. main.ts 설정</strong>
스토리북에서 mdx 파일을 올바르게 읽을 수 있도록 main.ts 파일의 stories 설정을 수정했습니다.</p>
<pre><code class="language-typescript">const config = {
  stories: [
    &quot;../src/**/*.mdx&quot;, // 문서화용 MDX 파일 추가
    &quot;../src/**/*.stories.@(js|jsx|ts|tsx)&quot; // 스토리용 파일
  ],
  addons: [
    &quot;@storybook/addon-docs&quot;, // 문서화 애드온
    &quot;@storybook/addon-essentials&quot;,
  ],
  framework: {
    name: &quot;@storybook/react-vite&quot;,
    options: {},
  },
  core: {
    builder: &quot;@storybook/builder-vite&quot;,
  },
};
export default config;</code></pre>
<p><strong>2. 필수 패키지 설치</strong>
최신 스토리북에서는 mdx3를 사용하기 때문에 필요한 패키지를 설치했습니다.</p>
<pre><code class="language-bash">npm install @storybook/react-vite @storybook/addon-docs @mdx-js/react @mdx-js/rollup --save-dev
</code></pre>
<p><strong>3. 파일 확장자 수정</strong>
스토리북의 역할 분리에 따라 <code>Color.stories.mdx</code> 파일을 <code>Color.mdx</code>로 이름 변경 후 문서화 페이지로 활용했습니다.</p>
<p>컴포넌트 스토리는 별도의 <code>.stories.ts</code> 파일에서 정의합니다.</p>
<p><strong>4. mdx 파일 작성</strong>
아래는 테스트를 위해 예시로 작성한 파일이니 참고 부탁드립니당🥸</p>
<pre><code>import { Meta } from &quot;@storybook/addon-docs&quot;;

&lt;Meta title=&quot;Foundation/Colors&quot; /&gt;

# 색상 팔레트

디자인 시스템에서 사용되는 색상들을 소개합니다. 이 색상들은 컴포넌트의 속성으로 사용됩니다.

## 기본 색상

| 색상 이름 | 색상 코드 |
| --------- | --------- |
| Primary   | #007bff   |
| Secondary | #6c757d   |
| Success   | #28a745   |
| Danger    | #dc3545   |
| Warning   | #ffc107   |
| Info      | #17a2b8   |
| Light     | #f8f9fa   |
| Dark      | #343a40   |

## 강조 색상

이 색상들은 UI의 주요 액션이나 강조에 사용됩니다.

| 색상 이름 | 색상 코드 |
| --------- | --------- |
| Accent    | #ff5733   |
| Highlight | #00c853   |
</code></pre><p><img src="https://velog.velcdn.com/images/ahn-sujin/post/7b4c5f8c-5434-4465-95cf-1d0ba46cbd7b/image.png" alt=""></p>
<br />

<h2 id="마무리">마무리</h2>
<p>이번 오류를 통해 공식 문서를 꼼꼼히 확인하는 것의 중요성을 다시 한번 깨달았습니다! 버전 업데이트와 함께 제공되는 공식 문서를 확인하고, 새로운 규칙과 사용법에 빠르게 적응하는 것도 개발자로서 중요한 역량이라는 점을 느꼈습니다.</p>
<p>앞으로도 문제를 만났을 때 기본 설정부터 차근차근 점검하고, 공식 문서와 커뮤니티 리소스를 활용하는 습관을 이어가야겠습니다. 😊</p>
<br />

<blockquote>
<h4 id="참고">참고</h4>
<p><a href="https://storybook.js.org/docs/writing-docs/mdx">MDX | Storybook docs</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[효율적인 git branch 전략을 향해 :TBD에서 Git-Flow로 ]]></title>
            <link>https://velog.io/@ahn-sujin/%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-git-branch-%EC%A0%84%EB%9E%B5%EC%9D%84-%ED%96%A5%ED%95%B4-TBD%EC%97%90%EC%84%9C-Git-Flow%EB%A1%9C</link>
            <guid>https://velog.io/@ahn-sujin/%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-git-branch-%EC%A0%84%EB%9E%B5%EC%9D%84-%ED%96%A5%ED%95%B4-TBD%EC%97%90%EC%84%9C-Git-Flow%EB%A1%9C</guid>
            <pubDate>Thu, 28 Nov 2024 12:04:40 GMT</pubDate>
            <description><![CDATA[<p>Git을 사용하는 개발자라면 한 번쯤은 Branch 전략에 대해 고민해 본 적이 있으실 텐데요! 저도 회사에서 기존에 사용하던 TBD 전략에서 Git-Flow로 전환하며 많은 고민을 했었습니다.
이번 포스팅에서는 TBD 전략이 왜 적합하지 않았는지, 그리고 Git-Flow로 변경하며 어떤 개선을 이뤘는지에 대해 공유해보려 합니다. 🥸 </p>
<h2 id="tbd">TBD</h2>
<hr>
<h3 id="tbd-전략이란">TBD 전략이란?</h3>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/8d2dee00-9833-4131-826d-33b1c1fef698/image.png" alt="TBD전략"></p>
<p>TBD(Trunk-Based Development) 전략은 모든 개발자가 하나의 main 브랜치(트렁크)를 중심으로 작업을 진행하는 Git 브랜치 전략입니다. 이 전략에서 중요한 점은 <strong>작은 단위로 자주 커밋</strong>하고, <strong>개발자가 빠르게 main 브랜치에 병합</strong>하는 것입니다.</p>
<p>개발자는 feature 브랜치에서 작업할 수 있지만, 이 브랜치를 오래 유지하지 않고 작은 작업 단위로 main 브랜치에 빠르게 병합합니다. 작업이 완료되면 PR을 생성하여 코드 리뷰를 받으며, 리뷰가 완료된 후 main 브랜치에 병합됩니다. 이러한 프로세스는 가능한 자주 일어나며, 모든 개발자는 최신 main 브랜치를 기반으로 작업을 계속합니다.</p>
<h3 id="tbd-전략을-사용한-이유는">TBD 전략을 사용한 이유는?</h3>
<p> 당시 서비스가 빠르게 성장하고 있었기 때문입니다. 이로 인해 잦은 배포가 이루어졌고, <strong>배포 주기가 정해져 있지 않은 상황</strong>에서 기획과 디자인이 완료되면 곧바로 개발에 착수하여, 개발이 끝나는 즉시 운영 서버에 배포하는 방식으로 진행되었습니다.</p>
<p>이러한 방식은 <strong>빠르게 기능을 개발하고, 유저의 반응을 확인한 뒤, 이를 기반으로 기능을 수정 및 보완</strong>하는 것을 목표로 했습니다. 따라서** 배포 과정에서 복잡한 절차를 줄이고자 했습니다.**</p>
<p>또한, 당시 개발 인원이 많지 않았기 때문에 긴 배포 프로세스가 오히려 비효율적이라고 판단되었습니다. TBD 전략은 작은 단위로 작업을 자주 병합하고, 빠르게 배포할 수 있는 방식을 제공했기 때문에 이러한 요구를 충족시키기에 적합하다고 보아 선택하게 되었습니다.</p>
<h3 id="어떤-문제점이-있었나">어떤 문제점이 있었나?</h3>
<p>하지만 서비스 안정화와 규모 확장으로 인해 다음과 같은 문제가 발생했습니다.</p>
<h4 id="배포-시기의-불일치">배포 시기의 불일치</h4>
<p>서비스의 규모가 커지고, <strong>장기간 프로젝트, 단기간 작업, 유지보수 작업</strong>이 동시에 진행되면서 개발자들 간 배포 시점이 서로 달라지는 상황이 빈번해졌습니다. TBD에서는 모든 작업이 main 브랜치에 자주 병합되기 때문에, 이러한 배포 시기의 불일치는 예기치 않은 충돌이나 의존성 문제를 발생시켰고, 결과적으로 배포 프로세스의 효율성을 저하시켰습니다.</p>
<h4 id="코드-정리-및-유지보수-부담-증가">코드 정리 및 유지보수 부담 증가</h4>
<p>작업 간 배포 시점의 차이로 인해 <strong>이미 배포된 코드와 배포 대기 중인 코드가 동시에 main 브랜치에 존재</strong>하는 상황이 생겼습니다. 이로 인해 <strong>미사용 코드 정리, 충돌 해결, 배포 이후의 추가적인 수정 작업</strong> 등 유지보수에 많은 시간이 소요되었습니다. 이러한 문제는 코드의 복잡성을 증가시키고, 작업 효율성을 저하시키는 결과를 낳았습니다.</p>
<br />

<h2 id="git-flow">git-flow</h2>
<hr>
<h3 id="git-flow-전략이란">git-flow 전략이란?</h3>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/687a4676-27e6-4c6b-acd9-107f7f10782e/image.png" alt="공식_gitflow">
Git-Flow 전략은 소프트웨어 개발에서 <strong>효율적인 브랜치 관리와 배포 프로세스</strong>를 지원하는 Git 브랜칭 모델입니다. 주요 브랜치로는 <strong>main(배포용), develop(개발용), feature(기능 개발), release(배포 준비), hotfix(긴급 수정)</strong> 브랜치가 있으며, 각각 명확한 목적과 작업 흐름을 가지고 있습니다.</p>
<h3 id="git-flow-전략을-선택한-이유는">git-flow 전략을 선택한 이유는?</h3>
<p>Git-Flow 전략을 선택한 이유는 <strong>작업 유형과 배포 주기를 명확히 분리하여 안정성을 확보</strong>하기 위함이었습니다. TBD 전략에서는 작업 간 배포 시점의 불일치로 인해 충돌과 의존성 문제가 발생하고, 배포되지 않은 코드와 배포된 코드가 동시에 main 브랜치에 존재하는 상황이 빈번했기 때문에, 이를 해결할 필요가 있었습니다. Git-Flow는 main, develop, feature, release, hotfix 브랜치를 통해 각 작업의 목적과 배포 프로세스를 명확히 구분하고, 체계적인 관리로 배포 안정성을 높일 수 있어 기존의 문제를 해결하는 데 적합했습니다.</p>
<h3 id="git-flow-를-어떻게-사용했나">git-flow 를 어떻게 사용했나?</h3>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/2039a79f-948b-4ea5-bb90-87dfe52a453d/image.png" alt="gitFlow"></p>
<p>저희 팀은 Git-Flow 전략을 원칙대로 사용하지 않고, 프로젝트 특성과 배포 프로세스에 맞게 변형하여 적용했습니다. 이를 통해 <strong>개발 완료와 배포 시기를 유연하게 분리하고, 작업 효율성과 코드 관리</strong>를 높였습니다. 아래는 세부적인 브랜치 구조와 사용 방식입니다.</p>
<h4 id="main-브랜치">main 브랜치</h4>
<p><strong>운영 서버</strong>에 배포된 <strong>안정된 코드</strong>를 관리하는 브랜치입니다. 버그 수정이나 긴급 작업 완료 후 이곳에 병합됩니다.</p>
<h4 id="release-브랜치-배포-직전">release 브랜치: 배포 직전</h4>
<p><strong>배포 직전 상태의 feature 브랜치</strong>들을 모아 관리하는 브랜치입니다. 배포 준비가 완료된 코드는 이 브랜치로 병합되며, 최종적으로 Main 브랜치로 PR을 생성해 배포를 진행합니다.</p>
<p>release에 병합된 feature 브랜치는 삭제하여 브랜치 관리를 간소화합니다.</p>
<h4 id="stage-브랜치-개발-완료">stage 브랜치: 개발 완료</h4>
<p><strong>QA 및 테스트를 위한 브랜치</strong>로, 배포 시기와 상관없이 개발이 완료된 feature 브랜치들을 모아 관리합니다.</p>
<blockquote>
<p><strong>Release 브랜치와의 차이점</strong></p>
</blockquote>
<ul>
<li>Stage 브랜치는 개발 완료 시점에 병합되며, 배포 날짜에 구애받지 않습니다.</li>
<li>이는 작업 완료와 배포를 분리하여, 긴 QA 과정이 필요한 작업과 즉시 배포 가능한 작업을 구분하기 위함이었습니다.</li>
</ul>
<h4 id="feature-브랜치--기능-개발">feature 브랜치 : 기능 개발</h4>
<p><strong>각 기능 개발을 위한 브랜치</strong>로, 작업이 완료되면 stage 브랜치로 병합하고 QA를 진행합니다. QA가 완료되고 배포 시점이 다가오면, release 브랜치로 병합합니다.</p>
<h4 id="bugfix-브랜치--오류-수정">bugfix 브랜치 : 오류 수정</h4>
<p><strong>운영 중 발생한 버그나 긴급 CS 이슈를 해결하기 위한 브랜치</strong>입니다. 작업 완료 후 main 브랜치로 PR을 생성해 바로 병합하고, 이후 main 브랜치와 release 브랜치를 동기화하여 배포 상태를 유지합니다.</p>
<h4 id="refactor-브랜치-코드-개선">refactor 브랜치: 코드 개선</h4>
<p>*<em>코드 리팩토링 *</em>작업을 위한 브랜치입니다. </p>
<h4 id="squash-merge-사용">Squash Merge 사용</h4>
<p>feature 브랜치를 release 브랜치로 병합할 때는 Squash Merge를 사용했습니다.</p>
<ul>
<li><p><strong>커밋 기록의 간결화</strong>
Squash Merge는 작업 과정을 한 번에 합쳐 하나의 커밋으로 병합하기 때문에, 불필요한 커밋 로그를 줄이고 히스토리를 깔끔하게 유지할 수 있습니다.</p>
</li>
<li><p><strong>PR 리뷰 과정 간소화</strong>
커밋 수가 많을 경우 PR 리뷰가 복잡해질 수 있지만, Squash Merge를 통해 PR을 단일 작업 단위로 처리할 수 있습니다.</p>
</li>
<li><p><strong>코드베이스 관리 효율성</strong>
병합 이후 각 브랜치의 기록을 단순화하여, Git 히스토리를 추적하거나 문제가 발생했을 때 디버깅을 쉽게 할 수 있습니다.</p>
</li>
</ul>
<br />

<h2 id="마무리">마무리</h2>
<hr>
<p>TBD 전략은 초기에는 빠르고 간소화된 배포 프로세스를 지원해 효과적이었지만, 서비스가 성장하고 팀 규모와 작업 범위가 확대되면서 여러 문제들이 발견됐습니다. 이러 문제를 해결하기 위해 Git-Flow 전략을 프로젝트에 맞게 변형하여 도입함으로써 작업과 배포를 명확히 분리하고 효율성을 높일 수 있었던 것 같습니다!</p>
<p>비록 전보다 지켜야하는 규칙과 관리해야하는 브랜치들이 많아지기는 했지만, 빠르고 간편한게 무조건 좋은 것 만은 아니라는 것도 깨달았습니다!ㅎㅎ 이번 경험을 통해 프로젝트 환경과 팀의 요구에 따라 브랜치 전략을 유연하게 조정하는 것이 중요하다는 것을 배울 수 있었습니다. 이런 시행착오를 통해 더 발전할 수 있을거라고 믿으며 우리 모두 상황에 맞는 브랜치 전략을 선택합시다~ ! 🙌🏻</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🧩 합성 컴포넌트로 효율적으로 개발하기 ]]></title>
            <link>https://velog.io/@ahn-sujin/%ED%95%A9%EC%84%B1-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%A1%9C-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9C%BC%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ahn-sujin/%ED%95%A9%EC%84%B1-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%A1%9C-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9C%BC%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 19 Sep 2024 13:31:06 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드 개발에서 빠질 수 없는 부분이 컴포넌트 개발인 것 같습니다. 겉으로 보기에는 단순해 보일 수 있지만 자세히 들여다보면 어떤 개발보다도 섬세하고 복잡한 작업이라는 생각이 듭니다. 현재 저는 개발할 때 컴포넌트를 두가지 기준으로 나눠서 작업하고 있습니다. <strong>단순 UI 를 표현하는 common 컴포넌트</strong>와 <strong>UI+기능 을 다루는 feature 컴포넌트</strong> 입니다. ( 여기서 <strong>기능</strong>의 기준은 컴포넌트안에서 API를 호출하여 데이터를 다루거나, 상태관리를 함으로써 UI의 변화를 주는 것을 말합니다. )</p>
<p>제가 소개할 합성 컴포넌트는 <strong>feature 컴포넌트</strong>에 속해있는 컴포넌트 중 하나입니다. 이번 포스팅에서는 합성 컴포넌트란 무엇이고, 합성 컴포넌트를 적용한 과정에 대해서 소개해보려고 합니다.</p>
<br />

<hr>
<h1 id="asis-컴포넌트">ASIS 컴포넌트</h1>
<p>오늘 다룰 컴포넌트는 책 UI를 나타내는 컴포넌트입니다. (편의상 <strong>Book컴포넌트</strong>라고 부르겠습니다!) API를 호출해서 책에 대한 정보(책 이미지, 책 이름, 출판사 이름, 책 플래그)를 받아오고 이를 UI에 맞게 배치해서 보여줍니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/e503b4c3-c6eb-421f-b19b-641113052e57/image.jpeg" alt=""></p>
<p>위와 같은 Book 컴포넌트를 구현하기 위한 코드는 아래와 같습니다. (이해를 돕기 위한 예시로 생략된 부분이 있으니 참고해서 봐주세요!)</p>
<pre><code class="language-typescript">
import Text from &quot;components/common/text&quot;
import Flag from &quot;components/common/flag&quot;
import OriginalBook from &quot;components/features/original/book&quot;
import OriginalBookImage from &quot;components/features/original/book/image&quot;
import OriginalBook from &quot;components/features/original/book/caption&quot;
import MySchoolBookImage from &quot;components/features/original/book/svg/image&quot;


const BookList = () =&gt; {
      ...

    return (
      ...

                {gerOriginalSchoolBookListResponse.data.book_list.map((item, index) =&gt; {
              return (
                &lt;OriginalBook
                  key={item.id}
                  onClick={() =&gt; onClickBookItem(item.id)}
                &gt;
                  {/* 책 이미지 png 파일 */}
                  {item.thumbnail_url &amp;&amp; &lt;OriginalBookImage src={item.thumbnail_url} /&gt;}
                    {/* 책 이미지 svg 파일 */}
                  {item.my_school_thumbnail_detail &amp;&amp; (
                    &lt;MySchoolBookImage schoolName={schoolName} subsubjectName={item.subsubject_name} colorDetail={item.my_school_thumbnail_detail} /&gt;
                  )}
                  {/* 책 출판사 */}
                  {item.school_publisher_name &amp;&amp; (
                    &lt;Text type=&quot;caption2&quot; color=&quot;gray_400&quot;&gt;
                      {item.school_publisher_name}
                    &lt;/Text&gt;
                  )}
                  {/* 책 이름 */}
                  &lt;OriginalBookCaption&gt;{item.name}&lt;/OriginalBookCaption&gt;
                  {!AuthOriginalActive?.is_active &amp;&amp; item.is_trial &amp;&amp; &lt;Flag type=&quot;book&quot;&gt;이 달의 무료 문제집&lt;/Flag&gt;}
                   {/* 책 플래그 */}
                  {!isOpen(item.opened_at) &amp;&amp; (
                    &lt;Flag type=&quot;book&quot; color=&quot;blue_400&quot;&gt;
                      {getMonth(item.opened_at)} 말 OPEN 예정
                    &lt;/Flag&gt;
                  )}
                &lt;/OriginalBook&gt;
              )
            })}

     ...

    )
}
</code></pre>
<p>이해를 돕기 위해 위 코드에 대해서 간단히 설명하자면</p>
<ul>
<li><code>gerOriginalSchoolBookListResponse</code> 는 책에 대한 정보를 갖고 있는 API response 값 입니다.</li>
<li><code>&lt;OriginalBook /&gt;</code> 은 <code>&lt;OriginalImage /&gt;</code> , <code>&lt;OriginalCaption /&gt;</code> 등 책을 구성하는데 필요한 요소들을 자식으로 받는 부모 컴포넌트입니다.</li>
<li><code>&lt;OriginalImage /&gt;</code> 은 책의 png 이미지를 나타내는 컴포넌트 입니다.</li>
<li><code>&lt;OriginalCaption /&gt;</code> 은 책의 이름을 나타내는 컴포넌트 입니다.</li>
<li><code>&lt;MySchoolBookImage /&gt;</code> 은 책의 svg 이미지를 랜더링 해주는 컴포넌트 입니다.</li>
<li><code>&lt;Flag /&gt;</code> 는 책의 플래그(9월 말 오픈예정 등)를 나타내는 공용 컴포넌트 입니다.</li>
<li><code>&lt;Text /&gt;</code> 는 출판사 이름을 나타내는 공용 컴포넌트 입니다.</li>
</ul>
<br />

<h2 id="문제점">문제점</h2>
<p>위와 같은 방식으로 컴포넌트를 구성했을 때 제가 느꼈던 문제점은 아래와 같습니다.</p>
<ol>
<li>매번 <code>&lt;OriginalBook&gt;, &lt;OriginalBookImage&gt;, &lt;OriginalBookCaption&gt;</code> 을 각각 import 해야합니다.</li>
<li><code>&lt;OriginalBookCaption&gt;</code> 을 사용 기준이 개발자마다 다릅니다.<ul>
<li><code>&lt;OriginalBookCaption&gt;</code> props로 이름을 받는게 아니라 children으로 받고 있어서 누구는 <strong>책 이름, 책 출판사까지 포함</strong>하고, 누구는 <strong>책 이름까지만 포함시키고 책 출판사는 따로 추가</strong>하는 방식으로 사용되어 협업시 혼란을 주고 있습니다.</li>
</ul>
</li>
<li><code>&lt;OriginalBookImage&gt;</code> 은 현재 png 파일만 받고 있어서 svg 파일의 경우 각각 페이지마다 해당 컴포넌트 <code>&lt;MySchoolBookImage&gt;</code> 를 import 해서 추가해야 합니다. </li>
<li>책 플래그(이 달의 무료 문제집, 오픈예정, 내신연구소 단독)를 표시해주려면 매번 <code>&lt;Flag&gt;</code> 컴포넌트를 import 해서 추가해야 합니다.</li>
<li>모든 response 값에 대해서 그 값이 존재하는지 아닌지에 대해서 체크를 하는 로직이 매번 필요합니다.</li>
</ol>
<br />

<h2 id="개선점">개선점</h2>
<ol>
<li>Book 컴포넌트를 구성하기 위한 요소들을 각각 import 하지 않고 한번만 import해서 사용 할 수 있도록 합니다.</li>
<li>각 컴포넌트의 기준을 정확하게 정의하여 개발자가 사용하는데 있어 혼란을 줄입니다.</li>
<li>컴포넌트의 사용 여부(보여지고 안보여지고)에 대한 로직은 컴포넌트를 import하는 페이지에서 처리하는 것이 아닌 해당 컴포넌트에서 처리하도록 합니다.</li>
</ol>
<br />

<hr>
<h1 id="tobe-컴포넌트">TOBE 컴포넌트</h1>
<p>위에 개선점을 반영시키기 위한 방법을 찾아보다가 합성 컴포넌트에 대해서 알게 되었고 제가 하려고 하는 방향성과 맞다고 판단되어 적용하게 되었습니다. 먼저 합성 컴포넌트에 대해서 간단히 소개하도록 하겠습니다.</p>
<h2 id="합성-컴포넌트란">합성 컴포넌트란?</h2>
<p>합성 컴포넌트는 하나의 컴포넌트를 여러 가지 집합체로 분리한 뒤, 분리된 각 컴포넌트를 사용하는 쪽에서 조합해서 사용하는 컴포넌트 패턴을 의미합니다.</p>
<p>간단한 예시로 html의 select를 볼 수 있는데, select는 <code>&lt;select&gt;</code>와 <code>&lt;option&gt;</code> 태그의 조합으로 이루어집니다. <code>&lt;select&gt;</code>와 <code>&lt;option&gt;</code>은 각각 독립적으로는 큰 의미가 없지만 사용하는 곳에서 이를 조합해 사용함으로써 화면에 의미 있는 요소가 됩니다.</p>
<pre><code class="language-javascript">
&lt;select&gt;
  &lt;option value=&quot;1&quot;&gt;Option 1&lt;/option&gt;
  &lt;option value=&quot;2&quot;&gt;Option 2&lt;/option&gt;
&lt;/select&gt;
</code></pre>
<br />

<h2 id="구현하기">구현하기</h2>
<blockquote>
<p>합성 컴포넌트 패턴을 적용한 예시를 위해 각각의 컴포넌트들의 상세한 props 와 로직들은 포함되어 있지 않으니 이 점 참고해서 봐주세요!</p>
</blockquote>
<h3 id="1-서브-컴포넌트">1. 서브 컴포넌트</h3>
<p>html의 <code>&lt;option&gt;</code> 태그에 해당하는 서브 컴포넌트를 구현합니다. Book 컴포넌트를 구성하는 각각의 컴포넌트들이 여기에 해당합니다. <code>&lt;BookImage&gt; &lt;BookCaption&gt; &lt;BookFlag&gt;</code></p>
<h3 id="2-메인-컴포넌트">2. 메인 컴포넌트</h3>
<p>html의 <code>&lt;select&gt;</code> 태그에 해당하는 메인 컴포넌트를 구현합니다. 서브 컴포넌트들을 묶어서 화면에 적잘하게 보이도록 하는 Wrapper 성격의 컴포넌트입니다.</p>
<pre><code class="language-typescript">import styled from &quot;@emotion/styled&quot;
import {ClickMotion} from &quot;@repo/ui&quot;

export interface OriginalBookGroupProps extends OriginalBookContextValue {
  children: React.ReactNode
  onClick?: () =&gt; void
}

export type Props = OriginalBookGroupProps &amp; HTMLAttributes&lt;HTMLDivElement&gt;

export const OriginalBookGroup: React.FC&lt;Props&gt; = ({children, onClick, ...props}) =&gt; {

  return (
      &lt;ClickMotion onClick={onClick}&gt;
        &lt;Wrapper {...props}&gt;
          {children}
        &lt;/Wrapper&gt;
      &lt;/ClickMotion&gt;
  )
}

const Wrapper = styled.div&lt;{width?: string}&gt;`
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 8px;
`
</code></pre>
<p>위처럼 작성하면 children으로 들어오는 서브 컴포넌트들을 받을 수 있고 순서대로 배치 할 수 있습니다.</p>
<h3 id="3-메인--서브를-묶어서-export하기">3. 메인 &amp; 서브를 묶어서 export하기</h3>
<p>이렇게 구현된 컴포넌트들을 묶어서 export하면 각각의 컴포넌트가 Book의 서브 컴포넌트임을 좀 더 확실하게 할 수 있어 코드의 가독성에 도움이 될 수 있습니다.</p>
<pre><code class="language-typescript">import {OriginalBookGroup} from &quot;./group&quot;
import {OriginalBookImage} from &quot;./image&quot;
import {OriginalBookCaption} from &quot;./caption&quot;
import {OriginalBookFlag} from &quot;./flag&quot;

export const OriginalBook = Object.assign(OriginalBookGroup, {
  Image: OriginalBookImage,
  Caption: OriginalBookCaption,
  Flag: OriginalBookFlag
})

</code></pre>
<h3 id="4-결과">4. 결과</h3>
<pre><code class="language-typescript">import { OriginalBook } from &quot;components/features/original/book&quot;


const BookList = () =&gt; {
      ...

    return (
      ...
            {gerOriginalSchoolBookListResponse.data.book_list.map((item, index) =&gt; (
              &lt;OriginalBook key={item.id} onClick={() =&gt; onClickBookItem(item.id)}&gt;
                &lt;OriginalBook.Flag isTrial={item.is_trial} isLms={item.is_lms_exclusive} openedAt={item.opened_at} /&gt;
                &lt;OriginalBook.Image
                  pngUrl={item.thumbnail_url}
                  svgColorInfo={item.my_school_thumbnail_detail}
                  subsubjectName={item.subsubject_name}
                /&gt;
                &lt;OriginalBook.Caption bookName={item.name} publisherName={item.school_publisher_name} /&gt;
              &lt;/OriginalBook&gt;
            ))}

     ...

    )
}
</code></pre>
<br />

<h2 id="문서화하기">문서화하기</h2>
<p>위에 개선점 중에 하나인 개발자들이 컴포넌트를 사용할 때 혼동을 줄이기 위해서는 컴포넌트에 대한 문서자료가 필요했습니다. 컴포넌트를 문서화하는 도구에는 스토리북과 같은 라이브러리들이 존재하지만 바로 도입하기에는 러닝커브가 있다는 것을 고려하여 일단 피그마에 정리해서 팀원들에게 공유했습니다.</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/ea859953-3a4d-4224-94a6-d821ebc39857/image.png" alt=""></p>
<br />

<hr>
<h1 id="마무리">마무리</h1>
<p>컴포넌트 개선 후 이전보다 간단해진 코드를 보면서 뿌듯함을 느꼈다. 이 점이 리팩토링하는 큰 이유 중 하나인 것 같다. 처음 합성 컴포넌트 개념에 대해서 접했을 때는 잘 이해가 가지 않았는데 내가 실제로 컴포넌트에 적용할 목적으로 계속 보다 보니 이해하기가 수월했던 것 같다. 역시 나는 실제로 내가 부딪혀봐야 아는 것 같다!ㅎㅎ</p>
<p>컴포넌트 개발은 항상 처음 시작할 때는 이 정도는 금방 하겠지? 라는 마음으로 시작하는데 하다 보면 어려운 점이 생겨서 길게 고민하게 되는 것 같다. 근데 나는 이런 고민하는 시간이 나쁘지만은 않은 것 같다. 이런 고민을 통해서 컴포넌트가 단순히 기능이나 UI 단위로 쪼개놓기만 한 것이 아니라 진짜 효율적인 개발을 위한 수준 높은 컴포넌트가 될 수 있는 과정이라는 생각이 들기 때문이다. 그리고 내가 개발한 컴포넌트를 다른 개발자들이 편하게 쓰고 일관된 코드와 UI를 보면 그때만큼 또 뿌듯한 게없는 것같다!ㅋㅋㅋ</p>
<p>이번 컴포넌트 개선을 하면서 아쉬웠던 점은 문서화 부분이다. 예전에 스토리북을 도입하려고 했었는데 그 당시 다른 기능 개발에 우선순위가 밀려서 제대로 도입하지 못했었다. 이제 컴포넌트가 점점 많아지면서 문서화의 필요성에 대해서 크게 느끼고 있다. 하루빨리 스토리북을 제대로 도입해서 제대로 된 디자인시스템을 구축하고 싶다! </p>
<br />
<br />

<h3 id="📚-참고">📚 참고</h3>
<p><a href="https://fe-developers.kakaoent.com/2022/220731-composition-component/">합성 컴포넌트로 재사용성 극대화하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[📲 React Native 웹뷰 통신하기]]></title>
            <link>https://velog.io/@ahn-sujin/React-Native-%EC%9B%B9%EB%B7%B0-%ED%86%B5%EC%8B%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ahn-sujin/React-Native-%EC%9B%B9%EB%B7%B0-%ED%86%B5%EC%8B%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 25 Aug 2024 13:45:26 GMT</pubDate>
            <description><![CDATA[<p>나는 회사에서 앱을 개발하고 있다. 보통 앱 개발을 한다고 하면 React Native, flutter, swift, kotlin을 사용해서 개발할 거라고 생각하지만 나는 Next.js로 개발하고 개발한 화면을 react-native 웹뷰로 띄워서 보여주는 방식으로 개발하고 있다.</p>
<p>처음에는 웹뷰를 통한 개발 방식이 쉽게 이해가지 않았었는데 이제는 익숙해지기도 했고 혹시 예전에 나처럼 웹뷰 개발에 대해서 궁금하거나 어려운 사람들을 위해서 정리해보고자 한다! ✨</p>
<p><img src="https://velog.velcdn.com/images/ahn-sujin/post/41e329d8-1564-459d-8acf-4003a5f09aac/image.png" alt=""></p>
<br />


<h1 id="1-웹뷰란">1. 웹뷰란?</h1>
<hr>
<p>웹뷰는 모바일 어플리케이션에서 웹 콘텐츠(웹 페이지)를 표시하는 컴포넌트이다. 네이티브 앱 내부에 HTML, CSS, Javascript 로 작성된 웹 페이지를 렌더링하는 방식으로 주로 네이티브와 웹을 혼합한 하이브리드 앱에서 많이 사용된다.</p>
<p>우리 회사같은 경우에는 앱 내부의 모든 페이지 및 기능들을 Next.js로 개발하고 있다. 즉, 개발 방식은 웹 개발과 동일하며 Next.js로 개발한 웹페이지를(URL) 웹뷰를 통해 보여준다.</p>
<h2 id="react-native-webview">react-native-webview</h2>
<p>react-native에서 웹뷰를 사용하기 위해서는 react-native-webview 라이브러리가 필요하다.
react-native 초기 버전에는 WebView 가 내장된 컴포넌트였으나, 현재는 별도의 패키지로 분리되어 react-native-webview  라는 이름으로 제공되고 있다.</p>
<p>이 라이브러리를 통해서 웹 페이지와 네이티브 앱 간의 통신과 상호작용을 관리할 수 있다. 간단한 예시 코드를 통해 기본 구조를 살펴보자.</p>
<blockquote>
<p>WebView 컴포넌트에서 제공하는 옵션은 매우 많지만 가장 기본으로 알고 있어야 하는 두가지 정도만 소개하려고 한다.</p>
</blockquote>
<pre><code class="language-typescript">import React from &#39;react&#39;;
import { WebView } from &#39;react-native-webview&#39;;

const MyWebView = () =&gt; {

  const handleMessage = (event) =&gt; {
    alert(&quot;Message from WebView: &quot; + event.nativeEvent.data);
  };

  return (
    &lt;WebView 
      source={{ uri: &quot;https://example.com&quot; }} 
        onMessage={handleMessage}
    /&gt;
  );
};

export default MyWebView;
</code></pre>
<ul>
<li>source<ul>
<li>웹뷰에서 로드할 콘텐츠를 지정하는 옵션이다.</li>
<li>URL을 전달하면 해당 웹 페이지가 웹뷰 내에서 랜더링된다.</li>
</ul>
</li>
<li>onMessage<ul>
<li>웹 페이지와 네이트브 앱 간의 통신을 처리하는 옵션이다.</li>
<li>웹뷰에서 수신한 메세지를 처리합니다. 웹 페이지에서 Javascript로 메세지를 전송하면 해당 함수가 호출된다. (자세한 예시는 아래에서 살펴보도록 할게요)</li>
</ul>
</li>
</ul>
<br />


<h1 id="2-웹뷰-통신이-필요할-때는">2. 웹뷰 통신이 필요할 때는?</h1>
<hr>
<p>웹에서는 제공하지 않는 <strong>앱에서만 제공하는 기능을 필요할 때</strong> 웹뷰 통신을 한다. 내가 경험했던 것을 예로 들자면 모달에서 흔히 볼 수 있는 <code>1일 동안 보지 않기</code> , <code>알림 설정 허용/거부</code> 등 여러가지 기능이 존재한다. 이런 기능들은 일반적인 웹 페이지에서는 구현할 수 없고 네이티브 기능을 직접 활용해야하기 때문에 네이티브 코드에서 처리한 후 그 결과를 웹 페이지로 다시 전달해야한다.</p>
<p>이런 네이티브 기능은 <strong>실제 디바이스나 시뮬레이터에서만 확인</strong>할 수 있기 때문에 개발 중에는 시뮬레이터를 통해 테스트를 진행하며 통신이 올바르게 이루어지는지 확인하는 것이 중요하다.</p>
<br />

<h1 id="3-예시-코드">3. 예시 코드</h1>
<hr>
<p>웹뷰 통신을 할때는 위에서 언급했던 react-native-webview를 통해서 할 수 있다. 이해를 돕기 위해서 자세한 예시를 살펴보도록 하자.</p>
<h2 id="asyncstorage">asyncStorage</h2>
<p>asyncStorage 는 내가 웹뷰 통신에서 가장 자주 사용하는 기능이다. 이름부터 감이 오겠지만 웹에서의 localStorage와 같은 역할을 한다고 생각하면 된다.</p>
<p>웹에서 클라이언트단에서 어떠한 정보를 임시로 저장해야할 때 사용하는 곳이 localStorage인데 앱에서도 어떠한 정보를 디바이스에 저장해야할 때 사용하는 것이 asyncStorage이다.
localStorage 처럼 Key 와 value 값으로 이루어져 저장하고 수정하고 삭제할 수 있다.</p>
<p>asyncStorage가 필요한 경우는 다양하지만 대표적으로 <code>다시 보지 않기</code> 가 있다.</p>
<blockquote>
<p>이해를 돕기위한 예제 코드로 생략된 부분이 있을 수 있으니 이 점 참고해주세요</p>
</blockquote>
<h3 id="1-nextjs---rn으로-데이터-보내기">1. Next.js -&gt; RN으로 데이터 보내기</h3>
<pre><code class="language-typescript">// Next.js

const EventModal = () =&gt; {

    // react native 로 메세지 전송
    const handleClickClose = () =&gt; {
       window.ReactNativeWebView.postMessage(&quot;hideOneDay&quot;)
    }  


    return (
        &lt;div&gt;
           ...
           &lt;button onClick={handleClickClose}&gt;다시 보지 않기&lt;/button&gt;
        &lt;/div&gt;
    )

}

export default EventModal

</code></pre>
<ul>
<li><code>window.ReactNativeWebView.postMessage(&quot;hideOneDay&quot;)</code>를 호출하여, 웹 페이지에서 React Native 웹뷰로 &quot;hideOneDay&quot;라는 메세지를 전달한다.</li>
<li>이 메세지는  React Native 애플리케이션으로 전달되어 특정 동작(예: &quot;다시 보지 않기&quot; 설정)을 트리거하게 된다.</li>
</ul>
<h3 id="2-rn에서-수신된-데이터-저장하기">2. RN에서 수신된 데이터 저장하기</h3>
<pre><code class="language-typescript">// React Native

import { WebView } from &#39;react-native-webview&#39;;
import AsyncStorage from &#39;@react-native-async-storage/async-storage&#39;;

const App = () =&gt; {

  // 웹뷰에서 받은 메시지 처리
  const handleMessage = async (event) =&gt; {
    const message = event.nativeEvent.data;
    if (message === &quot;hideOneDay&quot;) {
      try {
        // AsyncStorage에 데이터 저장
        await AsyncStorage.setItem(&quot;hideOneDay&quot;, &quot;true&quot;);
        alert(&quot;모달을 다시 보지 않도록 설정이 완료되었습니다.&quot;);
      } catch (error) {
        console.error(&quot;Error saving data to AsyncStorage&quot;, error);
      }
    }
  };

  return (
   &lt;WebView 
      source={{ uri: &quot;https://example.com&quot;}} 
      onMessage={handleMessage}
    /&gt;
  )
}


export default App
</code></pre>
<ul>
<li>handleMessage를 통해서 수신된 메세지를 처리한다.<ul>
<li><code>event.nativeEvent.data</code>를 통해 수신된 메시지를 확인하고, 메시지가 &quot;hideOneDay&quot;일 때, AsyncStorage에 &quot;hideOneDay&quot; 키로 &quot;true&quot; 값을 저장하게 된다.</li>
</ul>
</li>
<li>정상적으로 저장에 성공하면 alert이 노출된다.</li>
</ul>
<h3 id="3-rn에서-저장된-데이터-불러오기">3. RN에서 저장된 데이터 불러오기</h3>
<pre><code class="language-typescript">// React Native

import { WebView } from &#39;react-native-webview&#39;;
import AsyncStorage from &#39;@react-native-async-storage/async-storage&#39;;

const App = () =&gt; {
  const webViewRef = useRef(null);

  ...

  // 저장된 데이터 불러와 웹뷰로 전송
  useEffect(() =&gt; {
      const checkAsyncStorage = async () =&gt; {
       try {
        const hideOneDay = await AsyncStorage.getItem(&quot;hideOneDay&quot;);

        if (hideOneDay === &#39;true&#39; &amp;&amp; webViewRef.current) {
          // 저장된 데이터가 있으면 웹뷰로 메시지 전송
          webViewRef.current.postMessage(&quot;hideModal&quot;);
        }
      } catch (error) {
        console.error(&#39;Error retrieving data from AsyncStorage&#39;, error);
      }
    };

    checkAsyncStorage();
  }, [])

  return (
   &lt;WebView 
      ref={webViewRef}
      source={{ uri: &quot;https://example.com&quot;}} 
      onMessage={handleMessage}
    /&gt;
  )
}


export default App
</code></pre>
<ul>
<li>webViewRef 설정을 통해서 웹뷰에 직접 접근할 수 있다.</li>
<li>useEffect를 통해 checkAsyncStorage 함수가 실행되고 AsyncStorage 에서 &quot;hideOneDay&quot; 값을 불러온다. 만약 true로 설정되어 있으면 <code>webViewRef.current.postMessage(&quot;hideModal&quot;)</code> 로 호출하여 웹뷰로 &quot;hideModal&quot; 을 전송하며 이를 통해 웹 페이지에서 모달을 숨길 수 있다.</li>
</ul>
<h3 id="4-nextjs에서-모달-숨기기">4. Next.js에서 모달 숨기기</h3>
<pre><code class="language-typescript">
import EventModal from &quot;./EventModal&quot;

const Home = () =&gt; {

  // 모달의 표시 여부를 상태로 관리
  const [isModalVisible, setModalVisible] = useState(true);

  useEffect(() =&gt; {
    const handleMessage = (event: MessageEvent) =&gt; {
      // 메시지 내용 확인
      if (event.data === &quot;hideModal&quot;) {
        // 모달 숨기기
        setModalVisible(false);
      }
    };

    // 메시지 이벤트 리스너 등록
    window.addEventListener(&quot;message&quot;, handleMessage);

    // 컴포넌트 언마운트 시 이벤트 리스너 제거
    return () =&gt; {
      window.removeEventListener(&quot;message&quot;, handleMessage);
    };
  }, []);

    return (
     &lt;div&gt;
        {isModalVisible &amp;&amp; &lt;EventModal /&gt;}
           &lt;h1&gt;메인 페이지&lt;/h1&gt;
      &lt;/div&gt;
    )
}

export default Home

</code></pre>
<ul>
<li>useEffect를 통해 페이지가 랜더링되면 <code>window.addEventListener(&quot;message&quot;, handleMessage)</code> 를 호출하고 handleMessage 함수를 통해 &quot;hideModal&quot; 메세지를 받으면 isModalVisible 상태값을 업데이트 시켜 모달을 숨긴다.</li>
</ul>
<br />



<h1 id="4-마무리">4. 마무리</h1>
<hr>
<p>입사하고 같이 취업을 준비하던 동기들을 만나면 _<strong>&quot;회사에서 어떤 일을 하고 있어요?&quot;</strong>_가 가장 많이 듣는 질문이었다. 그리고 앱 개발을 한다고 하면 _<strong>&quot;React Native 사용하시나요?&quot;</strong>_라고 물어보시는데, 이때마다 난감(?)할 때가 있었다. 왜냐하면 우리 앱은 React Native로 되어있긴 하지만 그때 나는 React Native를 전혀 만지지 않고 있었고 Next.js로만 개발하고 있었기 때문이다.</p>
<p>생각해보면 그때 나는 웹뷰 개발에 대해 제대로 이해하고 있지 못해서 남에게 설명하는 데 자신이 없었던 것 같다. 하지만 언제나 그렇듯 막상 겪어보면 별거 없고 익숙해지기 마련이다. 2년차 개발자가 된 지금은 웹뷰 앱 개발에 대해 누가 물어본다면 자세하게 설명해줄 수 있다. 그래서 혹시 웹뷰 개발을 이제 막 시작했거나 궁금증을 가진 분들에게 도움이 되었으면 좋겠다는 마음으로 이 글을 쓰게 됐다.</p>
<p>물론 글을 정리하면서 나도 웹뷰에 대해 다시 한 번 정리하게 되어 좋았다. 이래서 블로그를 꾸준히 써야 하나보다ㅎㅎ 기록의 중요성!</p>
<p>사실 회사 코드에서 내가 사용한 부분을 예제로 가져오고 싶었는데, 회사 코드에서는 sendMessage 부분(RN 통신)을 lib 폴더에 모듈화해서 사용하고 있어서 내가 소개하고 싶었던 부분 외에도 복잡한 부분이 많아 직접 예제 코드를 만들었다. 이번 포스팅에서는 RN 통신의 기본 개념에 대해서만 소개하려고 했기 때문에 너무 딥한 내용은 최대한 덜어내려고 했다. RN 통신의 심화 버전은 다음 포스팅에서 다뤄야겠다.</p>
<br />
<br />

<h3 id="📚-참고">📚 참고</h3>
<ol>
<li><a href="https://medium.com/@tellingme/frontend-webview%EB%A1%9C-react%EB%A5%BC-app%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0-30bbe0533f30">[FrontEnd] Webview로 React를 App으로 만들기</a></li>
<li><a href="https://kyounghwan01.github.io/blog/React/react-native/react-native-webview/">react native webview 사용법</a></li>
</ol>
]]></description>
        </item>
    </channel>
</rss>