<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Doo</title>
    <link>https://doohyeong.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 30 May 2026 12:31:19 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Hyeong Nim</managingEditor>
    <item>
      <title>2025 FE CONF</title>
      <link>https://doohyeong.tistory.com/265</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;작년에 비해 축소된 스폰서 규모와 사은품에서 현재의 경제 상황을 엿볼 수 있었습니다. 그럼에도 불구하고, 1년 동안의 고민과 경험을 공유하려는 발표자들, 그리고 새로운 지식에 목마른 참가자들의 열기로 컨퍼런스는 붐볐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 컨퍼런스에서 가장 기대했던 세션은 '모노레포 절망편'이었습니다. 현재 저희 팀이 모노레포 도입을 고려 중이라 더욱 관심이 갔습니다. 과거에 모노레포를 사용하며 겪었던 불편함과 현재의 장점(패키지 공유, CI/CD 간소화) 사이에서 고민하던 저에게 큰 도움이 될 세션이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아쉬운 점도 있었습니다. 유명 연사들이 많지 않았고, 일부 세션은 기대만큼 깊이 있는 내용이 아니었습니다. 하지만 개발자로서의 경험이 쌓일수록 작은 인사이트 하나가 서비스 설계에 큰 영향을 준다는 것을 알기에, 이번 컨퍼런스도 분명 의미 있는 시간이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전반적으로 아쉬움과 기대가 교차했던 자리였지만, 동료 개발자들과 함께 고민을 나누고 배우는 소중한 경험이었습니다.&lt;br /&gt;제가 들었던 각 세션에 대한 자세한 내용은 아래 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.notion.so/25860a51546180fbaee8c068f78ea22d?pvs=21&quot;&gt;&lt;b&gt;스벨트를 통해 리액트 더 잘 이해하기&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.notion.so/memo-React-Compiler-25860a5154618094bf8dea14eaa64235?pvs=21&quot;&gt;&lt;b&gt;'memo'를 지울 결심 : React Compiler가 제안하는 미래&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.notion.so/25860a51546180ad96cadc2a9ae3105d?pvs=21&quot;&gt;&lt;b&gt;중요하지만 긴급하지 않은 일, 그럼에도 계획해야 하는 웹 접근성&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.notion.so/DX-25860a515461809faa22ee7035e18191?pvs=21&quot;&gt;&lt;b&gt;음성 인터페이스 개발에서 DX 향상하기: 모델 비교부터 직관적인 인터페이스 설계&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.notion.so/1-10-SEO-25860a51546180cfbbcfe215be770524?pvs=21&quot;&gt;&lt;b&gt;1년에 10억 원을 절약한, 강남언니의 SEO 웹 전략 공개&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.notion.so/TanStack-Query-25860a5154618015afcec71d60c655fc?pvs=21&quot;&gt;&lt;b&gt;TanStack Query 너머를 향해! 쿼리를 라우트까지 전파시키기&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.notion.so/14-1-25860a51546180e29dcde4951f5aa1f0?pvs=21&quot;&gt;&lt;b&gt;모노레포 절망편, 14개 레포로 부활하기까지 걸린 1년&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;</description>
      <category>life</category>
      <author>Hyeong Nim</author>
      <guid isPermaLink="true">https://doohyeong.tistory.com/265</guid>
      <comments>https://doohyeong.tistory.com/265#entry265comment</comments>
      <pubDate>Sat, 23 Aug 2025 23:05:30 +0900</pubDate>
    </item>
    <item>
      <title>[정리] React Server Components는 단지 &amp;ldquo;서버에서 돌리는 컴포넌트&amp;rdquo;가 아닙니다</title>
      <link>https://doohyeong.tistory.com/264</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;처음 React Server Components(RSC)를 접했을 때는 그저 SSR(서버사이드 렌더링)의 새 버전 정도로 생각했습니다.&lt;br /&gt;하지만 최근 Dan Abramov의 글을 읽고 나서야 그 생각이 완전히 바뀌었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;RSC는 단순한 기능이 아니라, &lt;/span&gt;&lt;b&gt;자바스크립트 모듈 시스템의 본질을 다시 바라보게 만드는 프로그래밍 모델&lt;/b&gt;&lt;span&gt;이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;모듈 시스템은 사람을 위한 것입니다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터는 모듈을 알지 못합니다. 컴퓨터는 실행 가능한 하나의 프로그램을 필요로 할 뿐이죠.&lt;br /&gt;&lt;span&gt;모듈은 &lt;/span&gt;&lt;b&gt;사람이 코드를 더 쉽게 작성하고 이해하기 위해 만든 추상화&lt;/b&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈은 다음을 가능하게 합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복잡한 프로그램을 파일 단위로 나눌 수 있게 하고&lt;/li&gt;
&lt;li&gt;코드 중 일부만 외부에 노출할 수 있게 하며&lt;/li&gt;
&lt;li&gt;동일한 코드를 여러 곳에서 재사용할 수 있게 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript에서는 이를 &lt;span&gt;import&lt;/span&gt;, &lt;span&gt;export&lt;/span&gt; 키워드를 통해 구현합니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;중요한 건 이 키워드들이 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;b&gt;실행 시점&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;에 실제로 어떤 의미를 갖느냐입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;JavaScript의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;import &lt;/b&gt;&lt;b&gt;는 &amp;ldquo;복사 붙여넣기&amp;rdquo;가 아닙니다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 &lt;span&gt;import&lt;/span&gt;는 외부 코드를 &amp;ldquo;불러오는&amp;rdquo; 것이라 생각하지만, C의 &lt;span&gt;#include&lt;/span&gt;처럼 단순한 복사 개념은 아닙니다.&lt;br /&gt;&lt;span&gt;JavaScript 모듈 시스템은 &lt;/span&gt;&lt;b&gt;모듈을 싱글턴(Singleton)으로 처리합니다.&lt;br /&gt;&lt;/b&gt;즉, 동일한 모듈이 여러 곳에서 불러와지더라도 실행은 단 한 번만 발생합니다.&lt;br /&gt;이 덕분에 각 모듈은 자신의 내부 상태를 안전하게 유지할 수 있으며, 전체 프로그램은 효율적인 구조로 구성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제는 &amp;ldquo;두 개의 환경&amp;rdquo;이 등장할 때 시작됩니다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 설명한 모듈 시스템은 단일 런타임(예: 브라우저 또는 Node.js)을 전제로 합니다.&lt;br /&gt;&lt;span&gt;하지만 실제 웹 애플리케이션은 &lt;/span&gt;&lt;b&gt;백엔드와 프런트엔드라는 두 환경에 걸쳐 실행&lt;/b&gt;&lt;span&gt;됩니다.&lt;br /&gt;&lt;/span&gt;이 두 환경은 각자 독립적인 모듈 시스템을 갖고 있고, 서로의 메모리에 접근하지 못합니다.&lt;br /&gt;그럼에도 불구하고, 우리는 종종 두 환경 간에 코드를 공유하고 싶어 합니다. 예컨대 유틸 함수, 밸리데이터, 모델 타입 등.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;공유하고 싶은 코드 vs 공유하면 안 되는 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공유는 쉬워 보입니다. 그냥 같은 파일을 양쪽에서 &lt;span&gt;import&lt;/span&gt; 하면 되니까요. 하지만 문제는 &amp;ldquo;공유하면 안 되는 코드&amp;rdquo;가 섞여 있을 때 생깁니다. 예를 들어 &lt;span&gt;fs.readFileSync&lt;/span&gt; 같은 Node.js 전용 API를 사용한 코드가 프런트엔드에 포함되면, &lt;span&gt;&lt;b&gt;런타임 오류&lt;/b&gt;&lt;/span&gt;가 발생합니다.&lt;br /&gt;더 심각한 경우는 서버 비밀 정보가 클라이언트 번들에 그대로 노출되는 경우입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이때 필요한 것이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;server-only&lt;/b&gt;&lt;b&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;client-only&lt;/b&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 독약(Poison Pill)입니다&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제를 사전에 방지하기 위해, RSC는 &lt;span&gt;&lt;b&gt;특정 모듈이 어느 환경에서만 실행되어야 하는지를 명시적으로 선언&lt;/b&gt;&lt;/span&gt;할 수 있는 도구를 제공합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1752501058505&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// secrets.js
import 'server-only';

export const SECRET = '1234';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 프런트엔드 번들에 포함되려 할 경우 &lt;span&gt;&lt;b&gt;빌드 에러&lt;/b&gt;&lt;/span&gt;를 일으킵니다. 즉, RSC는 &amp;ldquo;코드 위치&amp;rdquo;를 결정하지 않지만, &amp;ldquo;어디에 포함되어선 안 되는지&amp;rdquo;를 선언적으로 제어할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 철학은 단순하지만 강력합니다: &lt;b&gt;잘못된 실행 환경으로의 침투를 예방하려면, 애초에 포함 자체를 막아라.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&amp;lsquo;use client&amp;rsquo;, &amp;lsquo;use server&amp;rsquo;는 실행 위치를 &amp;ldquo;바꾸는&amp;rdquo; 게 아닙니다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 개발자들이 &lt;span&gt;'use client'&lt;/span&gt;를 컴포넌트 실행 위치를 지정하는 태그라고 오해합니다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하지만 이 지시어들은 &lt;/span&gt;&lt;b&gt;프런트엔드와 백엔드 모듈 시스템 사이의 &amp;ldquo;문(Door)&amp;rdquo;을 여는 역할&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;을 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;즉, &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;'use client'&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;가 선언된 모듈은 서버 코드에서 직접 실행되는 것이 아니라, &lt;/span&gt;React가 해당 함수를 &lt;span&gt;&lt;b&gt;스크립트 태그로 프런트엔드에 전달하고 연결&lt;/b&gt;&lt;/span&gt;할 수 있도록 만듭니다.&lt;/p&gt;
&lt;pre id=&quot;code_1752501101319&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ClientButton.tsx
'use client';
export function sayHello() {
  alert('Hi!');
}

// ServerPage.tsx
import { sayHello } from './ClientButton';

export function Page() {
  return &amp;lt;button onClick={sayHello}&amp;gt;눌러보세요&amp;lt;/button&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;하나의 프로그램, 두 개의 실행 환경&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Server Components는 이 모든 흐름을 통합합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;각 환경은 독립적인 모듈 시스템을 유지합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;일반 모듈은 자유롭게 공유되며&lt;/b&gt;&lt;/span&gt;, 실행 위치는 현재 컨텍스트에 따라 자동 결정됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특정 환경에만 존재해야 하는 모듈은 server-only, client-only로 제한합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다른 환경의 코드를 참조하고 싶을 땐, 'use client', 'use server'로 문을 엽니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결과적으로 RSC는 &lt;/span&gt;&lt;b&gt;&amp;ldquo;하나의 프로그램이 두 환경에서 실행되는 구조&amp;rdquo;를 안전하고 명확하게 지원하는 모델&lt;/b&gt;&lt;span&gt;을 제공합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리하며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Server Components는 &amp;ldquo;SSR보다 빠른 렌더링&amp;rdquo;이라는 기능적 개선만을 위한 기술이 아닙니다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이는 우리가 &lt;/span&gt;&lt;b&gt;프런트엔드와 백엔드가 나뉘어 있다는 전통적 사고방식&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;에서 벗어나, &lt;/span&gt;&lt;b&gt;&amp;ldquo;하나의 모듈 시스템이 두 환경에 걸쳐 작동하는 구조&amp;rdquo;를 어떻게 만들 것인가&lt;/b&gt;&lt;span&gt;라는 질문에 대한 실질적인 해답입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;프런트와 백을 나누는 것이 아니라, &lt;/span&gt;&lt;b&gt;&amp;ldquo;의도된 실행 위치를 명확히 표현할 수 있는 언어 체계&amp;rdquo;를 만든 것&lt;/b&gt;&lt;span&gt;입니다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그 언어가 바로 RSC이고, 그 문법이 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;use client&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;server-only&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;원문 &amp;amp; 참조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://overreacted.io/how-imports-work-in-rsc/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://overreacted.io/how-imports-work-in-rsc/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1752501170065&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;How Imports Work in RSC &amp;mdash; overreacted&quot; data-og-description=&quot;A layered module system.&quot; data-og-host=&quot;overreacted.io&quot; data-og-source-url=&quot;https://overreacted.io/how-imports-work-in-rsc/&quot; data-og-url=&quot;https://overreacted.io/how-imports-work-in-rsc/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/nlPvr/hyZngnA8Ah/yrRY94CsRsGMtWUfmhbbU0/img.png?width=1200&amp;amp;height=630&amp;amp;face=1095_65_1138_111,https://scrap.kakaocdn.net/dn/pEhaG/hyZjmCUDCD/DOsnKV7JwoaP80ftdrcTjk/img.png?width=1200&amp;amp;height=630&amp;amp;face=1095_65_1138_111&quot;&gt;&lt;a href=&quot;https://overreacted.io/how-imports-work-in-rsc/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://overreacted.io/how-imports-work-in-rsc/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/nlPvr/hyZngnA8Ah/yrRY94CsRsGMtWUfmhbbU0/img.png?width=1200&amp;amp;height=630&amp;amp;face=1095_65_1138_111,https://scrap.kakaocdn.net/dn/pEhaG/hyZjmCUDCD/DOsnKV7JwoaP80ftdrcTjk/img.png?width=1200&amp;amp;height=630&amp;amp;face=1095_65_1138_111');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;How Imports Work in RSC &amp;mdash; overreacted&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A layered module system.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;overreacted.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nodejs.org/api/packages.html#conditional-exports&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://nodejs.org/api/packages.html#conditional-exports&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1752501215502&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Modules: Packages | Node.js v24.4.0 Documentation&quot; data-og-description=&quot;Modules: Packages# Introduction# A package is a folder tree described by a package.json file. The package consists of the folder containing the package.json file and all subfolders until the next folder containing another package.json file, or a folder nam&quot; data-og-host=&quot;nodejs.org&quot; data-og-source-url=&quot;https://nodejs.org/api/packages.html#conditional-exports&quot; data-og-url=&quot;https://nodejs.org/api/packages.html#conditional-exports&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://nodejs.org/api/packages.html#conditional-exports&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nodejs.org/api/packages.html#conditional-exports&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Modules: Packages | Node.js v24.4.0 Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Modules: Packages# Introduction# A package is a folder tree described by a package.json file. The package consists of the folder containing the package.json file and all subfolders until the next folder containing another package.json file, or a folder nam&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nodejs.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>software engineering/frontend</category>
      <author>Hyeong Nim</author>
      <guid isPermaLink="true">https://doohyeong.tistory.com/264</guid>
      <comments>https://doohyeong.tistory.com/264#entry264comment</comments>
      <pubDate>Mon, 14 Jul 2025 22:52:51 +0900</pubDate>
    </item>
    <item>
      <title>Next.js 프로젝트의 SEO 최적화 전략</title>
      <link>https://doohyeong.tistory.com/263</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요, 이번 포스트에서는 Next.js 프로젝트에서 활용할 수 있는 효과적인 SEO 최적화 전략에 대해 이야기해보려고 합니다. 검색 엔진 최적화(SEO)는웹사이트의 가시성을 높이고 유기적 트래픽을 증가시키는 중요한 요소입니다. 특히 네이버와 같은 국내 검색 엔진에서의 노출은 모든 웹 서비스에 매우 중요하죠.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 이미지 &lt;code&gt;alt&lt;/code&gt; 속성 최적화&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반적인 문제점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 프로젝트에서 배너 이미지의 alt 속성이 '배너01', '배너02'와 같이 의미 없게 설정되어 있어 SEO에 부정적 영향을 주고 있습니다. 이미지를 검색 결과에 노출하기 위해서는 더 효과적인 방법이 필요합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;의미 있는 키워드(예: 제품 카테고리, 제품 특징)&lt;/b&gt;로 alt 텍스트를 구성하는 것이 검색엔진 노출에 유리합니다.&lt;/li&gt;
&lt;li&gt;SEO 우선순위: &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;, URL 일치도, 키워드 밀도, alt, meta, 사용자 체류 시간 등이 복합적으로 작용합니다.&lt;/li&gt;
&lt;li&gt;키워드는 페이지 내 여러 위치에서 자연스럽게 반복 노출되는 것이 중요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;// 개선 전
&amp;lt;img src=&quot;/banner-image.jpg&quot; alt=&quot;배너01&quot; /&amp;gt;

// 개선 후
&amp;lt;img src=&quot;/banner-image.jpg&quot; alt=&quot;신규 제품 출시 기념 프로모션&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 구조화 데이터 위치 및 적용 전략&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최적화 방향&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 페이지에 공통으로 &lt;a href=&quot;https://searchadvisor.naver.com/guide/structured-data-intro&quot;&gt;구조화 데이터&lt;/a&gt;를 삽입하는 것보다 &lt;b&gt;콘텐츠 목적에 맞는 페이지별 맞춤 삽입이 SEO에 더 유리&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;홈페이지, 제품 페이지, 콘텐츠 페이지 등 각각의 목적에 맞는 구조화 데이터를 적용해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;효과적인 구현 방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;효율적인 방법: 각 페이지 컴포넌트에서 목적에 맞는 구조화 데이터 적용&lt;/li&gt;
&lt;li&gt;지양해야 할 방법: Layout.tsx 같은 공통 컴포넌트에서 모든 페이지에 동일한 구조화 데이터 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;페이지별 추천 구조화 데이터&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;상품/제품 페이지&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;BreadcrumbList&lt;/code&gt;: 사이트 내 현재 페이지 위치 표시&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Product&lt;/code&gt;: 제품 정보 구조화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AggregateRating&lt;/code&gt;: 평점 정보&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Review&lt;/code&gt;: 리뷰 정보&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ItemList&lt;/code&gt;: 이미지 슬라이드용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;콘텐츠 페이지&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Article&lt;/code&gt;: 일반 콘텐츠 구조화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HowTo&lt;/code&gt;: 프로세스를 보여주는 콘텐츠&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;홈페이지&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ItemList&lt;/code&gt;: 제품/콘텐츠 목록 구조화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WebSite&lt;/code&gt;: 사이트 정보 구조화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Organization&lt;/code&gt;: 조직 정보 구조화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 예: 제품 페이지의 구조화 데이터 적용
export default function ProductDetail({ product }) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;script
        type=&quot;application/ld+json&quot;
        dangerouslySetInnerHTML={{
          __html: JSON.stringify({
            &quot;@context&quot;: &quot;https://schema.org&quot;,
            &quot;@type&quot;: &quot;Product&quot;,
            &quot;name&quot;: product.name,
            &quot;image&quot;: product.imageUrl,
            &quot;description&quot;: product.description,
            &quot;offers&quot;: {
              &quot;@type&quot;: &quot;Offer&quot;,
              &quot;price&quot;: product.price,
              &quot;priceCurrency&quot;: &quot;KRW&quot;
            }
          })
        }}
      /&amp;gt;

      {/* 페이지 콘텐츠 */}
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Heading 구조 최적화&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Heading 태그의 중요성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Heading 태그는 페이지의 구조와 콘텐츠의 계층을 나타내는 중요한 요소입니다.&lt;/li&gt;
&lt;li&gt;검색엔진은 Heading 태그를 통해 페이지의 주요 주제와 콘텐츠를 파악합니다.&lt;/li&gt;
&lt;li&gt;특히 &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; 태그는 페이지의 주요 주제를 나타내는 가장 중요한 요소입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;효과적인 Heading 구조 설계&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핵심 키워드를 명확하게 전달하는 간단한 구조가 효과적입니다.&lt;/li&gt;
&lt;li&gt;마케팅 문구나 부가 정보는 하위 Heading 태그에 배치하는 것이 좋습니다.&lt;/li&gt;
&lt;li&gt;검색엔진이 이해하기 쉽도록 논리적인 계층 구조를 만드는 것이 중요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;// 일반적인 예시
&amp;lt;h1&amp;gt;무선 이어버드&amp;lt;/h1&amp;gt;
&amp;lt;h2&amp;gt;신제품 출시 기념 특별 할인&amp;lt;/h2&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추천 툴&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;크롬 확장 프로그램 HeadingsMap&lt;/b&gt; &amp;rarr; 페이지 내 h1~h6 구조 점검에 유용&lt;/li&gt;
&lt;li&gt;이 도구를 사용하여 페이지의 헤딩 구조를 시각화하고 개선할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. SSR 적용 영역 확대&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SEO 관점에서의 SSR 중요성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네이버 등 국내 검색엔진은 CSR(클라이언트 사이드 렌더링) 영역의 콘텐츠를 잘 인식하지 못하는 경향이 있습니다.&lt;/li&gt;
&lt;li&gt;SSR(서버 사이드 렌더링) 처리를 통해 &lt;b&gt;사이트 신뢰도 향상 + 키워드 노출 + 콘텐츠 인덱싱&lt;/b&gt;이 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;효과적인 SSR 적용 전략&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핵심 콘텐츠, 제품 정보, 리뷰 등은 SSR로 처리하여 검색엔진이 인식할 수 있도록 합니다.&lt;/li&gt;
&lt;li&gt;단기 프로모션, 광고성 정보는 CSR로 처리하여 검색 결과에 지속적으로 노출되는 것을 방지합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 프로모션 영역을 클라이언트 컴포넌트로 분리
'use client'
export default function PromotionBanner() {
  // 프로모션 관련 렌더링 로직
}

// 핵심 콘텐츠는 서버 컴포넌트로 구현
export default function MainContent({ content }) {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;{content.title}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{content.description}&amp;lt;/p&amp;gt;
      {/* 주요 콘텐츠 정보 */}
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. &lt;code&gt;generateMetadata&lt;/code&gt; 기능 활용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js 13 이상에서는 &lt;code&gt;generateMetadata&lt;/code&gt; 함수를 사용하여 각 페이지별로 동적 메타데이터를 생성할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;활용 방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이지마다 적절한 &lt;code&gt;title&lt;/code&gt;과 &lt;code&gt;meta description&lt;/code&gt;을 동적으로 설정&lt;/li&gt;
&lt;li&gt;핵심 키워드를 포함한 메타데이터를 구성하여 검색 엔진 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// pages/[slug]/page.jsx
export async function generateMetadata({ params }) {
  // 데이터 가져오기
  const pageData = await fetchPageData(params.slug);

  return {
    title: pageData.title,
    description: pageData.summary,
    openGraph: {
      title: pageData.title,
      description: pageData.summary,
      images: [{ url: pageData.imageUrl }],
    },
  };
}

export default function ContentPage({ params }) {
  // 페이지 컴포넌트 내용
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최적화 팁&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메타 description은 150-160자 내외로 핵심 정보를 담아 작성&lt;/li&gt;
&lt;li&gt;타이틀에는 중요 키워드를 앞쪽에 배치&lt;/li&gt;
&lt;li&gt;메타데이터를 키워드로 과도하게 채우는 것은 오히려 역효과&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js에서 SEO를 최적화하는 것은 지속적인 개선이 필요한 과정입니다. 이 포스트에서 소개한 기법들은 어떤 Next.js 프로젝트에도 적용할 수 있는 범용적인 전략입니다. 특히 한국 시장에서는 네이버 검색엔진에 최적화하는 것이 중요하며, SSR을 적극 활용하고 구조화 데이터를 적절히 적용하는 것이 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 전략들을 적용하면 웹사이트의 검색 엔진 노출과 유기적 트래픽 증가에 긍정적인 효과가 있을 것입니다. 프론트엔드 개발자라면 SEO 최적화에 대한 이해가 프로젝트 성공에 큰 도움이 됩니다.&lt;/p&gt;</description>
      <category>software engineering/frontend</category>
      <author>Hyeong Nim</author>
      <guid isPermaLink="true">https://doohyeong.tistory.com/263</guid>
      <comments>https://doohyeong.tistory.com/263#entry263comment</comments>
      <pubDate>Sat, 17 May 2025 12:58:45 +0900</pubDate>
    </item>
    <item>
      <title>Apollo Client Refetch 구조 개선하기: SSE 알림과 Error 핸들링까지</title>
      <link>https://doohyeong.tistory.com/262</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스트에서는 Apollo Client를 사용하면서 Refetch를 효율적으로 구성하고, 실시간 알림을 SSE(Server-Sent Events)로 처리하는 방법을 소개합니다. 그리고 그 과정에서 고민했던 ErrorLink, ErrorPolicy 설정, 그리고 Subscription을 사용하지 않은 이유까지 함께 정리합니다.&lt;/p&gt;
&lt;hr data-end=&quot;465&quot; data-start=&quot;462&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;475&quot; data-start=&quot;467&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기존 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;589&quot; data-start=&quot;477&quot; data-ke-size=&quot;size16&quot;&gt;우리는 Hasura GraphQL을 메인 API 서버로 사용하고 있었습니다.&lt;br /&gt;그런데 서비스 도중 실시간 데이터 갱신이 필요한 순간이 생겼고, 이를 해결하기 위해 &quot;알림 시스템&quot;을 구축해야 했습니다.&lt;/p&gt;
&lt;p data-end=&quot;700&quot; data-start=&quot;591&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 Hasura의 Subscription 기능을 고려했지만,&lt;br /&gt;비용과 복잡성, 운영 안정성 문제로 Subscription이 아니라 SSE를 이용해 별도 알림 서버를 구축하기로 결정했습니다.&lt;/p&gt;
&lt;hr data-end=&quot;705&quot; data-start=&quot;702&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;736&quot; data-start=&quot;707&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;SSE를 통해 알림 수신 및 Refetch 처리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;913&quot; data-start=&quot;738&quot; data-ke-size=&quot;size16&quot;&gt;SSE로 알림 메시지를 수신하면, 특정 도메인 그룹에 따라 필요한 데이터를 Refetch하는 구조를 설계했습니다.&lt;br /&gt;여기서 가장 중요한 부분은, &lt;b&gt;어떤 Query Document를 Refetch할지 명시하지 않아도&lt;/b&gt;,&lt;br /&gt;&lt;b&gt;연관 도메인만 지정해서 자동으로 Refetch가 일어나는 구조&lt;/b&gt;를 만든 것입니다.&lt;/p&gt;
&lt;h3 data-end=&quot;934&quot; data-start=&quot;915&quot; data-ke-size=&quot;size23&quot;&gt;Refetch Link 구조&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1115&quot; data-start=&quot;936&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1028&quot; data-start=&quot;936&quot;&gt;context: { refetchable: true, refetchGroups: [...] }&lt;br /&gt;➔ Query에 이런 형태로 context를 설정합니다.&lt;/li&gt;
&lt;li data-end=&quot;1115&quot; data-start=&quot;1029&quot;&gt;Mutation 성공 시 ➔ client.context.refetchGroup 값을 읽어와서 관련된 Query들을 자동으로 Refetch합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1197&quot; data-start=&quot;1117&quot; data-ke-size=&quot;size16&quot;&gt;덕분에 개발자가 직접 Query Document를 일일이 지정하지 않아도, &lt;b&gt;도메인 기준으로만 관리하는 깔끔한 구조&lt;/b&gt;를 만들 수 있었습니다.&lt;/p&gt;
&lt;hr data-end=&quot;1202&quot; data-start=&quot;1199&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1227&quot; data-start=&quot;1204&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Refetch 도중 발생한 에러 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1268&quot; data-start=&quot;1229&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 이 구조가 잘 작동했지만, 특정 상황에서 문제가 발생했습니다.&lt;/p&gt;
&lt;p data-end=&quot;1278&quot; data-start=&quot;1270&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1388&quot; data-start=&quot;1279&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1301&quot; data-start=&quot;1279&quot;&gt;어떤 데이터가 서버에서 삭제되었는데,&lt;/li&gt;
&lt;li data-end=&quot;1332&quot; data-start=&quot;1302&quot;&gt;SSE 알림을 받아서 Refetch를 시도했을 때,&lt;/li&gt;
&lt;li data-end=&quot;1388&quot; data-start=&quot;1333&quot;&gt;이미 삭제된 데이터를 다시 요청하게 되면서 &lt;b&gt;404 Not Found&lt;/b&gt; 에러가 발생했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1424&quot; data-start=&quot;1390&quot; data-ke-size=&quot;size16&quot;&gt;이 에러는 Suspense Query 환경에서는 심각했습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1452&quot; data-start=&quot;1426&quot; data-ke-size=&quot;size23&quot;&gt;Suspense 사용 중 문제가 된 이유&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1556&quot; data-start=&quot;1454&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1500&quot; data-start=&quot;1454&quot;&gt;Suspense 기반 Query는 네트워크 에러를 내부적으로 다시 던져버립니다.&lt;/li&gt;
&lt;li data-end=&quot;1556&quot; data-start=&quot;1501&quot;&gt;그 결과, 화면이 깨지거나, Not Found 페이지가 의도치 않게 노출되는 문제가 생겼습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1608&quot; data-start=&quot;1558&quot; data-ke-size=&quot;size16&quot;&gt;결국, 이런 상황에서는 Suspense Query가 적합하지 않다는 결론을 내렸습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1619&quot; data-start=&quot;1610&quot; data-ke-size=&quot;size23&quot;&gt;해결 방법&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1707&quot; data-start=&quot;1621&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1666&quot; data-start=&quot;1621&quot;&gt;&lt;b&gt;Suspense를 제거하고, 일반 useQuery로 전환&lt;/b&gt;했습니다.&lt;/li&gt;
&lt;li data-end=&quot;1707&quot; data-start=&quot;1667&quot;&gt;&lt;b&gt;Apollo의 ErrorPolicy를 all로 변경&lt;/b&gt;했습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;1799&quot; data-start=&quot;1709&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면, Query 중 일부 필드에 에러가 발생해도 전체 데이터가 undefined로 되지 않고, 가능한 데이터는 유지한 채 에러 정보를 받을 수 있습니다.&lt;/p&gt;
&lt;hr data-end=&quot;1804&quot; data-start=&quot;1801&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1835&quot; data-start=&quot;1806&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;ErrorPolicy: 왜 all을 선택했을까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1879&quot; data-start=&quot;1837&quot; data-ke-size=&quot;size16&quot;&gt;Apollo Client의 ErrorPolicy는 세 가지 옵션이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1968&quot; data-start=&quot;1881&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1913&quot; data-start=&quot;1881&quot;&gt;none: 에러가 발생하면 아무것도 반환하지 않음.&lt;/li&gt;
&lt;li data-end=&quot;1943&quot; data-start=&quot;1914&quot;&gt;ignore: 에러를 무시하고 데이터를 반환.&lt;/li&gt;
&lt;li data-end=&quot;1968&quot; data-start=&quot;1944&quot;&gt;all: 에러와 데이터를 모두 반환.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2002&quot; data-start=&quot;1970&quot; data-ke-size=&quot;size16&quot;&gt;우리는 all을 선택했습니다. 이유는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2129&quot; data-start=&quot;2004&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2047&quot; data-start=&quot;2004&quot;&gt;SSE 알림으로 데이터 갱신 중 404 에러가 발생할 수 있기 때문입니다.&lt;/li&gt;
&lt;li data-end=&quot;2076&quot; data-start=&quot;2048&quot;&gt;그러나 전체 페이지를 깨뜨리고 싶지 않았습니다.&lt;/li&gt;
&lt;li data-end=&quot;2129&quot; data-start=&quot;2077&quot;&gt;사용자는 이미 가지고 있던 정상 데이터를 보고, 추가적으로 에러 메시지만 참고하면 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2163&quot; data-start=&quot;2131&quot; data-ke-size=&quot;size16&quot;&gt;에러를 감싸면서도 앱 전체 안정성을 지키는 선택이었습니다.&lt;/p&gt;
&lt;hr data-end=&quot;2168&quot; data-start=&quot;2165&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2199&quot; data-start=&quot;2170&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 Subscription을 사용하지 않았을까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2296&quot; data-start=&quot;2201&quot; data-ke-size=&quot;size16&quot;&gt;Hasura는 WebSocket 기반 Subscription 기능을 제공합니다.&lt;br /&gt;그런데 우리는 Subscription 대신 별도 SSE 서버를 사용하기로 결정했습니다.&lt;/p&gt;
&lt;p data-end=&quot;2313&quot; data-start=&quot;2298&quot; data-ke-size=&quot;size16&quot;&gt;그 이유는 다음과 같습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;2332&quot; data-start=&quot;2315&quot; data-ke-size=&quot;size23&quot;&gt;1. 운영 복잡성 최소화&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2443&quot; data-start=&quot;2334&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2396&quot; data-start=&quot;2334&quot;&gt;Hasura에 직접 SSE 기능을 추가하거나, Subscription을 확장하려면 커스터마이징이 필요합니다.&lt;/li&gt;
&lt;li data-end=&quot;2443&quot; data-start=&quot;2397&quot;&gt;이는 서버를 무겁게 만들고, 추후 버전 관리나 운영에서 문제가 될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;2478&quot; data-start=&quot;2445&quot; data-ke-size=&quot;size23&quot;&gt;2. 모든 개발자에게 Hasura가 편한 것은 아니다&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2588&quot; data-start=&quot;2480&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2546&quot; data-start=&quot;2480&quot;&gt;Hasura는 추상화가 잘 되어 있지만, 커스텀 이벤트나 세밀한 제어가 필요한 경우에는 오히려 불편할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;2588&quot; data-start=&quot;2547&quot;&gt;별도 서버를 두면 더 자유롭게 이벤트를 발행하고 관리할 수 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;2609&quot; data-start=&quot;2590&quot; data-ke-size=&quot;size23&quot;&gt;3. 버전 업그레이드 리스크&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2727&quot; data-start=&quot;2611&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2667&quot; data-start=&quot;2611&quot;&gt;Hasura를 업그레이드하려면 마이그레이션 작업이 필요할 수 있고, 이는 운영 리스크를 키웁니다.&lt;/li&gt;
&lt;li data-end=&quot;2727&quot; data-start=&quot;2668&quot;&gt;우리는 빠르게 실시간 알림을 구축해야 했기 때문에, 기존 Hasura를 건드리지 않는 쪽을 택했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;2743&quot; data-start=&quot;2729&quot; data-ke-size=&quot;size23&quot;&gt;4. SSE의 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2847&quot; data-start=&quot;2745&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2778&quot; data-start=&quot;2745&quot;&gt;SSE는 HTTP 기반으로 간단하게 구축할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;2814&quot; data-start=&quot;2779&quot;&gt;서버 리소스를 적게 소모하며, 로드밸런서 설정도 간단합니다.&lt;/li&gt;
&lt;li data-end=&quot;2847&quot; data-start=&quot;2815&quot;&gt;실시간 알림처럼 단방향 이벤트 전송에 매우 적합합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2945&quot; data-start=&quot;2849&quot; data-ke-size=&quot;size16&quot;&gt;결국 우리는 Hasura를 메인 API 서버로 유지하면서,&lt;br /&gt;별도 SSE 서버를 운영하여 실시간 알림과 데이터 Refetch를 효율적으로 관리하는 아키텍처를 완성했습니다.&lt;/p&gt;
&lt;hr data-end=&quot;2950&quot; data-start=&quot;2947&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;2957&quot; data-start=&quot;2952&quot;&gt;마무리&lt;/h1&gt;
&lt;p data-end=&quot;2977&quot; data-start=&quot;2959&quot; data-ke-size=&quot;size16&quot;&gt;이번 구조 개선을 통해 우리는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3059&quot; data-start=&quot;2978&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3010&quot; data-start=&quot;2978&quot;&gt;알림 수신 ➔ 관련 도메인만 지정하여 Refetch&lt;/li&gt;
&lt;li data-end=&quot;3037&quot; data-start=&quot;3011&quot;&gt;일부 에러 발생시에도 전체 서비스는 유지&lt;/li&gt;
&lt;li data-end=&quot;3059&quot; data-start=&quot;3038&quot;&gt;서버 확장과 운영 리스크 최소화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3084&quot; data-start=&quot;3061&quot; data-ke-size=&quot;size16&quot;&gt;이 세 가지를 모두 달성할 수 있었습니다.&lt;/p&gt;</description>
      <category>software engineering/frontend</category>
      <author>Hyeong Nim</author>
      <guid isPermaLink="true">https://doohyeong.tistory.com/262</guid>
      <comments>https://doohyeong.tistory.com/262#entry262comment</comments>
      <pubDate>Mon, 28 Apr 2025 16:46:00 +0900</pubDate>
    </item>
    <item>
      <title>퍼포먼스 최적화(Next.js App Router)</title>
      <link>https://doohyeong.tistory.com/261</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;배경: FSD(Feature-Sliced Design) + Barrel Files&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 설계는 Feature-Sliced Design (FSD)를 따랐습니다. Feature 단위로 독립성과 가독성을 확보하기 위해, &lt;a href=&quot;https://feature-sliced.github.io/documentation/docs/get-started/tutorial#define-a-strict-public-api&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;권장된 가이드&lt;/a&gt;에 따라&amp;nbsp;&lt;span&gt;&lt;b&gt;Barrel Files&lt;/b&gt;&lt;/span&gt;를 도입했습니다. 하지만 Feature 단위에서 내보내는 Public Index의 양이 많아지면서 필요 없는 코드까지 묶여서 파일이 무거워졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국: 아래와 같이 퍼포먼스에 이슈가 생겼고 Barrel Files를 걷어내기로 했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기 로딩 부하 증가&lt;/li&gt;
&lt;li&gt;SEO Core Web Vitals 점수 하락&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 진단: 구조적 성능 병목&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;과적축된 정적 Public API&lt;/li&gt;
&lt;li&gt;SSR이 필요하지 않은 컴포넌트에 Code Splitting이 되어 있지 않아 초기 렌더링 부하&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 &lt;span&gt;&lt;b&gt;LCP 2.1초&lt;/b&gt;&lt;/span&gt;, Performance Score &lt;span&gt;&lt;b&gt;75.88로 &lt;/b&gt;&lt;/span&gt;UX 관점에서 &amp;lsquo;명백한 이탈 위험 구간&amp;rsquo;이었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결 전략: 구조 레벨부터 리셋&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. Barrel Files 제거 &amp;rarr; 명시적 Import&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 컴포넌트를 개별 import로 변경해, 필요할 때만 필요한 코드만 가져오도록 구조 재설계.&lt;/p&gt;
&lt;pre id=&quot;code_1745823954632&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Before
import { Dialog, Tooltip, Dropdown } from '@/components/common';

// After
import Dialog from '@/components/common/Dialog';
import Tooltip from '@/components/common/Tooltip';
import Dropdown from '@/components/common/Dropdown';&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모노레포 패키지의 경우 필요 컴포넌트만 번들링을 통한 트리쉐이킹 100% 활성화&lt;/li&gt;
&lt;li&gt;모노레포 앱의 경우 필요한 컴포넌트를 직접 import 하도록 변경&lt;/li&gt;
&lt;li&gt;코드 의존성 명확화&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 초기 렌더링과 무관한 컴포넌트는 Dynamic Import + SSR False&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1745823971520&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import dynamic from 'next/dynamic';

const Dialog = dynamic(() =&amp;gt; import('@/components/common/Dialog'), { ssr: false });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dialog, Modal, Tooltip 등 인터랙티브 컴포넌트 및 번들사이즈에 방해되는 컴포넌트는 꼭 필요하지 않다면 초기 렌더링 제외&lt;/li&gt;
&lt;li&gt;HTML 크기 축소 &amp;rarr; Time to First Byte(TTFB) 개선&lt;/li&gt;
&lt;li&gt;Core Web Vitals(LCP, FCP) 직접 개선&lt;/li&gt;
&lt;li&gt;데이터가 무거운데 자주 방문하는 페이지의 데이터의 경우 FetchPolicy를 'cache-first'로 두어 FCP 개선 (단, 인증을 확인 하기 위해 별도 인증 정보 호출을 'network-only'로 통신)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;성과&lt;/b&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 116px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;b&gt;&lt;span&gt;항목&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;b&gt;개선 전&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;b&gt;&lt;span&gt;개선 후&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;b&gt;&lt;span&gt;변화율&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;평균 LCP&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;2.10초&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;b&gt;0.68초&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;▲ 약 67.6% 개선&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;Performance Score&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;75.88&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;b&gt;90.12&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;▲ 약 18.8% 개선&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;페이지당 평균 파일 크기&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;7.97 kB&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;6.15 kB&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;▼ 22.8% 감소&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;페이지당 First Load JS&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;745.88 kB&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;583.27 kB&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;▼ 21.8% 감소&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ LCP 0.7초 이하&lt;br /&gt;✅ Performance 90점 이상&lt;br /&gt;✅ 체감 로딩 속도 &amp;ldquo;즉시 반응&amp;rdquo; 수준&lt;/p&gt;</description>
      <category>software engineering/frontend</category>
      <category>App Router</category>
      <category>Dynamic import</category>
      <category>fsd</category>
      <category>Next.js</category>
      <category>Web Vitals</category>
      <category>성능 최적화</category>
      <category>프론트엔드 아키텍처</category>
      <author>Hyeong Nim</author>
      <guid isPermaLink="true">https://doohyeong.tistory.com/261</guid>
      <comments>https://doohyeong.tistory.com/261#entry261comment</comments>
      <pubDate>Mon, 28 Apr 2025 16:09:47 +0900</pubDate>
    </item>
    <item>
      <title>Next.js App Router에서 prefetch 전략</title>
      <link>https://doohyeong.tistory.com/260</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 프로젝트에서는 Next.js의 App Router를 기반으로 웹사이트를 개발했습니다.&lt;br /&gt;이 사이트는 다음과 같은 특징을 가지고 있었습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;386&quot; data-start=&quot;289&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;309&quot; data-start=&quot;289&quot;&gt;&lt;b&gt;모든 페이지가 로그인 필수&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;386&quot; data-start=&quot;310&quot;&gt;&lt;b&gt;모든 경로가 dynamic route (export const dynamic ='force-dynamic')&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;510&quot; data-start=&quot;388&quot; data-ke-size=&quot;size16&quot;&gt;초기에는 prefetch에 대해 깊게 고려하지 않고, 기본 &amp;lt;Link&amp;gt;만 사용했습니다.&lt;br /&gt;하지만 시간이 지날수록 성능 이슈를 체감하게 되었고, prefetch 전략을 개선하여 사이트를 훨씬 최적화할 수 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;533&quot; data-start=&quot;512&quot; data-ke-size=&quot;size16&quot;&gt;오늘은 그 경험을 정리해보려고 합니다.&lt;/p&gt;
&lt;h2 data-end=&quot;554&quot; data-start=&quot;540&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;프로젝트의 초기 상황&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;717&quot; data-start=&quot;556&quot; data-ke-size=&quot;size16&quot;&gt;Next.js App Router는 &amp;lt;Link&amp;gt;를 사용할 때 기본적으로 prefetch를 수행합니다.&lt;br /&gt;이 때 prefetch는 별도의 API 호출을 발생시키는 것이 아니라, &lt;b&gt;Server Component의 결과물(Flight Response)&lt;/b&gt; 을 미리 받아오는 형태입니다.&lt;/p&gt;
&lt;p data-end=&quot;735&quot; data-start=&quot;719&quot; data-ke-size=&quot;size16&quot;&gt;Network 패널에서 보면 /project/[id], /approval/[id] 같은 dynamic route로 &lt;b&gt;Server Component를 다운로드 받는 요청&lt;/b&gt;만 발생하고 별도 fetch API 요청이나 데이터 호출은 발생하지 않았습니다.&lt;/p&gt;
&lt;p data-end=&quot;948&quot; data-start=&quot;873&quot; data-ke-size=&quot;size16&quot;&gt;또한, 브라우저의 Network Connection 관점에서도 SSE나 websocket 같은 지속적인 연결은 관찰되지 않았습니다.&lt;/p&gt;
&lt;p data-end=&quot;1079&quot; data-start=&quot;950&quot; data-ke-size=&quot;size16&quot;&gt;요약하면,&lt;br /&gt;  &lt;b&gt;prefetch가 별도 API 트래픽을 증가시키진 않았지만&lt;/b&gt;,&lt;br /&gt;  &lt;b&gt;dynamic route가 많은 환경에서는 불필요한 Server Component 다운로드로 네트워크 비용이 커지고 있었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-end=&quot;1084&quot; data-start=&quot;1081&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1101&quot; data-start=&quot;1086&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;무슨 문제가 있었을까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1112&quot; data-start=&quot;1103&quot; data-ke-size=&quot;size16&quot;&gt;사이트의 특성상:&lt;/p&gt;
&lt;p data-end=&quot;1112&quot; data-start=&quot;1103&quot; data-ke-size=&quot;size16&quot;&gt;하나의 페이지에 수십~수백 개의 dynamic 링크가 등장하는 경우가 많았고, 각 링크가 개별적으로 prefetch되면서 &lt;b&gt;필요하지 않은 Server Component까지 대량으로 다운로드&lt;/b&gt;하는 상황이 발생했습니다.&lt;/p&gt;
&lt;p data-end=&quot;1260&quot; data-start=&quot;1243&quot; data-ke-size=&quot;size16&quot;&gt;결국 이런 문제가 나타났습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1395&quot; data-start=&quot;1262&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1292&quot; data-start=&quot;1262&quot;&gt;페이지 이동을 하지 않아도 브라우저 네트워크에 부하&lt;/li&gt;
&lt;li data-end=&quot;1316&quot; data-start=&quot;1293&quot;&gt;렌더링과 리소스 소모가 불필요하게 증가&lt;/li&gt;
&lt;li data-end=&quot;1360&quot; data-start=&quot;1317&quot;&gt;실제로 이동하려는 순간보다 먼저 대량의 데이터를 받아 메모리 사용량이 증가&lt;/li&gt;
&lt;li data-end=&quot;1395&quot; data-start=&quot;1361&quot;&gt;특히 모바일 네트워크 환경에서는 체감 속도가 급격히 느려짐&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1400&quot; data-start=&quot;1397&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1419&quot; data-start=&quot;1402&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Prefetch 전략 수립&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1449&quot; data-start=&quot;1421&quot; data-ke-size=&quot;size16&quot;&gt;문제를 인식한 후, 다음과 같은 전략을 세웠습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1472&quot; data-start=&quot;1451&quot; data-ke-size=&quot;size23&quot;&gt;1. 기본 prefetch 끄기&lt;/h3&gt;
&lt;p data-end=&quot;1516&quot; data-start=&quot;1474&quot; data-ke-size=&quot;size16&quot;&gt;우선, &amp;lt;Link&amp;gt; 컴포넌트에 기본 prefetch를 끄기 시작했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1745811342167&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Link href={`/project/${project.id}`} prefetch={false}&amp;gt;
  {project.name}
&amp;lt;/Link&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1667&quot; data-start=&quot;1611&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1667&quot; data-start=&quot;1611&quot;&gt;사용자가 직접 클릭하거나 명확한 액션을 취하기 전까지는 prefetch를 수행하지 않게 했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1691&quot; data-start=&quot;1669&quot; data-ke-size=&quot;size23&quot;&gt;2. 명시적 prefetch 적용&lt;/h3&gt;
&lt;p data-end=&quot;1781&quot; data-start=&quot;1693&quot; data-ke-size=&quot;size16&quot;&gt;필요한 경우에만 prefetch를 수동으로 트리거했습니다.&lt;br /&gt;예를 들어 사용자가 해당 링크를 hover하거나 focus했을 때 prefetch를 실행합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1745811359603&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;use client&quot;;
import { useRouter } from '@shared/navigation';

export default function ProjectItem({ id, name }: { id: string, name: string }) {
  const router = useRouter();

  const handleMouseEnter = () =&amp;gt; {
    router.prefetch(`/project/${id}`);
  };

  return (
    &amp;lt;Link href={`/project/${id}`} prefetch={false} onMouseEnter={handleMouseEnter}&amp;gt;
      {name}
    &amp;lt;/Link&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2182&quot; data-start=&quot;2175&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2268&quot; data-start=&quot;2184&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2219&quot; data-start=&quot;2184&quot;&gt;사용자가 실제로 접근할 가능성이 높은 링크만 prefetch&lt;/li&gt;
&lt;li data-end=&quot;2244&quot; data-start=&quot;2220&quot;&gt;네트워크와 브라우저 메모리 사용을 최소화&lt;/li&gt;
&lt;li data-end=&quot;2268&quot; data-start=&quot;2245&quot;&gt;결과적으로 성능이 훨씬 가벼워졌습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2273&quot; data-start=&quot;2270&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2286&quot; data-start=&quot;2275&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;적용 이후 결과&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2336&quot; data-start=&quot;2288&quot; data-ke-size=&quot;size16&quot;&gt;Prefetch 전략을 개선한 후, 다음과 같은 긍정적인 변화를 경험할 수 있었습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 100px;&quot; border=&quot;1&quot; data-end=&quot;2535&quot; data-start=&quot;2338&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;변경 전&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;변경 후&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;2443&quot; data-start=&quot;2376&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2391&quot; data-start=&quot;2376&quot;&gt;다운로드하는 데이터 양&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2425&quot; data-start=&quot;2391&quot;&gt;많음 (불필요한 Server Component 다운로드)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2443&quot; data-start=&quot;2425&quot;&gt;필요한 것만 최소 다운로드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;2470&quot; data-start=&quot;2444&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2459&quot; data-start=&quot;2444&quot;&gt;네트워크 대역폭 사용량&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2464&quot; data-start=&quot;2459&quot;&gt;높음&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2470&quot; data-start=&quot;2464&quot;&gt;감소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;2511&quot; data-start=&quot;2471&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2484&quot; data-start=&quot;2471&quot;&gt;페이지 렌더링 속도&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2504&quot; data-start=&quot;2484&quot;&gt;느림 (특히 리스트 많은 화면)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2511&quot; data-start=&quot;2504&quot;&gt;빨라짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;2535&quot; data-start=&quot;2512&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2524&quot; data-start=&quot;2512&quot;&gt;모바일 체감 속도&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2529&quot; data-start=&quot;2524&quot;&gt;느림&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;2535&quot; data-start=&quot;2529&quot;&gt;개선&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2606&quot; data-start=&quot;2537&quot; data-ke-size=&quot;size16&quot;&gt;특히, dynamic route가 많은 환경에서는 prefetch 제어만으로 체감 속도가&lt;b&gt;&amp;nbsp;개선&lt;/b&gt;되었습니다.&lt;/p&gt;
&lt;hr data-end=&quot;2611&quot; data-start=&quot;2608&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2618&quot; data-start=&quot;2613&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2699&quot; data-start=&quot;2620&quot; data-ke-size=&quot;size16&quot;&gt;Next.js App Router는 기본적으로 prefetch 기능을 제공합니다.&lt;br /&gt;이는 사용자 경험을 빠르게 만드는 강력한 무기이지만,&lt;/p&gt;
&lt;p data-end=&quot;2799&quot; data-start=&quot;2701&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;dynamic route가 많고, 로그인 세션이 필요한 사이트&lt;/b&gt;에서는&lt;br /&gt;&lt;b&gt;prefetch를 무조건 사용하는 것이 오히려 네트워크, 메모리 부하를 키울 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2805&quot; data-start=&quot;2801&quot; data-ke-size=&quot;size16&quot;&gt;따라서:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2846&quot; data-start=&quot;2807&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2826&quot; data-start=&quot;2807&quot;&gt;언제 prefetch를 수행할지&lt;/li&gt;
&lt;li data-end=&quot;2846&quot; data-start=&quot;2827&quot;&gt;언제 prefetch를 제한할지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2873&quot; data-start=&quot;2848&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;명확한 전략&lt;/b&gt;을 세우는 것이 필수입니다.&lt;/p&gt;</description>
      <category>software engineering/frontend</category>
      <author>Hyeong Nim</author>
      <guid isPermaLink="true">https://doohyeong.tistory.com/260</guid>
      <comments>https://doohyeong.tistory.com/260#entry260comment</comments>
      <pubDate>Mon, 28 Apr 2025 12:37:28 +0900</pubDate>
    </item>
    <item>
      <title>Next.js에서 next/image를 사용하는 이유와 optimize 설정에 대한 고민</title>
      <link>https://doohyeong.tistory.com/259</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js를 사용하면서 많은 사람들이 자연스럽게 선택하는 컴포넌트가 있습니다. 바로 &lt;b&gt;next/image&lt;/b&gt;입니다.&lt;br /&gt;이미지 관리를 자동화해주고 성능 최적화까지 해주니, 정말 매력적인 기능이죠.&lt;br /&gt;하지만 실제 운영 환경에서는 이 next/image가 예상치 못한 문제를 일으킬 수도 있습니다.&lt;br /&gt;특히 optimize: true 설정을 사용할 때 주의해야 할 점들이 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;498&quot; data-start=&quot;407&quot; data-ke-size=&quot;size16&quot;&gt;오늘은 제가 이 문제를 고민했던 경험을 바탕으로, 왜 next/image를 쓰는지, 그리고 optimize 설정을 현명하게 다루는 방법까지 정리해보려고 합니다.&lt;/p&gt;
&lt;hr data-end=&quot;503&quot; data-start=&quot;500&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;530&quot; data-start=&quot;505&quot; data-ke-size=&quot;size26&quot;&gt;왜 next/image를 사용하는가?&lt;/h2&gt;
&lt;p data-end=&quot;568&quot; data-start=&quot;532&quot; data-ke-size=&quot;size16&quot;&gt;먼저, next/image가 제공하는 장점은 정말 강력합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1144&quot; data-start=&quot;570&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;703&quot; data-start=&quot;570&quot;&gt;&lt;b&gt;자동 최적화 (Optimization)&lt;/b&gt;&lt;br /&gt;요청하는 디바이스에 맞게 이미지를 리사이즈하거나, WebP, AVIF 같은 최신 포맷으로 변환하여 최적화된 이미지를 전송해줍니다. 결과적으로 페이지 로딩 속도가 크게 향상됩니다.&lt;/li&gt;
&lt;li data-end=&quot;843&quot; data-start=&quot;705&quot;&gt;&lt;b&gt;지연 로딩 (Lazy Loading)&lt;/b&gt;&lt;br /&gt;사용자가 화면에 스크롤하기 전까지 이미지를 로딩하지 않습니다. 이로 인해 초기 로딩이 가벼워지고, LCP(Largest Contentful Paint) 같은 웹 퍼포먼스 지표도 좋아집니다.&lt;/li&gt;
&lt;li data-end=&quot;937&quot; data-start=&quot;845&quot;&gt;&lt;b&gt;Placeholder 지원 (Blur-up)&lt;/b&gt;&lt;br /&gt;이미지를 로딩하는 동안 흐릿한 미리보기 이미지를 제공하여, 사용자 경험(UX)을 부드럽게 만듭니다.&lt;/li&gt;
&lt;li data-end=&quot;1066&quot; data-start=&quot;939&quot;&gt;&lt;b&gt;Responsive srcset 자동 생성&lt;/b&gt;&lt;br /&gt;다양한 디바이스 해상도에 맞게 자동으로 srcset을 생성합니다. 고해상도 디스플레이(iPhone, 4K 모니터 등)에서도 선명한 이미지를 제공할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;1144&quot; data-start=&quot;1068&quot;&gt;&lt;b&gt;레이아웃 안정성 확보&lt;/b&gt;&lt;br /&gt;height와 width를 명시적으로 지정하게 함으로써 레이아웃 시프트(CLS)를 방지합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;1220&quot; data-start=&quot;1146&quot; data-ke-size=&quot;size16&quot;&gt;이 모든 기능을 &lt;b&gt;별도 설정 없이&lt;/b&gt;, 단순히 next/image를 사용하기만 하면 얻을 수 있다는 점은 정말 매력적이었습니다.&lt;/p&gt;
&lt;hr data-end=&quot;1225&quot; data-start=&quot;1222&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1251&quot; data-start=&quot;1227&quot; data-ke-size=&quot;size26&quot;&gt;optimize: true의 그림자&lt;/h2&gt;
&lt;p data-end=&quot;1308&quot; data-start=&quot;1253&quot; data-ke-size=&quot;size16&quot;&gt;하지만 optimize: true 설정을 켜고 운영해보면서 몇 가지 중요한 문제를 발견했습니다.&lt;/p&gt;
&lt;p data-end=&quot;1362&quot; data-start=&quot;1310&quot; data-ke-size=&quot;size16&quot;&gt;next/image는 optimize가 활성화되었을 때, 서버에서 다음 작업을 수행합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1428&quot; data-start=&quot;1364&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1381&quot; data-start=&quot;1364&quot;&gt;원본 이미지를 메모리에 올림&lt;/li&gt;
&lt;li data-end=&quot;1388&quot; data-start=&quot;1382&quot;&gt;리사이징&lt;/li&gt;
&lt;li data-end=&quot;1411&quot; data-start=&quot;1389&quot;&gt;포맷 변환 (WebP, AVIF 등)&lt;/li&gt;
&lt;li data-end=&quot;1428&quot; data-start=&quot;1412&quot;&gt;압축(quality 조정)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1488&quot; data-start=&quot;1430&quot; data-ke-size=&quot;size16&quot;&gt;이 과정은 CPU와 메모리 자원을 상당히 많이 소모합니다. 특히 다음과 같은 상황에서는 위험이 커집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1558&quot; data-start=&quot;1490&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1510&quot; data-start=&quot;1490&quot;&gt;고해상도 원본 이미지를 처리할 때&lt;/li&gt;
&lt;li data-end=&quot;1537&quot; data-start=&quot;1511&quot;&gt;동시에 많은 이미지 최적화 요청이 들어올 때&lt;/li&gt;
&lt;li data-end=&quot;1558&quot; data-start=&quot;1538&quot;&gt;서버 메모리가 충분하지 않은 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1661&quot; data-start=&quot;1560&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이미지 한 장 최적화에 5~10배의 메모리 사용이 발생&lt;/b&gt;할 수 있고, 이게 겹치면 서버 메모리가 터지면서 &lt;b&gt;OOM(Out of Memory) 크래시&lt;/b&gt;가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;1756&quot; data-start=&quot;1663&quot; data-ke-size=&quot;size16&quot;&gt;실제로 대규모 트래픽을 처리하는 서비스나, 이미지가 많은 서비스에서는 optimize가 켜진 상태로 서버를 운영하면 메모리 부족 문제를 경험하는 경우가 적지 않습니다.&lt;/p&gt;
&lt;hr data-end=&quot;1761&quot; data-start=&quot;1758&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1781&quot; data-start=&quot;1763&quot; data-ke-size=&quot;size26&quot;&gt;그렇다면 어떻게 해야 할까?&lt;/h2&gt;
&lt;p data-end=&quot;1810&quot; data-start=&quot;1783&quot; data-ke-size=&quot;size16&quot;&gt;저는 다음과 같은 방향으로 해결책을 고민했습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1836&quot; data-start=&quot;1812&quot; data-ke-size=&quot;size23&quot;&gt;1. 외부 이미지 최적화 서비스 사용&lt;/h3&gt;
&lt;p data-end=&quot;1918&quot; data-start=&quot;1838&quot; data-ke-size=&quot;size16&quot;&gt;next/image는 자체 최적화(optimize)를 끄고(optimize: false) 대신 외부 최적화 서비스를 연결할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;1926&quot; data-start=&quot;1920&quot; data-ke-size=&quot;size16&quot;&gt;대표적으로:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2011&quot; data-start=&quot;1928&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1960&quot; data-start=&quot;1928&quot;&gt;&lt;b&gt;Vercel의 Image Optimization&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1984&quot; data-start=&quot;1961&quot;&gt;&lt;b&gt;Cloudflare Images&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1996&quot; data-start=&quot;1985&quot;&gt;&lt;b&gt;Imgix&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;2011&quot; data-start=&quot;1997&quot;&gt;&lt;b&gt;ImageKit&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2090&quot; data-start=&quot;2013&quot; data-ke-size=&quot;size16&quot;&gt;이런 서비스는 고성능으로 이미지 최적화를 해주고, 글로벌 CDN에 캐싱까지 해주기 때문에 서버 부하를 거의 0에 가깝게 만들 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1745808638548&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// next.config.js
module.exports = {
  images: {
    loader: 'imgix', // 또는 'cloudinary', 'custom' 등
    path: 'https://your-image-service.com/',
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2331&quot; data-start=&quot;2265&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 설정하면 Next.js 서버는 최적화 작업을 하지 않고, 외부 서비스를 통해 최적화된 이미지를 바로 서빙합니다.&lt;/p&gt;
&lt;hr data-end=&quot;2336&quot; data-start=&quot;2333&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2354&quot; data-start=&quot;2338&quot; data-ke-size=&quot;size23&quot;&gt;2. 서버 메모리 확장&lt;/h3&gt;
&lt;p data-end=&quot;2483&quot; data-start=&quot;2356&quot; data-ke-size=&quot;size16&quot;&gt;가장 단순한 방법은 서버 사양을 올리는 것입니다. 메모리와 CPU를 확장하면 어느 정도 최적화 부하를 견딜 수 있습니다.&lt;br /&gt;하지만 이 방법은 &lt;b&gt;비용이 기하급수적으로 증가&lt;/b&gt;할 수 있어 장기적인 해결책으로는 추천하지 않습니다.&lt;/p&gt;
&lt;hr data-end=&quot;2488&quot; data-start=&quot;2485&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2523&quot; data-start=&quot;2490&quot; data-ke-size=&quot;size23&quot;&gt;3. 이미지 사전 최적화 (Preprocessing)&lt;/h3&gt;
&lt;p data-end=&quot;2653&quot; data-start=&quot;2525&quot; data-ke-size=&quot;size16&quot;&gt;빌드 시점이나 업로드 시점에 미리 이미지를 최적화해두고, 운영 서버에서는 가공 없이 바로 서빙하는 방법입니다.&lt;br /&gt;예를 들면, CMS에 이미지를 업로드할 때 sharp 같은 라이브러리로 미리 리사이징/압축을 걸어두는 식입니다.&lt;/p&gt;
&lt;hr data-end=&quot;2658&quot; data-start=&quot;2655&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2665&quot; data-start=&quot;2660&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-end=&quot;2768&quot; data-start=&quot;2667&quot; data-ke-size=&quot;size16&quot;&gt;next/image는 현대 프론트엔드 개발자에게 정말 강력한 도구입니다.&lt;br /&gt;특히 자동 최적화 기능(optimize)은 페이지 속도 개선과 사용자 경험 향상에 큰 도움이 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;2831&quot; data-start=&quot;2770&quot; data-ke-size=&quot;size16&quot;&gt;하지만,&lt;br /&gt;&lt;b&gt;optimize: true는 서버에 적지 않은 부담을 준다&lt;/b&gt;는 것을 반드시 기억해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2940&quot; data-start=&quot;2833&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2879&quot; data-start=&quot;2833&quot;&gt;작은 프로젝트, 저트래픽 서비스 &amp;rarr; 그냥 optimize를 켜고 쓰는 것도 OK&lt;/li&gt;
&lt;li data-end=&quot;2940&quot; data-start=&quot;2880&quot;&gt;중대형 서비스, 고트래픽 사이트 &amp;rarr; optimize를 끄고 외부 이미지 최적화 서비스를 고려해야 합니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>software engineering/frontend</category>
      <category>nextjs #imageoptimization #frontendarchitecture #performance #webperformance</category>
      <author>Hyeong Nim</author>
      <guid isPermaLink="true">https://doohyeong.tistory.com/259</guid>
      <comments>https://doohyeong.tistory.com/259#entry259comment</comments>
      <pubDate>Mon, 28 Apr 2025 11:51:10 +0900</pubDate>
    </item>
    <item>
      <title>실시간 데이터 갱신을 위한 SSE(Server-Sent Events)와 WebWorker 활용기</title>
      <link>https://doohyeong.tistory.com/258</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서, 전자결재 시스템과 같은 &lt;b&gt;상태 기반 데이터&lt;/b&gt;를 &lt;b&gt;실시간으로 갱신&lt;/b&gt;해야 하는 요구사항이 생겼습니다. 예를 들면, 사용자가 요청한 결재가 승인되거나 거절될 때 그 상태가 즉시 화면에 반영되어야 했습니다.&lt;br /&gt;이를 위해 저희는 여러 기술적 대안을 고민했고, 최종적으로 &lt;b&gt;Server-Sent Events(SSE)&lt;/b&gt; 를 사용하기로 결정했습니다.&lt;/p&gt;
&lt;hr data-end=&quot;418&quot; data-start=&quot;415&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;450&quot; data-start=&quot;420&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 WebSocket이 아닌 SSE를 선택했을까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;575&quot; data-start=&quot;452&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 자연스럽게 WebSocket을 떠올렸습니다. WebSocket은 양방향 통신이 가능하고, 대규모 실시간 애플리케이션(예: 채팅 서비스)에서는 훌륭한 선택이 될 수 있습니다. 하지만 우리의 경우는 조금 달랐습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;723&quot; data-start=&quot;577&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;629&quot; data-start=&quot;577&quot;&gt;&lt;b&gt;서버 &amp;rarr; 클라이언트 방향의 알림만 필요&lt;/b&gt;했습니다. (ex: 결재 상태 변경 알림)&lt;/li&gt;
&lt;li data-end=&quot;667&quot; data-start=&quot;630&quot;&gt;클라이언트가 서버에 실시간으로 메시지를 보낼 필요는 없었습니다.&lt;/li&gt;
&lt;li data-end=&quot;723&quot; data-start=&quot;668&quot;&gt;WebSocket을 사용하면 연결 유지 및 관리 비용이 더 높아지고, 구현 복잡도가 증가합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;810&quot; data-start=&quot;725&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉, 단방향 스트리밍만 필요한 상황&lt;/b&gt;이었기 때문에, 더 가볍고 간단한 &lt;b&gt;SSE(Server-Sent Events)&lt;/b&gt; 가 딱 맞는 선택이었습니다.&lt;/p&gt;
&lt;hr data-end=&quot;815&quot; data-start=&quot;812&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;834&quot; data-start=&quot;817&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;SSE 사용 시 고려한 점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;901&quot; data-start=&quot;836&quot; data-ke-size=&quot;size16&quot;&gt;SSE는 본질적으로 HTTP 기반이기 때문에 연결이 끊어질 수 있습니다. 특히 다음과 같은 상황을 고려해야 했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1004&quot; data-start=&quot;903&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;934&quot; data-start=&quot;903&quot;&gt;사용자의 네트워크가 일시적으로 끊겼다가 복구되는 경우&lt;/li&gt;
&lt;li data-end=&quot;967&quot; data-start=&quot;935&quot;&gt;브라우저가 일시적으로 리소스를 중단하거나 복구하는 경우&lt;/li&gt;
&lt;li data-end=&quot;1004&quot; data-start=&quot;968&quot;&gt;서버 측 유지 관리나 네트워크 이슈로 인해 세션이 끊기는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1035&quot; data-start=&quot;1006&quot; data-ke-size=&quot;size16&quot;&gt;이런 상황을 대비해 다음과 같은 정책을 수립했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1244&quot; data-start=&quot;1037&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1110&quot; data-start=&quot;1037&quot;&gt;&lt;b&gt;재시도 주기(retry interval)&lt;/b&gt; : 기본적으로 연결이 끊어진 경우 &lt;b&gt;3초&lt;/b&gt; 후 재시도를 하도록 설정했습니다.&lt;/li&gt;
&lt;li data-end=&quot;1174&quot; data-start=&quot;1111&quot;&gt;&lt;b&gt;최대 재시도 횟수&lt;/b&gt; : 무한히 재시도하지 않도록, &lt;b&gt;최대 10회&lt;/b&gt;까지 재시도 후 실패로 처리했습니다.&lt;/li&gt;
&lt;li data-end=&quot;1244&quot; data-start=&quot;1175&quot;&gt;서버가 retry 값을 명시적으로 내려주는 경우 그 값을 반영하여 유연하게 재시도 간격을 조정할 수 있도록 했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1306&quot; data-start=&quot;1246&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 함으로써 네트워크 환경이 불안정한 상황에서도 사용자가 자연스럽게 서비스를 이용할 수 있도록 했습니다.&lt;/p&gt;
&lt;hr data-end=&quot;1311&quot; data-start=&quot;1308&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1346&quot; data-start=&quot;1313&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;메인 스레드 부하를 줄이기 위해 WebWorker 활용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1408&quot; data-start=&quot;1348&quot; data-ke-size=&quot;size16&quot;&gt;SSE는 기본적으로 브라우저의 메인 스레드에서 실행됩니다. 하지만 다음과 같은 문제가 발생할 수 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1519&quot; data-start=&quot;1410&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1486&quot; data-start=&quot;1410&quot;&gt;사용자가 페이지를 이동하거나 다른 작업을 수행할 때, 메인 스레드에서 SSE 연결 관리 로직이 &lt;b&gt;잠깐 멈추거나 딜레이&lt;/b&gt; 되는 현상&lt;/li&gt;
&lt;li data-end=&quot;1519&quot; data-start=&quot;1487&quot;&gt;그로 인해 사용성 저하나 예상치 못한 오류 발생 가능성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1554&quot; data-start=&quot;1521&quot; data-ke-size=&quot;size16&quot;&gt;이를 방지하기 위해 &lt;b&gt;WebWorker&lt;/b&gt;를 도입했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1728&quot; data-start=&quot;1556&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1608&quot; data-start=&quot;1556&quot;&gt;&lt;b&gt;SSE 연결 및 이벤트 수신 로직&lt;/b&gt;을 WebWorker 내부에서 실행하도록 했습니다.&lt;/li&gt;
&lt;li data-end=&quot;1653&quot; data-start=&quot;1609&quot;&gt;메인 스레드는 UI 렌더링과 사용자 입력 처리에만 집중할 수 있게 했습니다.&lt;/li&gt;
&lt;li data-end=&quot;1728&quot; data-start=&quot;1654&quot;&gt;Worker는 SSE 연결 상태를 모니터링하고, 메시지가 오면 메인 스레드로 전달(postMessage)하는 구조로 구현했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1822&quot; data-start=&quot;1730&quot; data-ke-size=&quot;size16&quot;&gt;덕분에 사용자 경험(UX)이 매우 부드럽게 유지되었고, 특히 모바일 환경에서도 &lt;b&gt;네비게이션 중 끊김 없이&lt;/b&gt; 실시간 알림이 잘 동작하는 결과를 얻을 수 있었습니다.&lt;/p&gt;
&lt;hr data-end=&quot;1827&quot; data-start=&quot;1824&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1835&quot; data-start=&quot;1829&quot; data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-end=&quot;2001&quot; data-start=&quot;1837&quot; data-ke-size=&quot;size16&quot;&gt;이번 경험을 통해 &lt;b&gt;단방향 실시간 갱신이 필요한 경우 SSE를 적극 고려해볼 수 있다&lt;/b&gt;는 확신을 얻었습니다. 또한, 브라우저 메인 스레드를 가볍게 유지하고 안정적인 실시간 연결을 유지하기 위해 &lt;b&gt;WebWorker를 통한 비동기 처리&lt;/b&gt;가 얼마나 중요한지도 다시 한번 체감할 수 있었습니다.&lt;/p&gt;</description>
      <category>software engineering/frontend</category>
      <author>Hyeong Nim</author>
      <guid isPermaLink="true">https://doohyeong.tistory.com/258</guid>
      <comments>https://doohyeong.tistory.com/258#entry258comment</comments>
      <pubDate>Mon, 28 Apr 2025 11:45:32 +0900</pubDate>
    </item>
    <item>
      <title>JSON-Patch: 대규모 JSON 관리를 위한 효율적인 도구</title>
      <link>https://doohyeong.tistory.com/257</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현대의 소프트웨어 개발 환경에서는 대규모 JSON 데이터를 관리하고 동기화하는 일이 빈번합니다. 특히 OpenAPI와 같은 API 명세서를 다룰 때, 각 버전 간의 변경 사항을 추적하고 효율적으로 관리하는 것이 중요합니다. 이런 작업을 위해 JSON-Patch는 매우 유용한 도구로 자리 잡고 있습니다. 이번 글에서는 JSON-Patch란 무엇인지, 그리고 제가 경험한 사례를 통해 이를 어떻게 효과적으로 활용할 수 있는지 공유하고자 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;JSON-Patch란 무엇인가?&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;JSON-Patch&lt;/b&gt;&lt;/span&gt;&lt;span&gt;는 JSON 문서의 특정 부분을 추가, 삭제, 변경, 교체할 수 있도록 설계된 경량화된 표준입니다. &lt;/span&gt;&lt;span&gt;&lt;a&gt;RFC 6902&lt;/a&gt;&lt;/span&gt;&lt;span&gt;로 정의된 이 표준은 다음과 같은 특징을 가지고 있습니다:&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;작은 크기:&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 변경 사항만을 포함하기 때문에 네트워크 사용량을 최소화합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;구조화된 형식:&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 명령(operation)과 경로(path)를 기반으로 JSON 데이터를 수정합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;범용성:&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 다양한 언어와 프레임워크에서 사용 가능합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;JSON-Patch는 기본적으로 아래와 같은 6가지 연산자를 지원합니다:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;add&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 새로운 값을 추가합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;remove&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 값을 제거합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;replace&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 값을 교체합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;move&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 값을 다른 위치로 이동합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;copy&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 값을 복사합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;test&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 특정 경로의 값이 예상한 값과 일치하는지 확인합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다음은 간단한 예제입니다:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[
  { &quot;op&quot;: &quot;replace&quot;, &quot;path&quot;: &quot;/name&quot;, &quot;value&quot;: &quot;John Doe&quot; },
  { &quot;op&quot;: &quot;add&quot;, &quot;path&quot;: &quot;/age&quot;, &quot;value&quot;: 30 },
  { &quot;op&quot;: &quot;remove&quot;, &quot;path&quot;: &quot;/address&quot; }
]&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;JSON-Patch와 OpenAPI: 실전 사례&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;최근에 OpenAPI 명세서를 커스터마이징할 일이 있었습니다. Swagger 문서를 받고 이를 클라이언트 사이드에서 수정해야 하는 상황이었는데, 이때 &lt;/span&gt;&lt;span&gt;&lt;b&gt;json-joy/lib/json-patch&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 라이브러리를 활용했습니다. 이 라이브러리는 JSON-Patch 표준을 구현한 강력한 도구로, 변경 사항을 효율적으로 관리할 수 있도록 도와줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;OpenAPISpecification 클래스 설계&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;응집도를 높이기 위해 &lt;/span&gt;&lt;span&gt;OpenAPISpecification&lt;/span&gt;&lt;span&gt;이라는 클래스를 설계했습니다. 이 클래스는 Swagger 문서와 JSON-Patch를 다룰 수 있는 다양한 메서드를 제공합니다:&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;compare(spec1, spec2):&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 두 개의 명세서를 비교하여 차이를 반환합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;diff():&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 현재 명세서와 다른 버전의 차이를 계산합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;get(path):&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 특정 경로의 값을 가져옵니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;update(patch):&lt;/b&gt;&lt;/span&gt;&lt;span&gt; JSON-Patch를 적용하여 명세서를 업데이트합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히, 이 클래스는 다음과 같은 방식으로 활용되었습니다:&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-spread=&quot;true&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;명세서 버전 관리:&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 변경 사항을 JSON-Patch 형태로 기록하여, 이전 버전과의 차이를 명확히 확인하고 필요한 경우 쉽게 롤백할 수 있었습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;자동화된 업데이트:&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 클라이언트 애플리케이션에서 Swagger 명세서를 동적으로 수정해야 할 때, &lt;/span&gt;&lt;span&gt;update&lt;/span&gt;&lt;span&gt; 메서드를 통해 간단히 JSON-Patch를 적용할 수 있었습니다. 예를 들어, 특정 API 경로의 설명을 수정하거나 새로운 필드를 추가하는 작업이 수월해졌습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;테스트 및 검증:&lt;/b&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;compare&lt;/span&gt;&lt;span&gt; 메서드를 활용하여 명세서 간의 변경 사항을 검증하고, 누락된 수정 사항이나 예상치 못한 변경을 쉽게 발견할 수 있었습니다. 이를 통해 배포 전에 변경 사항의 안전성을 높일 수 있었습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;아래는 이를 활용한 주요 시나리오 중 하나입니다:&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1737534332448&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 명세서 간의 차이점 확인
const spec1 = { paths: { &quot;/users&quot;: { get: { description: &quot;Get users&quot; } } } };
const spec2 = { paths: { &quot;/users&quot;: { get: { description: &quot;Retrieve users list&quot; } } } };

const openAPISpec = new OpenAPISpecification(spec1);
const patch = openAPISpec.diff(spec2);
console.log(patch);
// 결과: [ { op: 'replace', path: '/paths/users/get/description', value: 'Retrieve users list' } ]

// JSON-Patch를 적용하여 명세서 업데이트
openAPISpec.update(patch);
console.log(openAPISpec.spec);
// 업데이트된 명세서 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. &lt;span&gt;&lt;b&gt;팀 협업 향상:&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 프론트엔드와 백엔드 개발자 간에 명확한 통신 규칙을 정의하고, 서로 JSON-Patch를 주고받으며 효율적으로 대규모 JSON 데이터를 관리할 수 있었습니다. 예를 들어, 백엔드에서는 Spring 기반 라이브러리를 사용해 클라이언트가 보낸 패치를 적용하고, 클라이언트는 이를 UI에 실시간 반영했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;JSON-Patch의 장점&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;효율성:&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 변경 사항만 처리하기 때문에 대규모 데이터 관리에 적합합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;유연성:&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 다양한 언어와 플랫폼에서 사용할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;협업 향상:&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 클라이언트와 서버 간의 명확한 통신 규칙을 설정할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;마무리&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;JSON-Patch는 단순하면서도 강력한 도구로, 특히 대규모 JSON 데이터를 다루는 환경에서 효율성을 극대화할 수 있습니다. 제가 경험한 OpenAPI 명세서 커스터마이징 사례는 이를 잘 보여주는 예입니다. 만약 JSON 데이터를 효과적으로 관리하고 싶은 개발자라면 JSON-Patch를 적극적으로 활용해 보시길 권장합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;더 나아가, 이러한 사례를 통해 데이터 관리의 효율성을 높이고, 팀 간 협업을 강화할 수 있는 방법을 고민해 보세요. 이 글이 JSON-Patch를 활용하고자 하는 개발자들에게 작은 영감이 되길 바랍니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>software engineering/frontend</category>
      <author>Hyeong Nim</author>
      <guid isPermaLink="true">https://doohyeong.tistory.com/257</guid>
      <comments>https://doohyeong.tistory.com/257#entry257comment</comments>
      <pubDate>Wed, 22 Jan 2025 17:27:34 +0900</pubDate>
    </item>
    <item>
      <title>KeycloakJs를 이용한 다중 계정 전환 Web Component개발기</title>
      <link>https://doohyeong.tistory.com/256</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;프로젝트 개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다수의 사내 서비스에 공통으로 사용할 수 있는 &lt;span&gt;&lt;b&gt;다중 계정 전환 컴포넌트&lt;/b&gt;&lt;/span&gt;를 10일 이내에 개발해야 했습니다.&lt;br /&gt;Keycloak에 대한 사전 지식이 없는 상태였으며, 관리자 권한도 없이 백엔드 인증 담당자와의 협업만으로 진행해야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트의 주요 목표는 다음 두 가지였습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;호스트 서비스로부터 완전히 독립된 형태로 제작&lt;/b&gt;&lt;span&gt; (스타일 및 스크립트 충돌 방지)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SSO 또는 인증(Keycloak)에 대한 이해가 없는 개발자도 쉽게 사용할 수 있도록 구현&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;컴포넌트 요구사항&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 과정에서 기능은 점진적으로 확장되었으며, 최종 요구사항은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로필 이미지 표시&lt;/li&gt;
&lt;li&gt;hover 시 유저 정보 표시&lt;/li&gt;
&lt;li&gt;click 시 유저 정보 + 계정 리스트 + 계정 전환 모달&lt;/li&gt;
&lt;li&gt;전체 로그아웃 기능&lt;/li&gt;
&lt;li&gt;계정 전환/로그아웃 시 다른 탭에 알림 Dialog 노출&lt;/li&gt;
&lt;li&gt;display: none 상태에서도 모달 동작 유지&lt;/li&gt;
&lt;li&gt;client_id, redirect_uri 등의 파라미터 동적 주입 가능&lt;/li&gt;
&lt;li&gt;계정 전환 시 &lt;span&gt;prompt&lt;/span&gt;, &lt;span&gt;select_account&lt;/span&gt; query parameter 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;프로젝트 디자인&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;lit-element-starter-ts&lt;span&gt; 기반으로 프로젝트 구성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;주요 컴포넌트: &lt;/span&gt;modal&lt;span&gt;, &lt;/span&gt;popover&lt;span&gt;, &lt;/span&gt;sso-profile&lt;span&gt;, &lt;/span&gt;sso-userinfo-popover&lt;span&gt;, &lt;/span&gt;sso-userinfo-modal&lt;/li&gt;
&lt;li&gt;&amp;lt;script&amp;gt;&lt;span&gt;를 통해 import한 뒤 &lt;/span&gt;&amp;lt;sso-profile /&amp;gt;&lt;span&gt;로 사용 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Typescript/React 사용자 지원을 위한 &lt;span&gt;.d.ts&lt;/span&gt; 제공&lt;/li&gt;
&lt;li&gt;환경변수는 &lt;span&gt;&amp;lt;script id=&quot;...&quot;&amp;gt;{...}&amp;lt;/script&amp;gt;&lt;/span&gt; 태그를 통해 전달, 내부적으로 기본값과 병합하여 사용&lt;/li&gt;
&lt;li&gt;Popover 위치 계산은 Floating UI 활용&lt;/li&gt;
&lt;li&gt;모달은 Shadow DOM 외부(&lt;span&gt;document.body&lt;/span&gt;)에 렌더링하여 독립성 확보&lt;/li&gt;
&lt;li&gt;Web Component 스타일링 문제 해결을 위해 모든 요소에 &lt;span&gt;part&lt;/span&gt; 제공&lt;/li&gt;
&lt;li&gt;Rollup을 통해 &lt;span&gt;UMD&lt;/span&gt;, &lt;span&gt;ESM&lt;/span&gt; 빌드 및 단일 파일 번들 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;이슈&lt;br /&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. keycloak에 대해 익숙하지 않음&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;keyCloack을 init할 때 login-required, check-sso를 선택할 수 있는데 모든 서비스는 로그인이 됐다고 가정하고 Sso만 검증할 수 있는 check-sso를 사용해야 했습니다. check-sso의 기본 동작을 이해하고 있지 않아 동작 방식을 이해해야 했고 시간이 소요됐습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;background-color: #efefef; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;await keycloak.init({
    onLoad: 'check-sso',
    silentCheckSsoRedirectUri: `${location.origin}/silent-check-sso.html`
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;check-sso는 iframe을 만들어 계정 전환, 로그아웃이 됐을 때 이벤트를 받아야 하는데 silent-check-sso.html을 만들고 아래 내용과 같은 형태를 담으라고 적혀있었습니다 (&lt;a href=&quot;https://www.keycloak.org/securing-apps/javascript-adapter#_using_the_adapter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;keycloak adapter 링크&lt;/a&gt;). 인증이 됐다면, keycloak 서버에서 받은 token을 전달해주는 역할이라고 하지만 전체적인 흐름이 없었기에 이해하는데 어려움이 있었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737527349935&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;script&amp;gt;parent.postMessage(location.href, location.origin)&amp;lt;/script&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그아웃이 발생했을 때 onAuthLogout으로 이벤트를 받을 수 있는데, 이 때 로그아웃이 아닌 계정전환을 하더라도 1번 계정은 로그아웃되고 2번 계정이 로그인 됐다면 이벤트가 발생하여 어떤 Alert를 띄울지 판단하는데 해당 기능이 필요했습니다.&lt;br /&gt;KeycloakJs에서는 별도의 checkSso를 제공하지 않았고, 이를 해결하기 위해 &lt;a href=&quot;https://www.keycloak.org/securing-apps/javascript-adapter#custom-adapters&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;customAdapter&lt;/a&gt;를 만들게 되었습니다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; style=&quot;background-color: #efefef; color: #212529; text-align: start;&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;await keycloak.init({
    ...,
    adapter: MyKeycloakAdapter,
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과론적으로 onAuthLogout시에 필요한 this.MyKecloakAdapter.checkSso()를 호출했고 그 내부 로직에는 CheckSso에 사용되는 KeycloakJs코드를 복붙하여 수정하여 사용했습니다. 생각보다 &lt;a href=&quot;https://unpkg.com/browse/keycloak-js@19.0.1/dist/keycloak.mjs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JS파일&lt;/a&gt; 자체는 작았고, 아래 부분이 필요하여 조건문 중 필요한 부분만 사용했습니다. (ResponseMode에 fragment와 query가 있었고 fragment 부분만 남겨두거나 등등)&lt;/p&gt;
&lt;pre id=&quot;code_1737528259951&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   var checkSsoSilently = function() {
                var ifrm = document.createElement(&quot;iframe&quot;);
                var src = kc.createLoginUrl({prompt: 'none', redirectUri: kc.silentCheckSsoRedirectUri});
                ifrm.setAttribute(&quot;src&quot;, src);
                ifrm.setAttribute(&quot;title&quot;, &quot;keycloak-silent-check-sso&quot;);
                ifrm.style.display = &quot;none&quot;;
                document.body.appendChild(ifrm);

                var messageCallback = function(event) {
                    if (event.origin !== window.location.origin || ifrm.contentWindow !== event.source) {
                        return;
                    }

                    var oauth = parseCallback(event.data);
                    processCallback(oauth, initPromise);

                    document.body.removeChild(ifrm);
                    window.removeEventListener(&quot;message&quot;, messageCallback);
                };

                window.addEventListener(&quot;message&quot;, messageCallback);
            };

    function parseCallback(url) {
        var oauth = parseCallbackUrl(url);
        if (!oauth) {
            return;
        }

        var oauthState = callbackStorage.get(oauth.state);

        if (oauthState) {
            oauth.valid = true;
            oauth.redirectUri = oauthState.redirectUri;
            oauth.storedNonce = oauthState.nonce;
            oauth.prompt = oauthState.prompt;
            oauth.pkceCodeVerifier = oauthState.pkceCodeVerifier;
        }

        return oauth;
    }

    function parseCallbackUrl(url) {
        var supportedParams;
        switch (kc.flow) {
            case 'standard':
                supportedParams = ['code', 'state', 'session_state', 'kc_action_status'];
                break;
            case 'implicit':
                supportedParams = ['access_token', 'token_type', 'id_token', 'state', 'session_state', 'expires_in', 'kc_action_status'];
                break;
            case 'hybrid':
                supportedParams = ['access_token', 'token_type', 'id_token', 'code', 'state', 'session_state', 'expires_in', 'kc_action_status'];
                break;
        }

        supportedParams.push('error');
        supportedParams.push('error_description');
        supportedParams.push('error_uri');

        var queryIndex = url.indexOf('?');
        var fragmentIndex = url.indexOf('#');

        var newUrl;
        var parsed;

        if (kc.responseMode === 'query' &amp;amp;&amp;amp; queryIndex !== -1) {
            newUrl = url.substring(0, queryIndex);
            parsed = parseCallbackParams(url.substring(queryIndex + 1, fragmentIndex !== -1 ? fragmentIndex : url.length), supportedParams);
            if (parsed.paramsString !== '') {
                newUrl += '?' + parsed.paramsString;
            }
            if (fragmentIndex !== -1) {
                newUrl += url.substring(fragmentIndex);
            }
        } else if (kc.responseMode === 'fragment' &amp;amp;&amp;amp; fragmentIndex !== -1) {
            newUrl = url.substring(0, fragmentIndex);
            parsed = parseCallbackParams(url.substring(fragmentIndex + 1), supportedParams);
            if (parsed.paramsString !== '') {
                newUrl += '#' + parsed.paramsString;
            }
        }

        if (parsed &amp;amp;&amp;amp; parsed.oauthParams) {
            if (kc.flow === 'standard' || kc.flow === 'hybrid') {
                if ((parsed.oauthParams.code || parsed.oauthParams.error) &amp;amp;&amp;amp; parsed.oauthParams.state) {
                    parsed.oauthParams.newUrl = newUrl;
                    return parsed.oauthParams;
                }
            } else if (kc.flow === 'implicit') {
                if ((parsed.oauthParams.access_token || parsed.oauthParams.error) &amp;amp;&amp;amp; parsed.oauthParams.state) {
                    parsed.oauthParams.newUrl = newUrl;
                    return parsed.oauthParams;
                }
            }
        }
    }

    function parseCallbackParams(paramsString, supportedParams) {
        var p = paramsString.split('&amp;amp;');
        var result = {
            paramsString: '',
            oauthParams: {}
        };
        for (var i = 0; i &amp;lt; p.length; i++) {
            var split = p[i].indexOf(&quot;=&quot;);
            var key = p[i].slice(0, split);
            if (supportedParams.indexOf(key) !== -1) {
                result.oauthParams[key] = p[i].slice(split + 1);
            } else {
                if (result.paramsString !== '') {
                    result.paramsString += '&amp;amp;';
                }
                result.paramsString += p[i];
            }
        }
        return result;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. Cross origin 문제&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 중에는 silent-check-sso.html의 orgin이 기존 localhost에 동일하게 serving하고 있었지만 배포 이후 특정 URL에서 서빙하다보니 메시지가 정상적으로 받아지지 않았습니다. 디버깅을 위해 브라우저 디버기을 했고 이벤트 리스너에서 받은 message의 event.data가 token이 아닌 알 수 없는 내용이 왔고 postMessage가 정상적으로 되지 않고 있다고 생각했습니다.&lt;br /&gt;그리고 해결을 위해 아래와 같이 targetOrigin을 *로 변경하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737529011034&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;script&amp;gt;parent.postMessage(location.href, '*')&amp;lt;/script&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째로 &lt;b&gt;keyCloak.init({onLoad: 'check-sso'})&lt;/b&gt;를 할 때 checkSsoSliently()를 호출했는데,&lt;/p&gt;
&lt;pre id=&quot;code_1737529087113&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  var checkSsoSilently = function() {
                var ifrm = document.createElement(&quot;iframe&quot;);
                var src = kc.createLoginUrl({prompt: 'none', redirectUri: kc.silentCheckSsoRedirectUri});
                ifrm.setAttribute(&quot;src&quot;, src);
                ifrm.setAttribute(&quot;title&quot;, &quot;keycloak-silent-check-sso&quot;);
                ifrm.style.display = &quot;none&quot;;
                document.body.appendChild(ifrm);

                var messageCallback = function(event) {

                    if (event.origin !== window.location.origin || ifrm.contentWindow !== event.source) {
                       return;
                    }

		// logic&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트를 받았을 때 event.origin과 window.location.origin이 동일하지 않아 init을 할 수 없었습니다. pnpm patch를 통해 &lt;b&gt;'event.origin !== window.location.origin'&lt;/b&gt; 부분만&amp;nbsp;&lt;b&gt;'&lt;span style=&quot;background-color: #fbfdff; color: #24292e; text-align: start;&quot;&gt;kc.silentCheckSsoRedirectUri&lt;/span&gt;.includes(event.origin)'&lt;/b&gt;으로 변경하여 checkSilentSsoRedirectUri로부터 event.origin이 왔는지 검증하는 로직으로 변경하였습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. End of Thrid-party cookies&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇몇 브라우저에서 SameSite 정책으로 인해 CheckSso를 확인할 수 없었습니다. 크롬에서 작업을 끝난 후에 다른 브라우저를 확인했을 때 아래와 같이 modern browser에서 iframe과 cookie에 강하게 의존하여 동작하지 않았습니다. SameSite가 아닌 경우에도 이용할 수 있는 방법을 찾았지만 Chrome 또한 2025년 상반기에 해당 기능을 block할 계획을 갖고 있다고 발표했고 만들고 나서 아래와 같은 정보를 알게되었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;일부 브라우저의 최신 버전에서는 Chrome의 SameSite나 완전히 차단된 타사 쿠키와 같이 타사가 사용자를 추적하지 못하도록 다양한 쿠키 정책이 적용됩니다. 이러한 정책은 시간이 지남에 따라 더 제한적이 될 가능성이 높으며 다른 브라우저에서도 채택될 것입니다. 결국 타사 컨텍스트의 쿠키는 브라우저에서 완전히 지원되지 않고 차단될 수 있습니다. 결과적으로 영향을 받는 어댑터 기능은 궁극적으로 더 이상 지원되지 않을 수 있습니다.&quot;SameSite=Lax by Default&quot; 정책이 있는 브라우저SSL/TLS 연결이 Keycloak 측과 애플리케이션 측에 구성된 경우 모든 기능이 지원됩니다. 예를 들어, Chrome은 버전 84부터 영향을 받습니다.타사 쿠키가 차단된 브라우저Silent는&amp;nbsp;check-sso&amp;nbsp;지원되지 않으며 기본적으로 일반(비침묵)으로 돌아갑니다 . 이 동작은 메서드&amp;nbsp;에 전달된 옵션을&amp;nbsp;check-sso설정하여 변경할 수 있습니다&amp;nbsp;. 이 경우&amp;nbsp;제한적인 브라우저 동작이 감지되면 완전히 비활성화됩니다.Regular&amp;nbsp;check-sso도 영향을 받습니다. Session Status iframe이 지원되지 않으므로 어댑터가 초기화될 때 Keycloak으로 추가 리디렉션을 수행하여 사용자의 로그인 상태를 확인해야 합니다. 이 확인은 iframe을 사용하여 사용자가 로그인했는지 여부를 알려주고 리디렉션은 사용자가 로그아웃한 경우에만 수행되는 표준 동작과 다릅니다.&lt;/blockquote&gt;
&lt;div style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Google은 2024년에 모든 Chrome 사용자를 대상으로 타사 쿠키를 종료할 예정이며, Safari에서는 이미 기본적으로 비활성화되어 있습니다. 이것이 당신에게 어떤 영향을 미치는지 살펴보겠습니다. &lt;br /&gt;우선, ID 서버와 앱이 동일한 루트 도메인을 공유하는 경우 영향을 받지 않습니다. &lt;br /&gt;예를 들어, 다음과 같은 경우: 귀하의 앱은 www.example.com 또는 dashboard.example.com에 호스팅됩니다.예를 들어 Keycloak와 같은 귀하의 ID 서버는 auth.example.com에 호스팅됩니다.&lt;br /&gt;당신은 영향을 받지 않습니다 ✅. 실제로 www.example.com, dashboard.example.com 및 auth.example.com은 모두 동일한 루트 도메인인 example.com을 공유합니다. 반면에, 당신이 다음의 경우에 있다면:&lt;br /&gt;귀하의 앱은 www.examples.com 또는 dashboard.example.com에 호스팅됩니다.귀하의 ID 서버는 auth.sowhere-else.com에 호스팅됩니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;참조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://unpkg.com/browse/keycloak-js@19.0.1/dist/keycloak.mjs&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://unpkg.com/browse/keycloak-js@19.0.1/dist/keycloak.mjs&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737529847406&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;UNPKG - keycloak-js&quot; data-og-description=&quot;&quot; data-og-host=&quot;unpkg.com&quot; data-og-source-url=&quot;https://unpkg.com/browse/keycloak-js@19.0.1/dist/keycloak.mjs&quot; data-og-url=&quot;https://unpkg.com/browse/keycloak-js@19.0.1/dist/keycloak.mjs&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://unpkg.com/browse/keycloak-js@19.0.1/dist/keycloak.mjs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://unpkg.com/browse/keycloak-js@19.0.1/dist/keycloak.mjs&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;UNPKG - keycloak-js&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;unpkg.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.keycloak.org/securing-apps/javascript-adapter#_modern_browsers&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.keycloak.org/securing-apps/javascript-adapter#_modern_browsers&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737529873922&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Keycloak JavaScript adapter - Keycloak&quot; data-og-description=&quot;In some situations, you may need to run the adapter in environments that are not supported by default, such as Capacitor. To use the JavasScript client in these environments, you can pass a custom adapter. For example, a third-party library could provide s&quot; data-og-host=&quot;www.keycloak.org&quot; data-og-source-url=&quot;https://www.keycloak.org/securing-apps/javascript-adapter#_modern_browsers&quot; data-og-url=&quot;https://www.keycloak.org/securing-apps/javascript-adapter#_modern_browsers&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.keycloak.org/securing-apps/javascript-adapter#_modern_browsers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.keycloak.org/securing-apps/javascript-adapter#_modern_browsers&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Keycloak JavaScript adapter - Keycloak&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;In some situations, you may need to run the adapter in environments that are not supported by default, such as Capacitor. To use the JavasScript client in these environments, you can pass a custom adapter. For example, a third-party library could provide s&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.keycloak.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oidc-spa.dev/resources/end-of-third-party-cookies&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.oidc-spa.dev/resources/end-of-third-party-cookies&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737529881602&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;End of third-party cookies | OIDC SPA&quot; data-og-description=&quot;Resources End of third-party cookies TL;DR; It's mostly inconsequential. Google is ending third-party cookies for all Chrome users in 2024 and are already disabled by default in Safari. Let's see how it might affect you. First of all, if your identity s&quot; data-og-host=&quot;docs.oidc-spa.dev&quot; data-og-source-url=&quot;https://docs.oidc-spa.dev/resources/end-of-third-party-cookies&quot; data-og-url=&quot;https://docs.oidc-spa.dev/resources/end-of-third-party-cookies&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/0I751/hyX4mX8zxr/2kOJOz5olfJgieot3gmTvk/img.png?width=1695&amp;amp;height=846&amp;amp;face=0_0_1695_846,https://scrap.kakaocdn.net/dn/bzqr6k/hyX4qfaZDr/2IFNEGmYLWw7FcLfPTHOzk/img.png?width=1695&amp;amp;height=846&amp;amp;face=0_0_1695_846&quot;&gt;&lt;a href=&quot;https://docs.oidc-spa.dev/resources/end-of-third-party-cookies&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.oidc-spa.dev/resources/end-of-third-party-cookies&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/0I751/hyX4mX8zxr/2kOJOz5olfJgieot3gmTvk/img.png?width=1695&amp;amp;height=846&amp;amp;face=0_0_1695_846,https://scrap.kakaocdn.net/dn/bzqr6k/hyX4qfaZDr/2IFNEGmYLWw7FcLfPTHOzk/img.png?width=1695&amp;amp;height=846&amp;amp;face=0_0_1695_846');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;End of third-party cookies | OIDC SPA&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Resources End of third-party cookies TL;DR; It's mostly inconsequential. Google is ending third-party cookies for all Chrome users in 2024 and are already disabled by default in Safari. Let's see how it might affect you. First of all, if your identity s&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.oidc-spa.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>software engineering/frontend</category>
      <author>Hyeong Nim</author>
      <guid isPermaLink="true">https://doohyeong.tistory.com/256</guid>
      <comments>https://doohyeong.tistory.com/256#entry256comment</comments>
      <pubDate>Wed, 22 Jan 2025 12:28:53 +0900</pubDate>
    </item>
  </channel>
</rss>