<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>공부하자</title>
    <link>https://5ffthewall.tistory.com/</link>
    <description>소프트웨어학부생 공부기록용</description>
    <language>ko</language>
    <pubDate>Tue, 26 May 2026 10:19:29 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>solfa</managingEditor>
    <item>
      <title>오픈클로(OpenClaw) 설치부터 제미나이 연동까지... 나만의 AI 비서 만들기(feat. 찍먹 후기)</title>
      <link>https://5ffthewall.tistory.com/143</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 요즘 AI 에이전트가 하도 핫해서 저도 한 번 찍먹해봤습니다.&lt;br /&gt;이름은 &lt;b&gt;OpenClaw(구 Clawdbot)&lt;/b&gt;인데, 내 로컬 PC에서 돌아가는 AI 비서라고 보면 돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-02 오전 2.28.45.png&quot; data-origin-width=&quot;3018&quot; data-origin-height=&quot;1702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXTdqv/dJMcabpxhqp/3YuZvFUBBeDfKK5zwKx1rk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXTdqv/dJMcabpxhqp/3YuZvFUBBeDfKK5zwKx1rk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXTdqv/dJMcabpxhqp/3YuZvFUBBeDfKK5zwKx1rk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXTdqv%2FdJMcabpxhqp%2F3YuZvFUBBeDfKK5zwKx1rk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3018&quot; height=&quot;1702&quot; data-filename=&quot;스크린샷 2026-02-02 오전 2.28.45.png&quot; data-origin-width=&quot;3018&quot; data-origin-height=&quot;1702&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://openclaw.ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://openclaw.ai/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1769966936527&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;OpenClaw &amp;mdash; Personal AI Assistant&quot; data-og-description=&quot;OpenClaw &amp;mdash; The AI that actually does things. Your personal assistant on any platform.&quot; data-og-host=&quot;openclaw.ai&quot; data-og-source-url=&quot;https://openclaw.ai/&quot; data-og-url=&quot;https://openclaw.ai/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eoSynY/dJMb9gxgo5K/xpykJKy4kMKEKKkeBeNdik/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cxEDJN/dJMb9iICa1z/2bCsRT9PMWH3K2xA9KKVh1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/pVA4d/dJMb9gxgo5J/2et2cx6RhSRueVOH917WK1/img.jpg?width=400&amp;amp;height=400&amp;amp;face=117_110_268_275&quot;&gt;&lt;a href=&quot;https://openclaw.ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://openclaw.ai/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eoSynY/dJMb9gxgo5K/xpykJKy4kMKEKKkeBeNdik/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cxEDJN/dJMb9iICa1z/2bCsRT9PMWH3K2xA9KKVh1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/pVA4d/dJMb9gxgo5J/2et2cx6RhSRueVOH917WK1/img.jpg?width=400&amp;amp;height=400&amp;amp;face=117_110_268_275');&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;OpenClaw &amp;mdash; Personal AI Assistant&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;OpenClaw &amp;mdash; The AI that actually does things. Your personal assistant on any platform.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;openclaw.ai&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;6&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6&quot;&gt;왜 쓰냐?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8&quot;&gt;내가 자는 동안 얘한테 시킬 수 있는 것들:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;뉴스/깃허브 요약:&lt;/b&gt; &quot;나 자는 동안 올라온 중요한 기술 뉴스나 관심 있는 Repo 업데이트 있으면 정리해 놔&quot; 하면 아침에 일어나서 요약본만 읽으면 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot;&gt;메일함 정리:&lt;/b&gt; 광고 메일 싹 날리고 중요한 메일만 골라놓게 시킬 수 있어요.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,2,0&quot;&gt;모니터링:&lt;/b&gt; 특정 사이트 가격 변동이나 티켓팅 오픈 같은 거 감시하게 시켜놓고 잠들면 됩니다. 항공권같은 거 이제 최저가 파악 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 일단 설치부터&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;준비물은 간단합니다. Node.js(22버전 이상)랑 Git-bash&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 가장 중요한 AI API Key가 필요해요. 제미나이, 클로드나 gpt도 됩니다!&lt;br /&gt;&lt;br /&gt;설치&amp;nbsp;커맨드는&amp;nbsp;다음과&amp;nbsp;같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769967074823&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i -g openclaw
openclaw onboard&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/egKfCg/dJMcag5rQBS/8FNf9Y4vYx0m8HGAuGKT30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/egKfCg/dJMcag5rQBS/8FNf9Y4vYx0m8HGAuGKT30/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1542&quot; data-origin-height=&quot;1146&quot; data-filename=&quot;스크린샷 2026-02-02 오전 2.33.25.png&quot; style=&quot;width: 45.9732%; margin-right: 10px;&quot; data-widthpercent=&quot;46.51&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/egKfCg/dJMcag5rQBS/8FNf9Y4vYx0m8HGAuGKT30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FegKfCg%2FdJMcag5rQBS%2F8FNf9Y4vYx0m8HGAuGKT30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1542&quot; height=&quot;1146&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ejhZGE/dJMcahpKh8u/UmIQiA1G3z1ubaljLEjn01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ejhZGE/dJMcahpKh8u/UmIQiA1G3z1ubaljLEjn01/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;614&quot; data-filename=&quot;스크린샷 2026-02-02 오전 2.33.10.png&quot; data-widthpercent=&quot;53.49&quot; style=&quot;width: 52.864%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ejhZGE/dJMcahpKh8u/UmIQiA1G3z1ubaljLEjn01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FejhZGE%2FdJMcahpKh8u%2FUmIQiA1G3z1ubaljLEjn01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;950&quot; height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이렇게 치면 막 뭐라고 물어보는데, 그냥 QuickStart로 쭉쭉 넘기면 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;15&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;I understand this is powerful and inherently risky. Continue? -&amp;gt; &lt;b data-index-in-node=&quot;65&quot; data-path-to-node=&quot;15,0,0&quot;&gt;Yes&lt;/b&gt;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Onboarding mode -&amp;gt; &lt;b data-index-in-node=&quot;19&quot; data-path-to-node=&quot;15,1,0&quot;&gt;QuickStart&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16&quot;&gt;AI 모델(제미나이) 연결&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;17&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Model/auth provider -&amp;gt; &lt;b data-index-in-node=&quot;23&quot; data-path-to-node=&quot;17,0,0&quot;&gt;Google&lt;/b&gt;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Google auth method -&amp;gt; &lt;b data-index-in-node=&quot;22&quot; data-path-to-node=&quot;17,1,0&quot;&gt;Google Gemini API key&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Enter Gemini API key -&amp;gt; 아까 구글 AI 스튜디오에서 받아온 키를 복사-붙여넣기 하세요.&lt;/li&gt;
&lt;li&gt;Default model -&amp;gt; 제미니는 구글 AI 스튜디오 Home &amp;gt; Quick Start 보면 현재 쓸 수 있는 모델명이 나와요&lt;/li&gt;
&lt;li&gt;Select channel -&amp;gt; &lt;b data-index-in-node=&quot;18&quot; data-path-to-node=&quot;21,0,0&quot;&gt;Skip for now&lt;/b&gt; (나중에 텔레그램이나 다른 거 연동할 때 해도 됩니다.)&lt;/li&gt;
&lt;li&gt;Configure skills now? -&amp;gt; &lt;b data-index-in-node=&quot;25&quot; data-path-to-node=&quot;21,1,0&quot;&gt;No&lt;/b&gt; (이것도 나중에 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22&quot;&gt;⑤ 훅(Hooks) 설정 (이건 3개 다 하세요!)&lt;/b&gt; Enable hooks? 라고 나오는데, 스페이스 바 눌러서 3개 다 체크하고 엔터&amp;nbsp; ㄱㄱ&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;23&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;23,0,0&quot;&gt;boot-md:&lt;/b&gt; 봇 켜질 때 내가 만든 규칙(마크다운 파일)을 읽어와요. &quot;너는 내 코딩 노예야&quot; 같은 페르소나 주입용!&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;23,1,0&quot;&gt;command-logger:&lt;/b&gt; 내가 시킨 거 다 기록해 줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;23,2,0&quot;&gt;session-memory:&lt;/b&gt; 대화 맥락을 기억해 줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 실제 구동 화면&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 완료되면 사진과 같은 대시보드 링크가 뜹니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d55aCZ/dJMcaac5lQ8/I9fNd28aMxvmO0M7oKl96k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d55aCZ/dJMcaac5lQ8/I9fNd28aMxvmO0M7oKl96k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d55aCZ/dJMcaac5lQ8/I9fNd28aMxvmO0M7oKl96k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd55aCZ%2FdJMcaac5lQ8%2FI9fNd28aMxvmO0M7oKl96k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;992&quot; height=&quot;350&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3014&quot; data-origin-height=&quot;804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmgCxe/dJMcad1SWY2/0fWGTRdtTNMWmKAsIjFhd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmgCxe/dJMcad1SWY2/0fWGTRdtTNMWmKAsIjFhd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmgCxe/dJMcad1SWY2/0fWGTRdtTNMWmKAsIjFhd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmgCxe%2FdJMcad1SWY2%2F0fWGTRdtTNMWmKAsIjFhd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3014&quot; height=&quot;804&quot; data-origin-width=&quot;3014&quot; data-origin-height=&quot;804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Dashboard 모습! 좌측에 메뉴가 있고 상단에 `Health OK`가 떠야 정상입니다.&lt;br /&gt;&lt;br /&gt;생각보다 UI가 깔끔해서 놀랐어요. 내가 만든 웹사이트보다 나은 듯&lt;br /&gt;&lt;br /&gt;이제 연동된 제미나이랑 대화를 해봤습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-02 오전 2.38.17.png&quot; data-origin-width=&quot;2384&quot; data-origin-height=&quot;642&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVhIzF/dJMcaihSfkb/jNafEseENe5RBV6OC6YxHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVhIzF/dJMcaihSfkb/jNafEseENe5RBV6OC6YxHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVhIzF/dJMcaihSfkb/jNafEseENe5RBV6OC6YxHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVhIzF%2FdJMcaihSfkb%2FjNafEseENe5RBV6OC6YxHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2384&quot; height=&quot;642&quot; data-filename=&quot;스크린샷 2026-02-02 오전 2.38.17.png&quot; data-origin-width=&quot;2384&quot; data-origin-height=&quot;642&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 데덴네 귀엽죠??&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 그래서 쓸만한가?&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-02 오전 2.41.06.png&quot; data-origin-width=&quot;2308&quot; data-origin-height=&quot;1404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cl4zTe/dJMcacPvZPG/nesSVkYKGZ3G2dKGiULJBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cl4zTe/dJMcacPvZPG/nesSVkYKGZ3G2dKGiULJBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cl4zTe/dJMcacPvZPG/nesSVkYKGZ3G2dKGiULJBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcl4zTe%2FdJMcacPvZPG%2FnesSVkYKGZ3G2dKGiULJBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2308&quot; height=&quot;1404&quot; data-filename=&quot;스크린샷 2026-02-02 오전 2.41.06.png&quot; data-origin-width=&quot;2308&quot; data-origin-height=&quot;1404&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음... 학생 무료 제미니 api 갖다 쓰니 제 데덴네가 멈췄어요,,,&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 이런 일 들 할 수 있어요&lt;br /&gt;&lt;br /&gt;-&amp;gt; 내가 관심가진 채용공고들 싹 모아서 매일 8시에 알려줘 + 내가 기존 가진 이력서 줄테니 그걸로 각 회사에 맞게 지원서 써주고 아침마다 날 찾아와 이런 거 가능 ㅋㅋㅋㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. 마치며&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항공권 특가 cron 봇 만들어서 실제로 최저가에 결제해서 일본 가는 후기로 찾아오겠습니다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치하다가 막히는 거 있으면 댓글 남겨주세요! 아는 선에서 답변해 드릴게요.&lt;br /&gt;&lt;br /&gt;#OpenClaw #오픈클로 #제미나이 #Gemini #AI비서 #나만의AI #공부기록 #IT블로그 #소프트웨어학부&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1769966922348&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - openclaw/openclaw: Your own personal AI assistant. Any OS. Any Platform. The lobster way.  &quot; data-og-description=&quot;Your own personal AI assistant. Any OS. Any Platform. The lobster way.   - GitHub - openclaw/openclaw: Your own personal AI assistant. Any OS. Any Platform. The lobster way.  &quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/openclaw/openclaw&quot; data-og-url=&quot;https://github.com/openclaw/openclaw&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/exglFG/dJMb85WOiix/EuOmq1QeBp1lAu88OlGHHk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cYuwco/dJMb895YD5M/4H4LQr8U3966BvkCXJRtT1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/openclaw/openclaw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/openclaw/openclaw&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/exglFG/dJMb85WOiix/EuOmq1QeBp1lAu88OlGHHk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cYuwco/dJMb895YD5M/4H4LQr8U3966BvkCXJRtT1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - openclaw/openclaw: Your own personal AI assistant. Any OS. Any Platform. The lobster way.  &lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Your own personal AI assistant. Any OS. Any Platform. The lobster way.   - GitHub - openclaw/openclaw: Your own personal AI assistant. Any OS. Any Platform. The lobster way.  &lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&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>
      <author>solfa</author>
      <guid isPermaLink="true">https://5ffthewall.tistory.com/143</guid>
      <comments>https://5ffthewall.tistory.com/143#entry143comment</comments>
      <pubDate>Mon, 2 Feb 2026 02:42:52 +0900</pubDate>
    </item>
    <item>
      <title>AI 인테리어 디자인의 혁명: 2025년 최고의 도구 8선</title>
      <link>https://5ffthewall.tistory.com/141</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;상상만 하던 인테리어를 실제로 구현할 수 있다면 어떨까요? AI 인테리어 디자인 도구의 등장으로 이제 누구나 전문가 수준의 공간 디자인이 가능해졌습니다. 특히 오늘의집 AI (Ohouse AI)는 단순히 예쁜 이미지를 만드는 것을 넘어, 실제 구매 가능한 가구로 디자인을 구성해 바로 주문할 수 있다는 점에서 게임 체인저가 되고 있습니다. 이 글에서는 2025년 주목해야 할 AI 인테리어 도구 8가지를 소개하고, 왜 오늘의집 AI가 한국 사용자에게 최고의 선택인지 자세히 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2025년 최고의 AI 인테리어 디자인 도구 8선&lt;/li&gt;
&lt;li&gt;오늘의집 AI가 특별한 이유: 상상을 현실로&lt;/li&gt;
&lt;li&gt;나에게 맞는 AI 인테리어 도구 선택 가이드&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;2025년 최고의 AI 인테리어 디자인 도구 8선&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 오늘의집 AI (Ohouse AI) -   강력 추천&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;디자인에서 구매까지, 꿈이 현실이 되는 유일한 플랫폼&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘의집 AI는 단순한 AI 인테리어 디자인 도구가 아닙니다. &lt;b&gt;한국 최대 인테리어 커머스 플랫폼인 오늘의집이 보유한 200만 개 이상의 실제 판매 상품 데이터를 기반으로 작동&lt;/b&gt;하는, 진짜 '살 수 있는' 인테리어를 만들어주는 혁신적인 서비스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오늘의집 AI만의 독보적인 장점:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 실제 구매 가능한 가구로만 디자인 구성&lt;/b&gt; 다른 AI 도구들이 가상의 가구로 그럴듯한 이미지만 생성한다면, 오늘의집 AI는 &lt;b&gt;현재 오늘의집에서 판매 중인 실제 상품으로만 디자인을 구성&lt;/b&gt;합니다. 마음에 드는 소파를 발견했다면? 클릭 한 번으로 상품 페이지로 이동해 가격, 사이즈, 리뷰까지 확인하고 바로 구매할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 예산 맞춤 디자인 제안&lt;/b&gt; &quot;500만원 예산으로 거실 꾸미기&quot;처럼 예산을 설정하면, AI가 해당 금액 내에서 구매 가능한 실제 상품들로 여러 가지 디자인 옵션을 제시합니다. 각 아이템의 정확한 가격이 표시되어 총 예산을 실시간으로 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 한국 주거 환경 완벽 대응&lt;/b&gt; 25평 아파트, 원룸, 오피스텔 등 한국 특유의 주거 형태를 완벽히 이해하고 있습니다. 특히 &lt;b&gt;베란다 확장, 안방 드레스룸, 현관 신발장&lt;/b&gt; 같은 한국형 공간 구성을 정확히 반영합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오늘의집 AI 사용 가이드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;STEP 1: 공간 사진 업로드 및 정보 입력&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹브라우저에서 ohouse.ai/ai-interior 접속 (설치 불필요!)&lt;/li&gt;
&lt;li&gt;현재 방 사진 업로드 또는 평수/구조 선택&lt;/li&gt;
&lt;li&gt;&quot;25평 아파트 거실&quot; 같은 구체적 정보 입력&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;STEP 2: 스타일과 예산 설정&lt;/b&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;b&gt;예산 범위 설정&lt;/b&gt; (예: 300-500만원)&lt;/li&gt;
&lt;li&gt;선호 브랜드나 특정 가구 유형 지정 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;STEP 3: AI 디자인 생성 및 상품 확인&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;3초 만에 5-10개의 디자인 옵션 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;각 가구 위에 마우스를 올리면 실제 상품명과 가격 표시&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;마음에 드는 아이템 클릭 시 상품 상세 페이지로 바로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;STEP 4: 장바구니 담기 및 구매&lt;/b&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;b&gt;오늘의집 포인트 적립 및 할인 혜택 자동 적용&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;STEP 5: 시공 서비스 연결 (선택사항)&lt;/b&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;3D 디자인을 기반으로 정확한 견적 산출&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Dreamina&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dreamina는 글로벌 시장을 타겟으로 한 범용 AI 디자인 도구입니다. 다양한 편집 기능이 강점이지만, &lt;b&gt;생성된 가구를 실제로 구매할 수 없다는 한계&lt;/b&gt;가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 기능:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI 인페인팅으로 부분 수정&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;&lt;b&gt;한계점:&lt;/b&gt; 실제 구매 연결 불가, 한국 주거 환경 이해 부족&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Planner 5D&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2D/3D 전환이 자유로운 디자인 툴이지만, &lt;b&gt;가상의 가구 라이브러리만 제공&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 기능:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;6,000개 가상 가구 아이템&lt;/li&gt;
&lt;li&gt;평면도 작성 기능&lt;/li&gt;
&lt;li&gt;VR 모드 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한계점:&lt;/b&gt; 실제 상품 정보 없음, 한국 가구 브랜드 부재&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. RoomGPT&lt;/h3&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;&lt;b&gt;주요 기능:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2-3분 내 빠른 결과&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;&lt;b&gt;한계점:&lt;/b&gt; 디자인만 제공, 구매나 시공 연결 없음&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Homestyler&lt;/h3&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;&lt;b&gt;주요 기능:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;12K 초고해상도 렌더링&lt;/li&gt;
&lt;li&gt;해외 브랜드 가구 라이브러리&lt;/li&gt;
&lt;li&gt;VR 투어 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한계점:&lt;/b&gt; 한국 브랜드 부족, 실제 구매 프로세스 복잡&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. Foyr Neo&lt;/h3&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;&lt;b&gt;주요 기능:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;60,000개 3D 모델&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;&lt;b&gt;한계점:&lt;/b&gt; 높은 월 구독료 (월 $49~), 학습 곡선 높음&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. Autodesk Revit&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;건축 전문 BIM 소프트웨어로 &lt;b&gt;일반 사용자용이 아닙니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 기능:&lt;/b&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;BIM 통합&lt;/li&gt;
&lt;li&gt;전문가 협업&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한계점:&lt;/b&gt; 매우 비싼 라이선스, 전문 교육 필요&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. DecorMatters&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AR 기능은 재미있지만 &lt;b&gt;실용성이 떨어집니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 기능:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AR 가구 배치&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;&lt;b&gt;한계점:&lt;/b&gt; 정확도 낮음, 한국 시장 미지원&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘의집 AI가 특별한 이유: 상상을 현실로&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 실제 구매 데이터 기반 AI 학습&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘의집 AI는 &lt;b&gt;1,000만 사용자의 실제 구매 패턴과 200만 개 상품 데이터&lt;/b&gt;를 학습했습니다. 이는 단순히 예쁜 이미지를 만드는 것이 아니라, 실제로 한국인이 선호하고 구매하는 인테리어를 제안한다는 의미입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 원클릭 구매 시스템&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;이 디자인 마음에 들어요!&quot; &amp;rarr; &quot;전체 구매하기&quot; &amp;rarr; 3일 후 배송 완료&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모든 과정이 오늘의집 AI에서는 현실입니다. 디자인에 사용된 모든 가구와 소품을:&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;li&gt;무료 배송 (조건 충족 시)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 가격 투명성&lt;/h3&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;b&gt;총 예산 실시간 표시&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;아이템별 정가와 할인가 비교&lt;/li&gt;
&lt;li&gt;대체 가능한 저렴한 옵션 제시&lt;/li&gt;
&lt;li&gt;시즌 세일 상품 우선 추천&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 실제 사용자 리뷰 연동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 제안한 가구들의:&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;/li&gt;
&lt;li&gt;실제 사용 사진 확인 가능&lt;/li&gt;
&lt;li&gt;사이즈, 색상 관련 구매 팁 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 통합 시공 서비스&lt;/h3&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;b&gt;오늘의집 인테리어 시공 견적 요청&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;AI 디자인 기반 정확한 비용 산출&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;나에게 맞는 AI 인테리어 도구 선택 가이드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘의집 AI를 선택해야 하는 경우:&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;단순 아이디어 스케치만 필요: RoomGPT&lt;/li&gt;
&lt;li&gt;전문 건축 설계: Autodesk Revit&lt;/li&gt;
&lt;li&gt;해외 거주자: Homestyler, Planner 5D&lt;/li&gt;
&lt;li&gt;순수 창작 목적: Dreamina&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 사용자 후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;김민지 (32세, 서울)&lt;/b&gt; &quot;오늘의집 AI로 거실 디자인 만들고 마음에 들어서 세트로 구매했어요. 상상했던 그대로 우리 집이 바뀌었고, 총 비용도 예산 내였어요!&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;박준호 (28세, 경기)&lt;/b&gt; &quot;다른 AI 툴은 그림만 예쁘게 나오는데, 오늘의집 AI는 진짜 살 수 있는 가구로 보여줘서 실용적이에요. 디자인 본 그대로 구매할 수 있다는 게 최고!&quot;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 인테리어 디자인의 미래는 단순히 예쁜 이미지를 생성하는 것이 아닙니다. &lt;b&gt;실제로 구현 가능한, 구매 가능한, 예산에 맞는 현실적인 디자인&lt;/b&gt;을 제공하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오늘의집 AI는 이 모든 것을 가능하게 하는 유일한 플랫폼&lt;/b&gt;입니다. 200만 개의 실제 상품, 1,000만 사용자의 구매 데이터, 검증된 시공 파트너, 그리고 강력한 AI 기술이 결합되어 당신의 상상을 현실로 만들어드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 바로 ohouse.ai/ai-interior 에서 무료로 시작해보세요. 설치 없이 웹브라우저에서 바로 사용 가능하며, 첫 디자인은 완전 무료입니다!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 묻는 질문&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q1. 오늘의집 AI에서 만든 디자인 그대로 구매할 수 있나요?&lt;/b&gt; 네, 100% 가능합니다! AI가 생성한 디자인의 모든 가구와 소품은 현재 오늘의집에서 판매 중인 실제 상품입니다. 각 아이템을 클릭하면 바로 상품 페이지로 이동하여 상세 정보를 확인하고 구매할 수 있습니다. 세트 구매 시 추가 할인도 제공됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q2. 예산을 설정하면 정말 그 안에서 디자인을 만들어주나요?&lt;/b&gt; 네, 오늘의집 AI의 핵심 기능 중 하나입니다. 예를 들어 &quot;300만원 예산&quot;을 설정하면, AI가 실제 판매가 기준으로 300만원 이내의 상품들로만 디자인을 구성합니다. 각 디자인 옵션마다 정확한 총액이 표시되며, 예산을 초과하지 않는 대체 상품도 추천해드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q3. 다른 AI 도구와 비교해서 디자인 퀄리티는 어떤가요?&lt;/b&gt; 오늘의집 AI는 실제 판매 상품의 고품질 이미지를 활용하기 때문에 매우 현실적이고 정확한 디자인을 생성합니다. 또한 1,000만 사용자의 실제 인테리어 데이터를 학습했기 때문에 한국 주거 환경에 최적화된, 실제로 적용 가능한 디자인을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q4. 사용료가 있나요?&lt;/b&gt; 기본 기능은 완전 무료입니다! 하루 10회까지 무료로 디자인을 생성할 수 있으며, 생성된 디자인의 상품을 구매하는 데는 추가 비용이 없습니다. 더 많은 디자인 생성이나 고급 기능을 원하시면 월 9,900원의 프리미엄 플랜을 이용하실 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q5. 시공 서비스도 연결되나요?&lt;/b&gt; 네, 오늘의집의 검증된 인테리어 시공 파트너를 통해 시공 서비스를 받으실 수 있습니다. AI 디자인을 바탕으로 정확한 견적을 받을 수 있으며, 오늘의집 앱에서 시공 진행 상황을 실시간으로 확인할 수 있습니다.&lt;/p&gt;</description>
      <category>AI</category>
      <category>ai인테리어</category>
      <category>방꾸미기</category>
      <category>인테리어</category>
      <category>집꾸미기</category>
      <author>solfa</author>
      <guid isPermaLink="true">https://5ffthewall.tistory.com/141</guid>
      <comments>https://5ffthewall.tistory.com/141#entry141comment</comments>
      <pubDate>Fri, 19 Sep 2025 16:12:16 +0900</pubDate>
    </item>
    <item>
      <title>[React/리액트] React에서 순차적 API 호출 처리하기: 위치 기반 버스 도착 정보 조회 사례</title>
      <link>https://5ffthewall.tistory.com/140</link>
      <description>&lt;h1&gt;React에서 순차적 API 호출 처리하기: 위치 기반 버스 도착 정보 조회 사례&lt;/h1&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;b&gt;한 API의 결과가 다른 API를 호출하는 전제 조건&lt;/b&gt;이 되는 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버스 번호판 인식 앱을 개발하다가&amp;nbsp;사용자의 위치를 먼저 서버에 전송하고, 그 위치 기반으로 버스 도착 정보를 조회해야 하는 상황이 발생했다. 이런 순차적 API 호출은 React에서 어떻게 처리할 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;React Query의 Dependent Queries&lt;/b&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;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 버스 번호를 입력하면 이 후에 위치 정보가 서버로 전송됨&lt;/li&gt;
&lt;li&gt;서버에서 위치를 처리하는데 약간의 시간이 필요함&lt;/li&gt;
&lt;li&gt;위치가 처리되면 해당 위치 기반으로 버스 도착 정보를 조회할 수 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하지만 위치 처리 전에 버스 도착 정보를 요청하면 빈 결과가 반환됨&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;다양한 해결 방법들&lt;br /&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;방법 1: 간단한 타임아웃 접근&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1746356224991&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ❌ 안타깝게도 이 방법은 예측 불가능
useEffect(() =&amp;gt; {
  locationTracker.startTracking();
  
  // 2초 후에 버스 정보 가져오기... 하지만 항상 충분한 시간일까?
  setTimeout(() =&amp;gt; {
    getBusArrival();
  }, 2000);
}, []);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법의 문제점&lt;/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;너무 짧으면 데이터를 놓치고, 너무 길면 UX가 저하됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;방법 2: Promise 체이닝&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1746356771015&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  let mounted = true;
  
  locationTracker.startTracking()
    .then(() =&amp;gt; new Promise(resolve =&amp;gt; setTimeout(resolve, 2000)))
    .then(() =&amp;gt; mounted &amp;amp;&amp;amp; getBusArrival())
    .then(data =&amp;gt; mounted &amp;amp;&amp;amp; setExpectedBuses(data))
    .catch(error =&amp;gt; console.error(error));
    
  return () =&amp;gt; { mounted = false; };
}, []);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장단점&lt;/b&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;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;방법 3: async/await + 상태 관리&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1746356794674&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  let mounted = true;
  
  const fetchData = async () =&amp;gt; {
    try {
      await locationTracker.startTracking();
      await new Promise(resolve =&amp;gt; setTimeout(resolve, 2000));
      if (mounted) {
        const data = await getBusArrival();
        setExpectedBuses(data);
      }
    } catch (error) {
      console.error(error);
    }
  };
  
  fetchData();
  return () =&amp;gt; { mounted = false; };
}, []);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장단점&lt;/b&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;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;방법 4: React Query의 Dependent Queries (현재 적용한 방법)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 위치 상태 확인 쿼리&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const { data: locationStatus } = useQuery({
  queryKey: ['locationStatus'],
  queryFn: async () =&amp;gt; {
    // 위치 추적이 시작되지 않았다면 시작
    if (!locationTracker.isTracking()) {
      locationTracker.startTracking();
      await new Promise(resolve =&amp;gt; setTimeout(resolve, 2000));
    }
    
    // 서버에서 위치를 처리했는지 확인
    try {
      await getBusArrival();
      return true; // 성공하면 위치가 처리된 것
    } catch {
      return false; // 실패하면 아직 처리 중
    }
  },
  refetchInterval: (data) =&amp;gt; data ? false : 2000, // 성공하면 멈춤
  enabled: true,
});
&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;위치 추적 여부를 먼저 확인하고, 추적 중이 아니면 자동으로 시작&lt;/li&gt;
&lt;li&gt;getBusArrival()을 시험 삼아 호출하여 서버가 위치를 처리했는지 확인하는 창의적인 방법&lt;/li&gt;
&lt;li&gt;성공하면 true를 반환하여 의존 쿼리 실행 허용&lt;/li&gt;
&lt;li&gt;refetchInterval 함수를 사용하여 성공 후에는 폴링 중지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 의존성이 있는 버스 도착 정보 쿼리&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const { data: expectedBuses = [] } = useQuery&amp;lt;BusInfo[]&amp;gt;({
  queryKey: ['busArrivals'],
  queryFn: getBusArrival,
  refetchInterval: 30000,
  enabled: locationStatus === true, //   핵심: 위치가 준비되었을 때만 실행
});
&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;enabled 옵션으로 locationStatus가 true일 때만 실행되도록 설정&lt;/li&gt;
&lt;li&gt;30초마다 새로운 버스 도착 정보를 가져오도록 자동 갱신 설정&lt;/li&gt;
&lt;li&gt;기본값으로 빈 배열을 설정하여 undefined 에러 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;React Query의 Dependent Queries란?&lt;br /&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;React Query 공식 문서에 따르면&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;Dependent (or serial) queries depend on previous ones to finish before they can execute. To achieve this, it's as easy as using the enabled option to tell a query when it is ready to run.&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1746356588392&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;Dependent Queries | TanStack Query React Docs&quot; data-og-description=&quot;useQuery dependent Query Dependent (or serial) queries depend on previous ones to finish before they can execute. To achieve this, it's as easy as using the enabled option to tell a query when it is r...&quot; data-og-host=&quot;tanstack.com&quot; data-og-source-url=&quot;https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries&quot; data-og-url=&quot;https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/n0Nhy/hyYMU02P8W/sWumtbitUZljGUq6g5qMHk/img.png?width=3000&amp;amp;height=1704&amp;amp;face=0_0_3000_1704,https://scrap.kakaocdn.net/dn/OwoVm/hyYPdFgpZR/zGHiKnqnUGiFKhEkZGKuv0/img.png?width=3000&amp;amp;height=1704&amp;amp;face=0_0_3000_1704&quot;&gt;&lt;a href=&quot;https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/n0Nhy/hyYMU02P8W/sWumtbitUZljGUq6g5qMHk/img.png?width=3000&amp;amp;height=1704&amp;amp;face=0_0_3000_1704,https://scrap.kakaocdn.net/dn/OwoVm/hyYPdFgpZR/zGHiKnqnUGiFKhEkZGKuv0/img.png?width=3000&amp;amp;height=1704&amp;amp;face=0_0_3000_1704');&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;Dependent Queries | TanStack Query React Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;useQuery dependent Query Dependent (or serial) queries depend on previous ones to finish before they can execute. To achieve this, it's as easy as using the enabled option to tell a query when it is r...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tanstack.com&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Dependent Queries&lt;/b&gt;는 특정 쿼리가 다른 쿼리의 결과에 의존하는 경우 사용된다. enabled 옵션을 사용하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;작동 원리&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. enabled 옵션이 false인 동안 쿼리는 실행되지 않는다.&lt;br /&gt;2. enabled가 true로 변경되면 쿼리가 실행된다.&lt;br /&gt;3. 이를 통해 데이터 fetch의 순서를 제어할 수 있다.&lt;/blockquote&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// locationStatus가 true일 때만 expectedBuses 쿼리 실행
enabled: locationStatus === true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상태 관리 흐름&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기 상태: status: 'pending', fetchStatus: 'idle'&lt;/li&gt;
&lt;li&gt;의존성 충족 시: status: 'pending', fetchStatus: 'fetching'&lt;/li&gt;
&lt;li&gt;데이터 로드 완료: status: 'success', fetchStatus: 'idle'&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 폴링 최적화&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;refetchInterval: (data) =&amp;gt; data ? false : 2000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최적화 포인트&lt;/b&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 쿼리 무효화 전략&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const queryClient = useQueryClient();

// 위치 변경 시 버스 정보 무효화
locationTracker.onLocationChange(() =&amp;gt; {
  queryClient.invalidateQueries(['busArrivals']);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 일관성 유지&lt;/b&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;더 나은 해결 방안들&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;방법 5: Server-Sent Events (SSE)&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1746356866710&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 클라이언트
useEffect(() =&amp;gt; {
  // 위치 전송
  await postUserLocation({ latitude, longitude });
  
  // SSE 연결 - 서버가 준비되면 알림
  const eventSource = new EventSource('/api/bus-arrival-stream');
  
  eventSource.onmessage = (event) =&amp;gt; {
    const data = JSON.parse(event.data);
    setExpectedBuses(data);
    eventSource.close();
  };
  
  eventSource.onerror = (error) =&amp;gt; {
    console.error('SSE Error:', error);
    eventSource.close();
  };
  
  return () =&amp;gt; eventSource.close();
}, []);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&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;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;방법 6: WebSocket&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1746356897638&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 클라이언트
useEffect(() =&amp;gt; {
  const ws = new WebSocket('ws://localhost:3000/bus-updates');
  
  ws.onopen = () =&amp;gt; {
    // 위치 정보 전송
    ws.send(JSON.stringify({ type: 'location', data: { latitude, longitude } }));
  };
  
  ws.onmessage = (event) =&amp;gt; {
    const data = JSON.parse(event.data);
    if (data.type === 'bus-arrivals') {
      setExpectedBuses(data.payload);
      ws.close();
    }
  };
  
  return () =&amp;gt; ws.close();
}, []);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장단점&lt;/b&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;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;방법 7: 서버에서 통합 엔드포인트 제공&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1746356918601&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 서버
app.post('/api/bus-tracking', async (req, res) =&amp;gt; {
  const { busNumber, latitude, longitude } = req.body;
  
  // 1. 위치 저장
  await saveUserLocation(userId, latitude, longitude);
  
  // 2. 통합 처리 후 결과 반환
  const busArrivals = await getBusArrivalsForLocation(userId, busNumber);
  
  res.json({ arrivals: busArrivals });
});&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 방법이 가장 깔끔한 이유&lt;/b&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;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;어떤 방식이 더 나을까?&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;현재 프로젝트에서의 선택 기준&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;서버 리소스가 제한적이라면&lt;/b&gt;: React Query + Dependent Queries&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실시간성이 중요하고 서버 개선이 가능하다면&lt;/b&gt;: SSE 또는 WebSocket&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가장 깔끔한 구조를 원한다면&lt;/b&gt;: 서버에서 통합 엔드포인트 제공&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;사실 이 문제의 근본적인 해결은 &lt;b&gt;백엔드 API 설계의 개선&lt;/b&gt;에 있다. 클라이언트가 순차적 API 호출을 처리하는 것보다, 서버에서 모든 처리를 완료하고 한 번에 결과를 제공하는 것이 가장 이상적이다. 이는 클라이언트 로직을 단순화하고, 네트워크 레이턴시를 줄이며, 더 나은 사용자 경험을 제공한다. ㅎ 백엔드에게 바꿔달라 요청해봐야겠다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자세한 코드는 pr 참고&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Team-GomSun/FE/pull/18&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Team-GomSun/FE/pull/18&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1746357242138&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;feat: 버스 도착 전 위치 상태 확인 추가 by ssolfa &amp;middot; Pull Request #18 &amp;middot; Team-GomSun/FE&quot; data-og-description=&quot;1️⃣ 어떤 작업을 했나요? (Summary) resolved refactor: API 요청 순서 지정&amp;nbsp;#17 위치 추적 후 버스 도착에 대한 종속 쿼리 구현 사용자 위치 정보 전송 후 이후에 주변 도착 버스 정보를 가져와야 하는데&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Team-GomSun/FE/pull/18&quot; data-og-url=&quot;https://github.com/Team-GomSun/FE/pull/18&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eOduRs/hyYM2SjWQg/rSFHn0Y8xpamkK8FzMkaT1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bMPWQa/hyYMS9ZhmY/QOkKSDNnWlPUN2A8M0u32K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Team-GomSun/FE/pull/18&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Team-GomSun/FE/pull/18&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eOduRs/hyYM2SjWQg/rSFHn0Y8xpamkK8FzMkaT1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bMPWQa/hyYMS9ZhmY/QOkKSDNnWlPUN2A8M0u32K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;feat: 버스 도착 전 위치 상태 확인 추가 by ssolfa &amp;middot; Pull Request #18 &amp;middot; Team-GomSun/FE&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1️⃣ 어떤 작업을 했나요? (Summary) resolved refactor: API 요청 순서 지정&amp;nbsp;#17 위치 추적 후 버스 도착에 대한 종속 쿼리 구현 사용자 위치 정보 전송 후 이후에 주변 도착 버스 정보를 가져와야 하는데&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&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;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-04 오후 8.10.36.png&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;814&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Og6a0/btsNKwRntW8/l0kKxXcDhftqWu3iRqje70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Og6a0/btsNKwRntW8/l0kKxXcDhftqWu3iRqje70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Og6a0/btsNKwRntW8/l0kKxXcDhftqWu3iRqje70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOg6a0%2FbtsNKwRntW8%2Fl0kKxXcDhftqWu3iRqje70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;502&quot; height=&quot;438&quot; data-filename=&quot;스크린샷 2025-05-04 오후 8.10.36.png&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;814&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;location 뒤에 버스 도착 정보인 arrival이 오는 걸 확신하는 코드가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 아무리 생각해도 이런 경우 (주기적으로 get을 하는 것 보다)는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트에서 flag를 걸어서 확인하고 이후에 순차적으로 요청을 하는 것 보다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드에서 조건이 성립했을 때 sse나 웹소켓으로 보내주는게 맞는 것 같다.&lt;/p&gt;</description>
      <author>solfa</author>
      <guid isPermaLink="true">https://5ffthewall.tistory.com/140</guid>
      <comments>https://5ffthewall.tistory.com/140#entry140comment</comments>
      <pubDate>Sun, 4 May 2025 20:13:06 +0900</pubDate>
    </item>
    <item>
      <title>Claude 사용해서 Figma로 디자인 해달라고 하면 얼마나 잘 해줄까?</title>
      <link>https://5ffthewall.tistory.com/139</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://5ffthewall.tistory.com/137&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://5ffthewall.tistory.com/137&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1745305105024&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Figma에 Cladue 연결해서 AI에게 디자인 생성해달라고 하기인데 이제 MCP를 곁들인&amp;hellip;&quot; data-og-description=&quot;오픈소스 과목에서 프로젝트 UI 디자인을 맡게 되었다.지금까지 개발자로만 이루어진 프로젝트를 하면서 항상 떠맡듯이 디자인을 맡아왔는데 이번에는 내가 자진해서 디자인을 해보고싶다고 &quot; data-og-host=&quot;5ffthewall.tistory.com&quot; data-og-source-url=&quot;https://5ffthewall.tistory.com/137&quot; data-og-url=&quot;https://5ffthewall.tistory.com/137&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/WdPmn/hyYIdFHIFb/IlGvKjnBfzkQqDjYmStkrK/img.png?width=800&amp;amp;height=529&amp;amp;face=0_0_800_529,https://scrap.kakaocdn.net/dn/OFlei/hyYH94l34O/uWWFKkFv0jqbOk0k90iU0K/img.png?width=800&amp;amp;height=529&amp;amp;face=0_0_800_529,https://scrap.kakaocdn.net/dn/Hdqqn/hyYFA2BAaZ/N1Fk0sWQOIdVxBUIdVetg1/img.png?width=2508&amp;amp;height=1248&amp;amp;face=0_0_2508_1248&quot;&gt;&lt;a href=&quot;https://5ffthewall.tistory.com/137&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://5ffthewall.tistory.com/137&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/WdPmn/hyYIdFHIFb/IlGvKjnBfzkQqDjYmStkrK/img.png?width=800&amp;amp;height=529&amp;amp;face=0_0_800_529,https://scrap.kakaocdn.net/dn/OFlei/hyYH94l34O/uWWFKkFv0jqbOk0k90iU0K/img.png?width=800&amp;amp;height=529&amp;amp;face=0_0_800_529,https://scrap.kakaocdn.net/dn/Hdqqn/hyYFA2BAaZ/N1Fk0sWQOIdVxBUIdVetg1/img.png?width=2508&amp;amp;height=1248&amp;amp;face=0_0_2508_1248');&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;Figma에 Cladue 연결해서 AI에게 디자인 생성해달라고 하기인데 이제 MCP를 곁들인&amp;hellip;&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;오픈소스 과목에서 프로젝트 UI 디자인을 맡게 되었다.지금까지 개발자로만 이루어진 프로젝트를 하면서 항상 떠맡듯이 디자인을 맡아왔는데 이번에는 내가 자진해서 디자인을 해보고싶다고&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;5ffthewall.tistory.com&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글을 참고해서 클로드로 피그마 디자인을 하는 환경을 구축해뒀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 어느정도로 쓸만 한지를 직접 보여주겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 디자인 시스템 주고 적용해서 디자인 해달라고 하기&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-22 오후 3.35.01.png&quot; data-origin-width=&quot;1562&quot; data-origin-height=&quot;1282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/P6OA5/btsNrb8KulK/9IninGF5VxkSyInQHf8S50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/P6OA5/btsNrb8KulK/9IninGF5VxkSyInQHf8S50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/P6OA5/btsNrb8KulK/9IninGF5VxkSyInQHf8S50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FP6OA5%2FbtsNrb8KulK%2F9IninGF5VxkSyInQHf8S50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;553&quot; height=&quot;454&quot; data-filename=&quot;스크린샷 2025-04-22 오후 3.35.01.png&quot; data-origin-width=&quot;1562&quot; data-origin-height=&quot;1282&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-22 오후 3.35.09.png&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;1456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doOIM0/btsNvAk6ivI/m5k4K1rHtdAJMBF3QVbCR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doOIM0/btsNvAk6ivI/m5k4K1rHtdAJMBF3QVbCR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doOIM0/btsNvAk6ivI/m5k4K1rHtdAJMBF3QVbCR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdoOIM0%2FbtsNvAk6ivI%2Fm5k4K1rHtdAJMBF3QVbCR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;553&quot; height=&quot;592&quot; data-filename=&quot;스크린샷 2025-04-22 오후 3.35.09.png&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;1456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사진과 같은 디자인 시스템이 담긴 링크를 클로드에게 주고 이 걸 활용해서 디자인을 해달라고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트는 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;(https://www.figma.com/design/aRDYrhvhDnIJLehFWvyLi3/~~ 링크 첨부)&lt;br /&gt;이걸 보고 여기에 있는 디자인 시스템을 활용해서 앱 디자인을 만들어줘! 피그마에 바로 붙여넣을 수 있게 보내주고 &lt;br /&gt;앱 주제는 행복을 기록하는 감정 기록 일기장이야 앱 형식의 디자인을 보내줘! 만들어야 하는 화면은 1. 로그인 화면 2. 감정을 기록하는 기록 화면 (일기장처럼, 일기랑 감정 정도(숫자)를 입력받아) 3. 감정을 기록한 점수를 볼 수 있는 캘린더 화면, 점수에 따라 각 날짜의 진하기가 진해져 4. 개인정보를 담는 개인 페이지 5. 기록한 일기들을 스크롤형식으로 계속 볼 수 있는 저장소 화면 이렇게 5개 만들어줘&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-22 오후 3.37.30.png&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;1398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xgMrx/btsNvdjesIh/SYkVWJ0LjakjT65hzk5jc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xgMrx/btsNvdjesIh/SYkVWJ0LjakjT65hzk5jc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xgMrx/btsNvdjesIh/SYkVWJ0LjakjT65hzk5jc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxgMrx%2FbtsNvdjesIh%2FSYkVWJ0LjakjT65hzk5jc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1296&quot; height=&quot;1398&quot; data-filename=&quot;스크린샷 2025-04-22 오후 3.37.30.png&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;1398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 시스템을 온전히 가져다 쓴다기 보다는 색상만 골라 온 느낌?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인이 없는 클로드라 디자인 요소들을 바로 가져다 쓸 수 없고 클로드 자체가 또 만들어내야 해서 그런 것 같기도 하다. 아쉽긴 하지만 주요 포인트 색상을 주고 프로토타입 정도 만드는 정도는 매우 만족할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 작업하던 디자인 주고 추가로 화면 만들어 달라고 하기&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-22 오후 3.43.38.png&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wMoBc/btsNu1433vL/8gqZwBiIi3mTgL0ckkNwN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wMoBc/btsNu1433vL/8gqZwBiIi3mTgL0ckkNwN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wMoBc/btsNu1433vL/8gqZwBiIi3mTgL0ckkNwN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwMoBc%2FbtsNu1433vL%2F8gqZwBiIi3mTgL0ckkNwN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1134&quot; height=&quot;796&quot; data-filename=&quot;스크린샷 2025-04-22 오후 3.43.38.png&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 내가 기존에 작업하던 피그마 화면이다. 여기에 추가로 화면을 만들어달라고 요청해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트는 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;https://www.figma.com/design/aRDYrhvhDnIJLehFWvyLi3/OpenS 링크링크&lt;br /&gt;여기에 다른 화면들 그려진걸 보고 비슷한 느낌으로 로그인과 회원가입 화면을 두 개 만들어줘 회원가입은 구글, 카카오 두 개 넣을거야 피그마에 바로 붙여넣을 수 있게 보내줘!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-22 오후 3.45.40.png&quot; data-origin-width=&quot;1460&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3NDFU/btsNu1YlibY/fX15wkeRLnGBDk7eKGuGsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3NDFU/btsNu1YlibY/fX15wkeRLnGBDk7eKGuGsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3NDFU/btsNu1YlibY/fX15wkeRLnGBDk7eKGuGsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3NDFU%2FbtsNu1YlibY%2FfX15wkeRLnGBDk7eKGuGsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1460&quot; height=&quot;780&quot; data-filename=&quot;스크린샷 2025-04-22 오후 3.45.40.png&quot; data-origin-width=&quot;1460&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭐 나름... 있어야 하는 폼은 다 있고 전체적인 레이아웃은 괜찮은 것 같다. 가이드 정도는 받은 느낌? 디테일은 수정하면 되니까 이 정도면 나쁘지~~ 않을 수도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 페이지를 만들어달라고 해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트는 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;추가화면을 만들어줘&lt;br /&gt;프로필 사진 업로드 옵션 - 회원가입 과정에서 프로필 사진을 설정할 수 있는 기능을 추가하면 사용자 참여도를 높일 수 있습니다. 이거 개인 페이지 화면과 이거를 만들어줘&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-22 오후 3.46.48.png&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;1220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2waiY/btsNt7qfbWE/w1ArX9nffTMZ2OKO1WCsVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2waiY/btsNt7qfbWE/w1ArX9nffTMZ2OKO1WCsVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2waiY/btsNt7qfbWE/w1ArX9nffTMZ2OKO1WCsVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2waiY%2FbtsNt7qfbWE%2Fw1ArX9nffTMZ2OKO1WCsVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;607&quot; height=&quot;604&quot; data-filename=&quot;스크린샷 2025-04-22 오후 3.46.48.png&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;1220&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭐 이정도면 만족?? 글씨체도 기존에 설정되어있던 pretandard로 써준다. 플러그인 있는 커서로 쓰면 확실히 더 괜찮겠는걸...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 아무것도 없는 화면에 만들어 달라고 하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트는 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;(pdf 첨부) -&amp;gt; 앱 방향성 담긴 프로젝트 소개/기능 문서였음&lt;br /&gt;이런 앱을 만들거야. 앱 화면을 만들어주라.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-22 오후 3.50.38.png&quot; data-origin-width=&quot;834&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhuzcy/btsNvAZJNja/M2oVZfVNUgcpDTdyfgWSz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhuzcy/btsNvAZJNja/M2oVZfVNUgcpDTdyfgWSz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhuzcy/btsNvAZJNja/M2oVZfVNUgcpDTdyfgWSz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhuzcy%2FbtsNvAZJNja%2FM2oVZfVNUgcpDTdyfgWSz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;558&quot; height=&quot;963&quot; data-filename=&quot;스크린샷 2025-04-22 오후 3.50.38.png&quot; data-origin-width=&quot;834&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가끔 가다가 &lt;b&gt;흔들어서 활성화&lt;/b&gt; 이런 건 건질 수 있다. 본격 디자인 전에 한 번쯤은 돌려보고 괜찮은 레퍼런스 있으면 가져오고 이런 식으로 큰 기대 없이 쓰면 괜찮은 정도인 것 같다. 난 커서와 플러그인 같이 쓰는 그걸 너무 해보고 싶은데 커서 결제한 사람 누구야 어디있어!!! ㅜㅜㅜ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;암튼 이정도로 사용할 수 있다는 걸 보여주고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가끔 가다 쓸 듯?&lt;/p&gt;</description>
      <category>공부 기록</category>
      <category>AI</category>
      <category>Figma</category>
      <category>MCP</category>
      <category>인공지능</category>
      <category>커서</category>
      <category>클로드</category>
      <category>클로드로디자인</category>
      <category>피그마</category>
      <category>피그마에클로드</category>
      <category>피그마에클로드연결</category>
      <author>solfa</author>
      <guid isPermaLink="true">https://5ffthewall.tistory.com/139</guid>
      <comments>https://5ffthewall.tistory.com/139#entry139comment</comments>
      <pubDate>Tue, 22 Apr 2025 15:58:01 +0900</pubDate>
    </item>
    <item>
      <title>Docker와 ngrok을 활용한 react-webcam 개발 환경 구축</title>
      <link>https://5ffthewall.tistory.com/138</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;배경 및 문제 상황&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React-Webcam 라이브러리를 사용해서 개발을 진행하던 중, 모바일 환경에서 테스트할 때 중요한 문제점을 발견했다. &lt;br /&gt;모바일 브라우저에서 카메라 접근은 보안상의 이유로 &lt;b&gt;HTTPS 프로토콜에서만&lt;/b&gt; 허용되기 때문에, 로컬 개발 환경이 기본적으로 사용하는 HTTP로는 모바일 테스트가 불가능했다. 이를 해결하기 위해 HTTPS 접속을 제공하는 ngrok을 사용하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 방법은 다음과 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;React Webcam 개발 환경 설정 가이드 - ngrok 사용 편&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;준비사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Git&lt;/li&gt;
&lt;li&gt;Node.js&lt;/li&gt;
&lt;li&gt;pnpm (설치 방법: npm install -g pnpm)&lt;/li&gt;
&lt;li&gt;ngrok 계정 (&lt;a href=&quot;https://ngrok.com&quot;&gt;https://ngrok.com&lt;/a&gt; 에서 가입)&lt;/li&gt;
&lt;/ul&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;&lt;b&gt;1. 프로젝트 클론하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744952867732&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git clone https://github.com/Team-GomSun/FE 
cd FE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 의존성 설치하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꼭 클론한 폴더 안에서 작성해주세요! (pnpm 없으면 설치해야 함)&lt;/p&gt;
&lt;pre id=&quot;code_1744952880413&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pnpm install&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 개발 서버 실행&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744952896805&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pnpm run dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행하면 다음과 같이 뜬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-18 오전 9.06.56.png&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rzFKh/btsNqHSLHW5/eDChW521132KFjZn5dYvD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rzFKh/btsNqHSLHW5/eDChW521132KFjZn5dYvD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rzFKh/btsNqHSLHW5/eDChW521132KFjZn5dYvD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrzFKh%2FbtsNqHSLHW5%2FeDChW521132KFjZn5dYvD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1052&quot; height=&quot;384&quot; data-filename=&quot;스크린샷 2025-04-18 오전 9.06.56.png&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;384&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. ngrok 설치&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744952930088&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Mac/Linux: brew install ngrok 또는 npm install -g ngrok
Windows: https://ngrok.com/download 에서 다운로드 후 설치&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-18 오전 9.07.52.png&quot; data-origin-width=&quot;1250&quot; data-origin-height=&quot;1034&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9Qmb7/btsNrgNQjDT/46vfJ7n0beAkbNts1uuVT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9Qmb7/btsNrgNQjDT/46vfJ7n0beAkbNts1uuVT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9Qmb7/btsNrgNQjDT/46vfJ7n0beAkbNts1uuVT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9Qmb7%2FbtsNrgNQjDT%2F46vfJ7n0beAkbNts1uuVT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1250&quot; height=&quot;1034&quot; data-filename=&quot;스크린샷 2025-04-18 오전 9.07.52.png&quot; data-origin-width=&quot;1250&quot; data-origin-height=&quot;1034&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. ngrok 인증 설정 (처음 한 번만)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744952958031&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ngrok config add-authtoken [YOUR_AUTH_TOKEN]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6. HTTPS 터널 실행&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744952967618&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ngrok http 3000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7. 생성된 HTTPS 주소를 사용하여 모바일에서 접속&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에서 Forwarding 된 주소!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-18 오전 9.08.27.png&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HpbOs/btsNpXPPXz8/ULw1kHXNSrShF23ZfjZem1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HpbOs/btsNpXPPXz8/ULw1kHXNSrShF23ZfjZem1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HpbOs/btsNpXPPXz8/ULw1kHXNSrShF23ZfjZem1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHpbOs%2FbtsNpXPPXz8%2FULw1kHXNSrShF23ZfjZem1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1152&quot; height=&quot;730&quot; data-filename=&quot;스크린샷 2025-04-18 오전 9.08.27.png&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;730&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-18 오후 2.09.56.png&quot; data-origin-width=&quot;1492&quot; data-origin-height=&quot;954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEUIOx/btsNqSzKiR9/QsYgm4zx8ToxrdSKxRCXEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEUIOx/btsNqSzKiR9/QsYgm4zx8ToxrdSKxRCXEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEUIOx/btsNqSzKiR9/QsYgm4zx8ToxrdSKxRCXEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEUIOx%2FbtsNqSzKiR9%2FQsYgm4zx8ToxrdSKxRCXEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1492&quot; height=&quot;954&quot; data-filename=&quot;스크린샷 2025-04-18 오후 2.09.56.png&quot; data-origin-width=&quot;1492&quot; data-origin-height=&quot;954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이렇게 접속이 가능하다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 기존 워크플로우는 다음과 같았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;터미널에서 pnpm run dev 명령어로 개발 서버 실행&lt;/li&gt;
&lt;li&gt;별도의 터미널을 열어 ngrok http 3000 명령어 실행해 HTTPS 터널 생성&lt;/li&gt;
&lt;li&gt;생성된 ngrok URL을 통해 모바일에서 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정은 번거로울 뿐 아니라, 팀원마다 환경 설정이 달라 일관된 테스트 환경을 유지하기 어려웠다. 게다가 협업을 하게 되는 팀원 분은 pnpm 설치라던지 ngrok 설치 등 번거로운 설치 과정을 겪어야 같은 개발 환경을 구축할 수 있기 때문에 이런 점을 해결하기 위해 도커를 통해 개발 환경 구성을 해보려고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;도커를 통해 개발 환경 구성하기 with ngrok&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;목표 설정&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모바일 환경에서 카메라 테스트가 가능한 HTTPS 개발 환경 구축&lt;/li&gt;
&lt;li&gt;팀원 간 일관된 개발 환경 제공&lt;/li&gt;
&lt;li&gt;설정 과정 간소화&amp;nbsp;(최소한의&amp;nbsp;명령어)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;검토한 솔루션은 세 가지가 있었다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 수동 ngrok 설정 문서화 - 위에 쓴 거!!&lt;/b&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 로컬 SSL 인증서 생성&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점: ngrok 의존성 제거, 안정적인 로컬 URL&lt;/li&gt;
&lt;li&gt;단점: 모바일 기기에 인증서 설치 필요, 복잡한 설정 - 이건 좀 아니지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Docker + ngrok 통합 환경&lt;/b&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;단점: Docker 학습 필요, 추가 시스템 자원 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;채택한 솔루션: Docker + ngrok 통합 환경&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker와 ngrok을 통합한 개발 환경을 구축하기로 결정했다. 이 방식은 환경 일관성과 설정 간소화라는 목표를 동시에 달성할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;구현 세부사항&lt;/b&gt;&lt;/h3&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;Dockerfile: Node.js 및 pnpm 기반 이미지 정의&lt;/li&gt;
&lt;li&gt;docker-compose.yml: 애플리케이션과 ngrok 서비스 구성&lt;/li&gt;
&lt;li&gt;setup.sh: 환경 변수 설정 및 Docker 실행 자동화&lt;/li&gt;
&lt;li&gt;.dockerignore: 불필요한 파일 제외로 빌드 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 코드는 밑에서 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Team-GomSun/FE/pull/6&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Team-GomSun/FE/pull/6&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1744953140232&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;feat: docker 개발 환경 구성 by ssolfa &amp;middot; Pull Request #6 &amp;middot; Team-GomSun/FE&quot; data-og-description=&quot;1️⃣ 어떤 작업을 했나요? (Summary) resolved feat: Docker 개발 환경 구성 및 HTTPS 지원 추가&amp;nbsp;#5 React-Webcam이 모바일 환경에서 카메라에 접근하기 위해서는 HTTPS 프로토콜이 필수적입니다. 따라서 Docker와&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Team-GomSun/FE/pull/6&quot; data-og-url=&quot;https://github.com/Team-GomSun/FE/pull/6&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/csdF6A/hyYIgWhlvg/ymN0b0tGj8O5Fl5Aj6vql1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/9PmhP/hyYIcM7pYy/Z2F6DkvCwLMMQmRqz8peTK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Team-GomSun/FE/pull/6&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Team-GomSun/FE/pull/6&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/csdF6A/hyYIgWhlvg/ymN0b0tGj8O5Fl5Aj6vql1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/9PmhP/hyYIcM7pYy/Z2F6DkvCwLMMQmRqz8peTK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;feat: docker 개발 환경 구성 by ssolfa &amp;middot; Pull Request #6 &amp;middot; Team-GomSun/FE&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1️⃣ 어떤 작업을 했나요? (Summary) resolved feat: Docker 개발 환경 구성 및 HTTPS 지원 추가&amp;nbsp;#5 React-Webcam이 모바일 환경에서 카메라에 접근하기 위해서는 HTTPS 프로토콜이 필수적입니다. 따라서 Docker와&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&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;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;중요한 의사결정 포인트&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 왜 Docker를 선택했는가?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원마다 Node.js 버전과 패키지 관리자 설정이 달랐고, AI 구현 담당 팀원은 패키지 매니저 설치, ngrok 설치 등 번거로운 환경 설정을 거쳐야 했다. 편한 협업을 위해 Docker를 통한 개발 환경 표준화가 필요했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 왜 git clone 과정은 도커라이징하지 않았는가?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;git 클론 과정까지 도커라이징하는 것도 가능했지만,&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이런 접근 방식 대신 '개발자가 직접 git clone 후 setup.sh 실행'하는 방식을 선택한 이유는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개발 투명성 &lt;/b&gt;: 개발자가 코드를 직접 받아 확인하는 과정이 중요했다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;점진적 도입과 학습 곡선 고려 &lt;/b&gt;: 모든 것을 한 번에 도커라이징하면 학습 장벽이 높아질 수 있다. git clone은 개발자들에게 익숙한 작업이므로, 이 부분은 유지하고 환경 설정 부분만 자동화함으로써 도입 장벽을 낮추었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개발 환경에서만 도커 필요&lt;/b&gt; : 배포 환경에서 필요한 게 아니라 모바일 테스트를 위한 개발 환경에서만 필요했기 때문에 기존 Git 기반 개발 워크플로우를 그대로 살리면서, 모바일 테스트 환경만 간소화하는 방향으로 접근했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 장점이 추가적인 자동화로 얻을 수 있는 편의성보다 더 가치 있다고 판단했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 왜 Docker Compose를 사용했는가?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 Dockerfile만 사용하는 대신 Docker Compose를 도입한 이유는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다중 서비스 관리: 개발 서버(Next.js)와 HTTPS 터널링(ngrok)이라는 두 개의 서비스를 분리해 관리할 필요가 있었다.&lt;/li&gt;
&lt;li&gt;네트워크 연결 자동화: Compose는 서비스 간 네트워크 연결을 자동 설정해주었다.&lt;/li&gt;
&lt;li&gt;단일 명령어 제어: docker-compose up/down 명령어로 전체 환경을 한 번에 제어할 수 있었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대안으로 shell 스크립트를 이용해 Docker 명령을 순차적으로 실행하는 방법도 있었지만, 선언적이고 유지보수가 용이한 Compose 방식이 더 적합하다고 판단했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;사용자 경험 개선&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 복잡한 설정 과정을:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;git clone [저장소URL]
cd [프로젝트폴더]
pnpm install
pnpm run dev
# 새 터미널 열기
ngrok http 3000
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 간소화했다:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;git clone [저장소URL]
cd [프로젝트폴더]
chmod +x setup.sh
./setup.sh YOUR_NGROK_AUTH_TOKEN
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 작성한 개발 환경 가이드는 다음과 같다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;React Webcam 개발 환경 설정 가이드 - Docker 사용 편&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm, ngrok 등의 불필요한 설치를 피하기 위한 Docker 개발 환경 설정 가이드입니다. 이 가이드는 ReactWebcam 프로젝트를 Docker와 ngrok을 사용하여 HTTPS 환경으로 설정하는 방법을 안내합니다.&lt;/p&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;Docker 설치&lt;/b&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://www.docker.com/products/docker-desktop/&quot;&gt;Docker Desktop&lt;/a&gt; 설치 (Windows/Mac)&lt;/li&gt;
&lt;li&gt;Linux의 경우 &lt;a href=&quot;https://docs.docker.com/engine/install/&quot;&gt;Docker Engine&lt;/a&gt; 설치&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ngrok 계정 생성 및 인증 토큰 획득&lt;/b&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://dashboard.ngrok.com/signup&quot;&gt;ngrok 회원가입&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;로그인 후 &lt;a href=&quot;https://dashboard.ngrok.com/get-started/your-authtoken&quot;&gt;Your Authtoken&lt;/a&gt; 페이지에서 인증 토큰 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치 및 실행 단계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 프로젝트 클론하기&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;git clone &amp;lt;https://github.com/Team-GomSun/FE&amp;gt;
cd FE
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 환경 변수 설정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;.env 파일을 만들어서&lt;/b&gt; 환경 변수를 설정해주세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-18 오전 2.08.04.png&quot; data-origin-width=&quot;1848&quot; data-origin-height=&quot;730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfsckh/btsNqnnknT9/8K6gPtexeAqDhwvsCgR7ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfsckh/btsNqnnknT9/8K6gPtexeAqDhwvsCgR7ek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfsckh/btsNqnnknT9/8K6gPtexeAqDhwvsCgR7ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcfsckh%2FbtsNqnnknT9%2F8K6gPtexeAqDhwvsCgR7ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1848&quot; height=&quot;730&quot; data-filename=&quot;스크린샷 2025-04-18 오전 2.08.04.png&quot; data-origin-width=&quot;1848&quot; data-origin-height=&quot;730&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 프로젝트 루트에 .env 파일 생성
echo &quot;NGROK_AUTH_TOKEN=YOUR_NGROK_AUTH_TOKEN&quot; &amp;gt; .env
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(YOUR_NGROK_AUTH_TOKEN 부분에 자신의 ngrok 인증 토큰을 입력하세요!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 실행 권한 부여 및 애플리케이션 시작&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에 해당 명령어를 입력해주세요.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;chmod +x setup.sh
./setup.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 확인 및 브라우저에서 접속&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크립트가 실행되면 약 10초 후에 다음과 같은 정보가 표시됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-18 오전 2.03.11.png&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czIoZX/btsNrwQt8th/mNsYp3ixhCkN5524KrzWrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czIoZX/btsNrwQt8th/mNsYp3ixhCkN5524KrzWrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czIoZX/btsNrwQt8th/mNsYp3ixhCkN5524KrzWrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczIoZX%2FbtsNrwQt8th%2FmNsYp3ixhCkN5524KrzWrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;940&quot; height=&quot;514&quot; data-filename=&quot;스크린샷 2025-04-18 오전 2.03.11.png&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표시된 ngrok URL을 브라우저에서 열거나 모바일 기기에서 접속할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;========================================
애플리케이션이 다음 URL에서 실행 중입니다:
&amp;lt;https://xxxx-xxx-xxx-xxx-xxx.ngrok-free.app&amp;gt; -&amp;gt; 여기 접속!
========================================
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 환경 정리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 작업을 마친 후 다음 명령어를 통해 환경을 정리해주세요.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker-compose down
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결론 및 교훈&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트를 통해 개발 환경 표준화의 중요성과 Docker의 유용성을 느꼈다. 써보고 싶던 도커 환경 구성을 해봐서 재밌었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 &lt;b&gt;프로덕션 환경이 아닌 개발 환경에 초점을 맞추고 있으며&lt;/b&gt;, Vercel 배포 과정에는 영향을 주지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후에는 프로덕션 환경에 대한 Docker 설정도 고려해볼 수 있을 것이다.&lt;/p&gt;</description>
      <category>React</category>
      <category>docker</category>
      <category>https</category>
      <category>ngrok</category>
      <category>개발</category>
      <category>도커</category>
      <category>보안프로토콜</category>
      <category>자동화</category>
      <category>프론트</category>
      <category>프론트엔드</category>
      <author>solfa</author>
      <guid isPermaLink="true">https://5ffthewall.tistory.com/138</guid>
      <comments>https://5ffthewall.tistory.com/138#entry138comment</comments>
      <pubDate>Fri, 18 Apr 2025 15:16:14 +0900</pubDate>
    </item>
    <item>
      <title>Figma에 Cladue 연결해서 AI에게 디자인 생성해달라고 하기인데 이제 MCP를 곁들인&amp;hellip;</title>
      <link>https://5ffthewall.tistory.com/137</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스 과목에서 프로젝트 UI 디자인을 맡게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 개발자로만 이루어진 프로젝트를 하면서 항상 떠맡듯이 디자인을 맡아왔는데 이번에는 내가 자진해서 디자인을 해보고싶다고 했다. 그동안 디자인 시스템을 개발하거나 동아리 내의 디자인 시스템인 Handy를 사용하면서 디자인에 관심이 생기기도 했고 요즘 핫하다는 MCP를 사용해서 디자인을 AI로 만들어내고 싶었기 때문에 처음으로 자진해서 디자인을 해보고싶다고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 오늘 할 건&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Figma에 claude 연결하기!!&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 MCP에 관해 간단히 설명을 해보고 넘어가겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;MCP(Model Context Protocol)란? &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP는 Model Context Protocol의 약자로, 대형 언어 모델(LLM)이 외부 애플리케이션과 상호작용할 수 있게 해주는 표준화된 프로토콜이다. AI와 다른 소프트웨어가 서로 정보를 주고받을 수 있는 통로를 만들어준다고 생각하면 된다. 진짜 이해하기 쉽게 설명해보자면 내가 claude를 구독중이라면 이걸 아무 소프트웨어랑 붙여서 결합할 수 있게 하는 것! 다양한 소프트웨어에 코파일럿을 붙이는 느낌이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/GLips/Figma-Context-MCP?tab=readme-ov-file#figma-mcp-server&quot;&gt;https://github.com/GLips/Figma-Context-MCP?tab=readme-ov-file#figma-mcp-server&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1744163159178&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - GLips/Figma-Context-MCP: MCP server to provide Figma layout information to AI coding agents like Cursor&quot; data-og-description=&quot;MCP server to provide Figma layout information to AI coding agents like Cursor - GLips/Figma-Context-MCP&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/GLips/Figma-Context-MCP?tab=readme-ov-file#figma-mcp-server&quot; data-og-url=&quot;https://github.com/GLips/Figma-Context-MCP&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/beHPZN/hyYB9Rk32V/0yoquwJLCS3lWbVQtTxKmk/img.png?width=1200&amp;amp;height=600&amp;amp;face=988_135_1040_193,https://scrap.kakaocdn.net/dn/6yyRw/hyYExjmUI0/sYj7pyfxL2iDe8H5L5m5xk/img.png?width=1200&amp;amp;height=600&amp;amp;face=988_135_1040_193&quot;&gt;&lt;a href=&quot;https://github.com/GLips/Figma-Context-MCP?tab=readme-ov-file#figma-mcp-server&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/GLips/Figma-Context-MCP?tab=readme-ov-file#figma-mcp-server&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/beHPZN/hyYB9Rk32V/0yoquwJLCS3lWbVQtTxKmk/img.png?width=1200&amp;amp;height=600&amp;amp;face=988_135_1040_193,https://scrap.kakaocdn.net/dn/6yyRw/hyYExjmUI0/sYj7pyfxL2iDe8H5L5m5xk/img.png?width=1200&amp;amp;height=600&amp;amp;face=988_135_1040_193');&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;GitHub - GLips/Figma-Context-MCP: MCP server to provide Figma layout information to AI coding agents like Cursor&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;MCP server to provide Figma layout information to AI coding agents like Cursor - GLips/Figma-Context-MCP&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만들어져있는 걸 사용하기만 하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.39.08.png&quot; data-origin-width=&quot;2508&quot; data-origin-height=&quot;1248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zLjN6/btsNdoGlVI8/Cg0SFpU3840YaSz1XNDXSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zLjN6/btsNdoGlVI8/Cg0SFpU3840YaSz1XNDXSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zLjN6/btsNdoGlVI8/Cg0SFpU3840YaSz1XNDXSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzLjN6%2FbtsNdoGlVI8%2FCg0SFpU3840YaSz1XNDXSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2508&quot; height=&quot;1248&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.39.08.png&quot; data-origin-width=&quot;2508&quot; data-origin-height=&quot;1248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/sonnylazuardi/cursor-talk-to-figma-mcp&quot;&gt;https://github.com/sonnylazuardi/cursor-talk-to-figma-mcp&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor는 이렇게 피그마에 플러그인도 있고 예제들도 많은데 Cladue는 많이 없다. 그래서 내가 해봤다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Figma와 Cladue를 연결하기 ^_^&lt;/b&gt;&lt;/h4&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;Figma 계정 (플랜은 상관 없음)&lt;/li&gt;
&lt;li&gt;Figma API 액세스 토큰&lt;/li&gt;
&lt;li&gt;Node.js (v16.0 이상)&lt;/li&gt;
&lt;li&gt;Claude 계정 (pro? 무료도 되는지는 모르겠다)&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1단계 : Figma API 토큰 받기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Figma API에 접근하기 위한 토큰이 필요하다. Figma 계정 설정에서 쉽게 발급받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1-1. Figma에 로그인하고 프로필 아이콘을 클릭한다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.41.42.png&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;996&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWx8NK/btsNdcTJ8Rb/p1KbOaoD1vNf7gfgRCiitk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWx8NK/btsNdcTJ8Rb/p1KbOaoD1vNf7gfgRCiitk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWx8NK/btsNdcTJ8Rb/p1KbOaoD1vNf7gfgRCiitk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWx8NK%2FbtsNdcTJ8Rb%2Fp1KbOaoD1vNf7gfgRCiitk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;343&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.41.42.png&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;996&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1-2. 설정(Settings) 메뉴로 이동한다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1-3. 메뉴에서 '보안(Security)' 탭을 선택한다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.41.57.png&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhQAXz/btsNdWbv003/ooy1vGAlNKFXs72431FI8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhQAXz/btsNdWbv003/ooy1vGAlNKFXs72431FI8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhQAXz/btsNdWbv003/ooy1vGAlNKFXs72431FI8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdhQAXz%2FbtsNdWbv003%2Fooy1vGAlNKFXs72431FI8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;782&quot; height=&quot;290&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.41.57.png&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;1-4. 아래로 스크롤하여 '개인 액세스 토큰(Personal access tokens)' 섹션을 찾는다. &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.42.03.png&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dPZ50E/btsNcmbsK9s/M7Wa5ZTKFnsYfukMBqQQs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dPZ50E/btsNcmbsK9s/M7Wa5ZTKFnsYfukMBqQQs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dPZ50E/btsNcmbsK9s/M7Wa5ZTKFnsYfukMBqQQs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdPZ50E%2FbtsNcmbsK9s%2FM7Wa5ZTKFnsYfukMBqQQs0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1184&quot; height=&quot;192&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.42.03.png&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;1-5 '새 토큰 생성(Create a new personal access token)'을 클릭한다. &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.42.52.png&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;1362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHv19q/btsNdy9JS84/1LijMghKs4K6DUPC6Xtfbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHv19q/btsNdy9JS84/1LijMghKs4K6DUPC6Xtfbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHv19q/btsNdy9JS84/1LijMghKs4K6DUPC6Xtfbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHv19q%2FbtsNdy9JS84%2F1LijMghKs4K6DUPC6Xtfbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;452&quot; height=&quot;644&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.42.52.png&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;1362&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;1-6. 토큰의 이름을 입력하고 '생성(Create)'을 클릭한다. -&amp;gt; 여기에서 접근 권한 (오른쪽 access는 모두 허용해둔다. write and read나 read only 모두 허용! 근데 안해도 될 것 같다. claude가 figma 캔버스에 직접 그리는 방법은 아니고 캔버스만 보고 새로운 디자인을 만들어주는거라! 지금 생각하니 read only만 해도 될 듯)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.42.57.png&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DcdK4/btsNckEEexd/W94ybjWVAu7qse0LaPj4yK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DcdK4/btsNckEEexd/W94ybjWVAu7qse0LaPj4yK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DcdK4/btsNckEEexd/W94ybjWVAu7qse0LaPj4yK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDcdK4%2FbtsNckEEexd%2FW94ybjWVAu7qse0LaPj4yK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;980&quot; height=&quot;386&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.42.57.png&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;386&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;1-7. 생성된 토큰을 안전한 곳에 복사해 둔다. (이 토큰은 한 번만 표시되므로 꼭 저장해두기!)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2단계: &lt;b&gt;IDE에 &lt;/b&gt;Figma MCP 서버 추가하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Figma와 Claude 사이의 다리 역할을 할 MCP 서버를 설정해보자. 나는 Figma-Context-MCP의 Quickstart를 참고했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.framelink.ai/docs/quickstart?utm_source=github&amp;amp;utm_medium=readme&amp;amp;utm_campaign=readme&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.framelink.ai/docs/quickstart?utm_source=github&amp;amp;utm_medium=readme&amp;amp;utm_campaign=readme&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1744163486285&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;Quickstart - Framelink Figma MCP Reference&quot; data-og-description=&quot;If you need more support to get your IDE configured, check out the official docs for your editor. You can also join our Discord if you need assistance.&quot; data-og-host=&quot;www.framelink.ai&quot; data-og-source-url=&quot;https://www.framelink.ai/docs/quickstart?utm_source=github&amp;amp;utm_medium=readme&amp;amp;utm_campaign=readme&quot; data-og-url=&quot;https://www.framelink.ai/docs/quickstart?utm_campaign=readme&amp;amp;utm_medium=readme&amp;amp;utm_source=github&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/4P5md/hyYBi2zVdP/gkgoAmjylhQyv7KwtnCTYk/img.png?width=932&amp;amp;height=748&amp;amp;face=0_0_932_748,https://scrap.kakaocdn.net/dn/3AQZB/hyYEvFUMru/OVU7KhZHMMG1u6czOuyhrk/img.png?width=1036&amp;amp;height=530&amp;amp;face=0_0_1036_530,https://scrap.kakaocdn.net/dn/xsBTt/hyYB8rpshp/PglYu14umbSJO9Bb5FZBE1/img.png?width=559&amp;amp;height=559&amp;amp;face=0_0_559_559&quot;&gt;&lt;a href=&quot;https://www.framelink.ai/docs/quickstart?utm_source=github&amp;amp;utm_medium=readme&amp;amp;utm_campaign=readme&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.framelink.ai/docs/quickstart?utm_source=github&amp;amp;utm_medium=readme&amp;amp;utm_campaign=readme&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/4P5md/hyYBi2zVdP/gkgoAmjylhQyv7KwtnCTYk/img.png?width=932&amp;amp;height=748&amp;amp;face=0_0_932_748,https://scrap.kakaocdn.net/dn/3AQZB/hyYEvFUMru/OVU7KhZHMMG1u6czOuyhrk/img.png?width=1036&amp;amp;height=530&amp;amp;face=0_0_1036_530,https://scrap.kakaocdn.net/dn/xsBTt/hyYB8rpshp/PglYu14umbSJO9Bb5FZBE1/img.png?width=559&amp;amp;height=559&amp;amp;face=0_0_559_559');&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;Quickstart - Framelink Figma MCP Reference&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;If you need more support to get your IDE configured, check out the official docs for your editor. You can also join our Discord if you need assistance.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.framelink.ai&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 IDE는 MCP 서버에 대한 JSON 설정을 지원한다. 이를 통해 Quickstart를 할 수 있게 된건데! IDE에서 MCP 구성 파일을 업데이트하면 MCP 서버가 자동으로 다운로드되고 활성화되는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2-1. Claude Desktop을 설치한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mcp 설정을 하기 위해서는 앱이 필요하다고 해서 깔았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.54.12.png&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mgNZv/btsNc7SoKfx/AAOHLflB3rrCFfWrYwYGzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mgNZv/btsNc7SoKfx/AAOHLflB3rrCFfWrYwYGzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mgNZv/btsNc7SoKfx/AAOHLflB3rrCFfWrYwYGzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmgNZv%2FbtsNc7SoKfx%2FAAOHLflB3rrCFfWrYwYGzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;410&quot; height=&quot;358&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.54.12.png&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;588&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 설치 후에 설정에 들어가면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.54.20.png&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;1106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bop1qz/btsNcUMvTcz/uhCIlE3ZLxqNecLP1gdzKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bop1qz/btsNcUMvTcz/uhCIlE3ZLxqNecLP1gdzKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bop1qz/btsNcUMvTcz/uhCIlE3ZLxqNecLP1gdzKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbop1qz%2FbtsNcUMvTcz%2FuhCIlE3ZLxqNecLP1gdzKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;656&quot; height=&quot;461&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.54.20.png&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;1106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 섹션을 선택하면 설정 편집이 있는데 이걸 누르면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.54.35.png&quot; data-origin-width=&quot;1578&quot; data-origin-height=&quot;970&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWJiHr/btsNdjLInYF/qKYJgNXwqaGZKvv9f1vw20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWJiHr/btsNdjLInYF/qKYJgNXwqaGZKvv9f1vw20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWJiHr/btsNdjLInYF/qKYJgNXwqaGZKvv9f1vw20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWJiHr%2FbtsNdjLInYF%2FqKYJgNXwqaGZKvv9f1vw20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1578&quot; height=&quot;970&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.54.35.png&quot; data-origin-width=&quot;1578&quot; data-origin-height=&quot;970&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 설정을 편집할 수 있는 claude_desktop_config.json으로 이동시켜준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 이것저것 명령어들을 넣어서 mcp를 사용하는 방식이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2-2. claude_desktop_config.json에 JSON 추가하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Framelink Figma MCP 서버를 추가하기 위해서는&lt;/p&gt;
&lt;pre id=&quot;code_1744163810114&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;mcpServers&quot;: {
    &quot;Framelink Figma MCP&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;figma-developer-mcp&quot;,
        &quot;--figma-api-key=YOUR-KEY&quot;,
        &quot;--stdio&quot;
      ]
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 붙여넣어준다. --figma-api-key에는 아까 발급받은 키를 넣어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Claude와 같은 AI 도구가 자동으로 MCP 서버를 시작하는 식으로 동작한다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npx라 깃허브 클론 받고 그럴 필요 없이 알아서 다운받고 처리해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2-3. 클로드 앱 재시작하기&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-09 오전 11.00.24.png&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9whwB/btsNcuN2EBj/MYO9BfKLRJnsG0078l7hu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9whwB/btsNcuN2EBj/MYO9BfKLRJnsG0078l7hu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9whwB/btsNcuN2EBj/MYO9BfKLRJnsG0078l7hu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9whwB%2FbtsNcuN2EBj%2FMYO9BfKLRJnsG0078l7hu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;299&quot; height=&quot;312&quot; data-filename=&quot;스크린샷 2025-04-09 오전 11.00.24.png&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;json을 변경한 후에는 꼭 앱을 종료 후 재시작해야한다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqbKDd/btsNdVjoEoV/QOYVMIqbUtccLPuMoKBKzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqbKDd/btsNdVjoEoV/QOYVMIqbUtccLPuMoKBKzK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;410&quot; data-filename=&quot;스크린샷 2025-04-09 오전 9.42.57.png&quot; style=&quot;width: 63.9082%; margin-right: 10px;&quot; data-widthpercent=&quot;64.66&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqbKDd/btsNdVjoEoV/QOYVMIqbUtccLPuMoKBKzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqbKDd%2FbtsNdVjoEoV%2FQOYVMIqbUtccLPuMoKBKzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1474&quot; height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pmeIf/btsNek34ipQ/Qcavf0kQu86TqKWy9CMJ5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pmeIf/btsNek34ipQ/Qcavf0kQu86TqKWy9CMJ5k/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;228&quot; data-filename=&quot;스크린샷 2025-04-09 오전 9.43.02.png&quot; style=&quot;width: 34.929%;&quot; data-widthpercent=&quot;35.34&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pmeIf/btsNek34ipQ/Qcavf0kQu86TqKWy9CMJ5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpmeIf%2FbtsNek34ipQ%2FQcavf0kQu86TqKWy9CMJ5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;448&quot; height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이렇게 망치표시가 뜬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-09 오전 11.02.27.png&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LJjVI/btsNc6eWELe/SHwCF8C26fROCLH5vKEESK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LJjVI/btsNc6eWELe/SHwCF8C26fROCLH5vKEESK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LJjVI/btsNc6eWELe/SHwCF8C26fROCLH5vKEESK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLJjVI%2FbtsNc6eWELe%2FSHwCF8C26fROCLH5vKEESK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;619&quot; height=&quot;193&quot; data-filename=&quot;스크린샷 2025-04-09 오전 11.02.27.png&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 설정에 running이 뜨면 잘 돌아가고 있는 것이고&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-09 오전 9.43.11.png&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;922&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7jPXD/btsNcF9ZpqP/Z0kiwgZjf5GksRMvWpYia1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7jPXD/btsNcF9ZpqP/Z0kiwgZjf5GksRMvWpYia1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7jPXD/btsNcF9ZpqP/Z0kiwgZjf5GksRMvWpYia1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7jPXD%2FbtsNcF9ZpqP%2FZ0kiwgZjf5GksRMvWpYia1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;524&quot; height=&quot;405&quot; data-filename=&quot;스크린샷 2025-04-09 오전 9.43.11.png&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;922&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 망치 아이콘을 클릭하거나 커넥트 아이콘을 클릭해서 내가 추가한 mcp들을 확인할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3단계: Claude에게 디자인 생성 요청하기 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Claude에게 Figma 디자인을 생성해달라고 요청해보자. 명령 전에 Claude에게 피그마 디자인을 링크를 통해 알려줘야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.59.04.png&quot; data-origin-width=&quot;1670&quot; data-origin-height=&quot;1382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Em9CD/btsNeqpBww0/hOrKke78cJ405FCRxhYR71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Em9CD/btsNeqpBww0/hOrKke78cJ405FCRxhYR71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Em9CD/btsNeqpBww0/hOrKke78cJ405FCRxhYR71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEm9CD%2FbtsNeqpBww0%2FhOrKke78cJ405FCRxhYR71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;646&quot; height=&quot;535&quot; data-filename=&quot;스크린샷 2025-04-09 오전 10.59.04.png&quot; data-origin-width=&quot;1670&quot; data-origin-height=&quot;1382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 내가 원하는 부분의 링크를 Copy link to selection을 통해 복사한 후에 이걸 가지고 명령어를 입력하면 된다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3-1. Claude를 열고 새 대화를 시작한다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-09 오전 11.03.29.png&quot; data-origin-width=&quot;1048&quot; data-origin-height=&quot;282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmOHgh/btsNdmhv8TN/w9Mwey8DJVhBTRPEPUKz50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmOHgh/btsNdmhv8TN/w9Mwey8DJVhBTRPEPUKz50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmOHgh/btsNdmhv8TN/w9Mwey8DJVhBTRPEPUKz50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmOHgh%2FbtsNdmhv8TN%2Fw9Mwey8DJVhBTRPEPUKz50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;652&quot; height=&quot;175&quot; data-filename=&quot;스크린샷 2025-04-09 오전 11.03.29.png&quot; data-origin-width=&quot;1048&quot; data-origin-height=&quot;282&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 링크와 함께 명령을 작성하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-09 오전 11.04.00.png&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;714&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cX8y2q/btsNesVgxBK/tvAqfERBKkXOXnxjWgcgZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cX8y2q/btsNesVgxBK/tvAqfERBKkXOXnxjWgcgZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cX8y2q/btsNesVgxBK/tvAqfERBKkXOXnxjWgcgZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcX8y2q%2FbtsNesVgxBK%2FtvAqfERBKkXOXnxjWgcgZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;423&quot; height=&quot;299&quot; data-filename=&quot;스크린샷 2025-04-09 오전 11.04.00.png&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;714&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 22달러짜리 똑똑한 나의 사랑스러운 클로드가 피그마 파일을 분석해준다. 내가 보내준 링크에는 흰 바탕의 기본 프레임만 존재했다. 이걸 알고 알아서 피그마가 모바일 앱 화면을 뚝딱뚝딱 만들어줬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-09 오전 11.05.21.png&quot; data-origin-width=&quot;2014&quot; data-origin-height=&quot;1332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NZcwS/btsNdX9lGXg/KPZGG7ssZFMTLsFdmJnfak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NZcwS/btsNdX9lGXg/KPZGG7ssZFMTLsFdmJnfak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NZcwS/btsNdX9lGXg/KPZGG7ssZFMTLsFdmJnfak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNZcwS%2FbtsNdX9lGXg%2FKPZGG7ssZFMTLsFdmJnfak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;594&quot; height=&quot;393&quot; data-filename=&quot;스크린샷 2025-04-09 오전 11.05.21.png&quot; data-origin-width=&quot;2014&quot; data-origin-height=&quot;1332&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3-2. Figma에 코드를 복붙한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제공해준 SVG 코드 전체를 복사해서 피그마에 붙여넣기를 하면 피그마가 자동으로 SVG를 가져오고 편집 가능한 요소로 변환해준다!&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-09 오전 11.06.46.png&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;1480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIriDK/btsNcitefsC/WhzWvgQycVfylkUpPGkVmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIriDK/btsNcitefsC/WhzWvgQycVfylkUpPGkVmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIriDK/btsNcitefsC/WhzWvgQycVfylkUpPGkVmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIriDK%2FbtsNcitefsC%2FWhzWvgQycVfylkUpPGkVmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;541&quot; height=&quot;465&quot; data-filename=&quot;스크린샷 2025-04-09 오전 11.06.46.png&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;1480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각보다 성능이 괜찮다는 걸 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;디자인 작업 시 팁&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;디자인 작업 뿐만 아니라 프롬프팅을 해본 사람이라면 모두 공감할 것인데 ㅎ 0부터 10까지를 전부 만들어달라고 하기보다는&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 이미 제작되어 있는 디자인의 다른 버전을 만들어 달라고 하자. - 이 디자인을 다크모드로 만들어줘. 포인트 색상은 #~~ 이걸로 수정해주고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 레퍼런스를 먹이자. - &lt;b&gt;이건 아무것도 없는 상태에서 디자인을 만들어달라고 하기보다는 레퍼런스를 모아두고 모아둔 링크를 줘서 이런 스타일로 만들어달라고 하면 더 효과적일 것 같다. &lt;/b&gt;+ 참고 이미지나 스타일 가이드를 제공하자 - &quot;Material Design 스타일로&quot; 또는 &quot;Apple의 Human Interface Guidelines를 따라&quot; 같은 표현을 사용하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. MCP를 중첩으로 사용하자. - 22달러 뽕을 뽑아야 한다!!! 디자인 시스템이 존재한다면 그걸 mcp를 통해 먹이든 뭘 먹이든 먹여서 피그마에 만들어달라 하자!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 서버 설정이 조금 까다로웠지만, 한 번 설정을 마치고 나니 너무 재밌었고 여러 MCP들도 다양하게 사용해보고 싶어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/punkpeye/awesome-mcp-servers&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/punkpeye/awesome-mcp-servers&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1744164978971&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - punkpeye/awesome-mcp-servers: A collection of MCP servers.&quot; data-og-description=&quot;A collection of MCP servers. Contribute to punkpeye/awesome-mcp-servers development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/punkpeye/awesome-mcp-servers&quot; data-og-url=&quot;https://github.com/punkpeye/awesome-mcp-servers&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/yJuqL/hyYBfdGiDH/j9BE3DKVoIOp3kpMdBi7sK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/edqUDb/hyYEANZMnt/7zdPkoKoj9FbpZ92zqn8u0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/punkpeye/awesome-mcp-servers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/punkpeye/awesome-mcp-servers&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/yJuqL/hyYBfdGiDH/j9BE3DKVoIOp3kpMdBi7sK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/edqUDb/hyYEANZMnt/7zdPkoKoj9FbpZ92zqn8u0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - punkpeye/awesome-mcp-servers: A collection of MCP servers.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A collection of MCP servers. Contribute to punkpeye/awesome-mcp-servers development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 어썸한 mcp 들을 모아두는 깃허브도 존재하니 심심할 때 마다 자주 찾아봐야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 모든 디자인 작업을 대체할 수는 없지만, 초기 아이디어 구상이나 프로토타입 제작을 훨씬 빠르게 할 수 있도록 도와준다는 점에서 그리고 개발자로만 이루어진 프로젝트를 하는 입장에서 이런 도구는 너무나 감사하고... 더 써보고 후기로 찾아오겠다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;a href=&quot;https://ohouse.ai/ai-interior&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ohouse.ai/ai-interior&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1760663464913&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;Ohouse AI - AI Room Planner | Web Version&quot; data-og-description=&quot;Upload a room photo and let Ohouse AI instantly transform it into stunning interior styles. Try AI-powered design for free in your browser!&quot; data-og-host=&quot;ohouse.ai&quot; data-og-source-url=&quot;https://ohouse.ai/ai-interior&quot; data-og-url=&quot;https://ohouse.ai/ai-interior/en-US&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dOdm5I/hyZLzhyYWL/1QL9RxKP6TC9pZF1HYErhK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/LmQnQ/hyZLsQiqwf/8qcutanOgmEsExk23RRUqk/img.png?width=144&amp;amp;height=144&amp;amp;face=0_0_144_144&quot;&gt;&lt;a href=&quot;https://ohouse.ai/ai-interior&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ohouse.ai/ai-interior&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dOdm5I/hyZLzhyYWL/1QL9RxKP6TC9pZF1HYErhK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/LmQnQ/hyZLsQiqwf/8qcutanOgmEsExk23RRUqk/img.png?width=144&amp;amp;height=144&amp;amp;face=0_0_144_144');&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;Ohouse AI - AI Room Planner | Web Version&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Upload a room photo and let Ohouse AI instantly transform it into stunning interior styles. Try AI-powered design for free in your browser!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ohouse.ai&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>공부 기록</category>
      <category>AI</category>
      <category>cursor</category>
      <category>MCP</category>
      <category>디자인</category>
      <category>디자인시스템</category>
      <category>인공지능</category>
      <category>자동화</category>
      <category>컴포넌트</category>
      <category>프론트</category>
      <category>피그마</category>
      <author>solfa</author>
      <guid isPermaLink="true">https://5ffthewall.tistory.com/137</guid>
      <comments>https://5ffthewall.tistory.com/137#entry137comment</comments>
      <pubDate>Wed, 9 Apr 2025 11:18:44 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js] Parallel Routes를 활용한 모달 상태 관리 하기 - URL 기반상태관리 / useSearchParams 와 Suspense 같이 사용하기</title>
      <link>https://5ffthewall.tistory.com/136</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;모달(Modal)은 사용자에게 중요한 정보를 표시하거나 작업을 완료하도록 유도하는 UI 요소로 널리 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 모달의 상태 관리는 생각보다 복잡할 수 있다. 보통 모달의 열림/닫힘 상태를 관리하기 위해 보통 boolean 값을 사용하는 방식을 많이 채택한다. 최근에는 URL을 기반으로 모달 상태를 관리하는 방법이 떠오르고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Next.js의 App Router에서 제공하는 Parallel Routes 기능을 활용하여 모달 상태 관리를 URL 기반으로 리팩토링한 경험을 공유하려 한다. 단순히 boolean 값으로 관리하던 모달 상태를 왜 URL 기반으로 바꾸었는지, 그리고 이러한 접근 방식이 어떤 이점을 가져다주는지 함께 살펴보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Parallel Routes란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js의 App Router에서 도입된 Parallel Routes는 동일한 레이아웃 내에서 여러 페이지를 동시에 렌더링할 수 있게 해주는 기능이다. 이 기능은 @폴더명 형식의 특수 폴더 구조를 통해 구현된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/routing/parallel-routes&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://nextjs.org/docs/app/building-your-application/routing/parallel-routes&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1742282123649&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;Routing: Parallel Routes | Next.js&quot; data-og-description=&quot;Simultaneously render one or more pages in the same view that can be navigated independently. A pattern for highly dynamic applications.&quot; data-og-host=&quot;nextjs.org&quot; data-og-source-url=&quot;https://nextjs.org/docs/app/building-your-application/routing/parallel-routes&quot; data-og-url=&quot;https://nextjs.org/docs/app/building-your-application/routing/parallel-routes&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/mglug/hyYurjAO61/XF4K8lYNMz5HVJXMIqkDO0/img.png?width=843&amp;amp;height=441&amp;amp;face=0_0_843_441,https://scrap.kakaocdn.net/dn/gm6tC/hyYqREiCcQ/cwB3VQ5z1jjDzGB07iEzC1/img.png?width=843&amp;amp;height=441&amp;amp;face=0_0_843_441,https://scrap.kakaocdn.net/dn/3FQzK/hyYqTPCrrk/VlbYH9sEk5MgxdWtpiP77k/img.png?width=1600&amp;amp;height=942&amp;amp;face=0_0_1600_942&quot;&gt;&lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/routing/parallel-routes&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nextjs.org/docs/app/building-your-application/routing/parallel-routes&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/mglug/hyYurjAO61/XF4K8lYNMz5HVJXMIqkDO0/img.png?width=843&amp;amp;height=441&amp;amp;face=0_0_843_441,https://scrap.kakaocdn.net/dn/gm6tC/hyYqREiCcQ/cwB3VQ5z1jjDzGB07iEzC1/img.png?width=843&amp;amp;height=441&amp;amp;face=0_0_843_441,https://scrap.kakaocdn.net/dn/3FQzK/hyYqTPCrrk/VlbYH9sEk5MgxdWtpiP77k/img.png?width=1600&amp;amp;height=942&amp;amp;face=0_0_1600_942');&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;Routing: Parallel Routes | Next.js&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Simultaneously render one or more pages in the same view that can be navigated independently. A pattern for highly dynamic applications.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nextjs.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;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;app/
├── layout.tsx
├── page.tsx
└── @modal
    ├── default.tsx
    └── page.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 구조에서 layout.tsx는 다음과 같이 작성할 수 있다&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    &amp;lt;html&amp;gt;
      &amp;lt;body&amp;gt;
        {children}
        {modal}
      &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;modal 슬롯은 @modal 폴더의 컨텐츠를 렌더링하며, 이를 통해 메인 컨텐츠(children)와 모달을 동시에 표시할 수 있다.&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;boolean 상태에서 URL 기반 상태 관리로의 전환&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;기존 방식: boolean 상태를 사용한 모달 관리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 React에서 모달을 관리할 때는 다음과 같이 boolean 상태를 사용한다:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const [isModalOpen, setIsModalOpen] = useState(false);

// 모달 열기
const openModal = () =&amp;gt; setIsModalOpen(true);

// 모달 닫기
const closeModal = () =&amp;gt; setIsModalOpen(false);

return (
  &amp;lt;&amp;gt;
    &amp;lt;button onClick={openModal}&amp;gt;모달 열기&amp;lt;/button&amp;gt;
    {isModalOpen &amp;amp;&amp;amp; &amp;lt;Modal onClose={closeModal} /&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;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;: 모달이 열린 상태에서 뒤로 가기 버튼을 누르면 이전 페이지로 이동하며, 모달 상태는 URL에 반영되지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태 공유의 어려움&lt;/b&gt;: 여러 컴포넌트 간에 모달 상태를 공유하려면 상태 관리 라이브러리나 Context API를 사용해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;직접 링크의 어려움&lt;/b&gt;: 특정 모달이 열린 상태의 페이지로 직접 링크를 제공하기 어렵다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론 : 복잡하고 모달 열린 상태로 공유가 가능하게 하고싶음 -&amp;gt; URL 기반으로 바꾸자!&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;URL 기반 모달 상태 관리&amp;nbsp;&lt;/b&gt;&lt;b&gt;구현 방법&lt;/b&gt;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 폴더 구조 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, Parallel Routes를 위한 폴더 구조를 다음과 같이 설정했다:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;src/app/
├── @modal
│   ├── default.tsx  // 모달이 없을 때 표시할 컴포넌트
│   └── page.tsx     // 모달 컴포넌트
├── layout.tsx       // 루트 레이아웃
└── page.tsx         // 메인 페이지
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 기본 컴포넌트 설정 (default.tsx)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모달이 표시되지 않을 때는 default.tsx가 렌더링된다:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/app/@modal/default.tsx
export default function Default() {
  return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 모달 컴포넌트 구현 (page.tsx)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모달 컴포넌트는 URL 쿼리 파라미터를 확인하여 표시 여부를 결정한다:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/app/@modal/page.tsx
'use client';
import { useRouter, useSearchParams } from 'next/navigation';

export default function Modal() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const isVisible = searchParams.get('duplicate') === 'true';
  
  if (!isVisible) return null;
  
  return (
    &amp;lt;div
      className=&quot;fixed inset-0 flex items-center justify-center bg-gray-500 bg-opacity-50&quot;
      onClick={() =&amp;gt; router.back()}
    &amp;gt;
      &amp;lt;div
        className=&quot;w-[90%] max-w-md rounded-xl bg-white p-6 text-center font-pretendard font-medium text-black shadow-lg&quot;
        onClick={(e) =&amp;gt; e.stopPropagation()}
      &amp;gt;
        &amp;lt;p className=&quot;text-4xl&quot;&amp;gt; &amp;lt;/p&amp;gt;
        &amp;lt;p className=&quot;mt-4 text-lg md:text-2xl&quot;&amp;gt;이미 등록된 전화번호입니다&amp;lt;/p&amp;gt;
        &amp;lt;p className=&quot;text-lg md:text-2xl&quot;&amp;gt;다른 번호로 시도해주세요!&amp;lt;/p&amp;gt;
        &amp;lt;button
          onClick={() =&amp;gt; router.back()}
          className=&quot;mt-6 w-[210px] rounded-full bg-[#6B5CFF] py-3 text-lg font-semibold text-white hover:bg-[#5A52EE] md:w-full&quot;
        &amp;gt;
          확인했어요
        &amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 레이아웃 설정 (layout.tsx)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트 레이아웃에서는 children과 modal 슬롯을 모두 렌더링한다:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/app/layout.tsx
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  console.log('modal slot exists:', !!modal);
  return (
    &amp;lt;html lang=&quot;ko&quot;&amp;gt;
      &amp;lt;body&amp;gt;
        {children}
        {modal}
      &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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 첫 메인에서 모달을 사용했기 때문에 RootLayout에 modal을 추가해줬다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. 메인 페이지에서 모달 열기 (page.tsx)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 모달을 열기 위해서는 단순히 URL을 변경하면 된다:&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;// src/app/page.tsx (일부)
const handleSubmit = async (e: FormEvent) =&amp;gt; {
  e.preventDefault();
  // ...
  
  try {
    const { error } = await supabase.from('users').insert([{ phone_number: phoneNumber }]);
    if (error) {
      if (error.code === '23505') {
        // 기존: setIsModalOpen(true);
        // 변경: URL로 상태 관리
        router.push('/?duplicate=true');
        return;
      }
      throw error;
    }
    // ...
  } catch (error) {
    // ...
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 setIsModalOpen(true)를 통해 상태를 변경했지만, 이제는 router.push('/?duplicate=true')를 통해 URL을 변경한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-03-18 오후 4.12.46.png&quot; data-origin-width=&quot;3012&quot; data-origin-height=&quot;1804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cd7pGK/btsMOtt9yWC/Dt7e1PCnrXgvdrXReE7xJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cd7pGK/btsMOtt9yWC/Dt7e1PCnrXgvdrXReE7xJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cd7pGK/btsMOtt9yWC/Dt7e1PCnrXgvdrXReE7xJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcd7pGK%2FbtsMOtt9yWC%2FDt7e1PCnrXgvdrXReE7xJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3012&quot; height=&quot;1804&quot; data-filename=&quot;스크린샷 2025-03-18 오후 4.12.46.png&quot; data-origin-width=&quot;3012&quot; data-origin-height=&quot;1804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모달이 잘 뜨는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ppussung-tarot.yourssu.com/?duplicate=true&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ppussung-tarot.yourssu.com/?duplicate=true&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 링크로 들어가면 바로 모달 창으로 접근하는 것도 볼 수 있다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 코드는 여기에서 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/yourssu/ppusyung-tarot/commit/11f6f60544090d45aa067b30c3771f8a310773e6&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/yourssu/ppusyung-tarot/commit/11f6f60544090d45aa067b30c3771f8a310773e6&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;단일 모달 컴포넌트에서 다중 상태 처리하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복수의 모달 타입을 하나의 컴포넌트에서 처리하는 방법이다. 모달 안의 컨텐츠만 달라져야 했었는데 이런 단일 모달 컴포넌트의 다중 상태를 어떻게 처리하는지 실제 프로젝트에서 활용한 코드를 살펴보자:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/app/@modal/page.tsx
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import { Suspense } from 'react';

function ModalContent() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const isDuplicate = searchParams.get('duplicate') === 'true';
  const isResourceExhausted = searchParams.get('resourceExhausted') === 'true';

  if (!isDuplicate &amp;amp;&amp;amp; !isResourceExhausted) return null;

  return (
    &amp;lt;div
      className=&quot;fixed inset-0 flex items-center justify-center bg-gray-500 bg-opacity-50&quot;
      onClick={() =&amp;gt; router.back()}
    &amp;gt;
      &amp;lt;div
        className=&quot;w-[90%] max-w-md rounded-xl bg-white p-6 text-center font-pretendard font-medium text-black shadow-lg&quot;
        onClick={(e) =&amp;gt; e.stopPropagation()}
      &amp;gt;
        {isDuplicate &amp;amp;&amp;amp; (
          &amp;lt;&amp;gt;
            &amp;lt;p className=&quot;text-4xl&quot;&amp;gt; &amp;lt;/p&amp;gt;
            &amp;lt;p className=&quot;mt-4 text-lg md:text-2xl&quot;&amp;gt;이미 등록된 전화번호입니다&amp;lt;/p&amp;gt;
            &amp;lt;p className=&quot;text-lg md:text-2xl&quot;&amp;gt;다른 번호로 시도해주세요!&amp;lt;/p&amp;gt;
          &amp;lt;/&amp;gt;
        )}

        {isResourceExhausted &amp;amp;&amp;amp; (
          &amp;lt;&amp;gt;
            &amp;lt;p className=&quot;text-2xl&quot;&amp;gt; &amp;lt;/p&amp;gt;
            &amp;lt;p className=&quot;mt-4 text-base md:text-xl&quot;&amp;gt;서비스 자원이 모두 소진되었습니다.&amp;lt;/p&amp;gt;
            &amp;lt;p className=&quot;text-base md:text-xl&quot;&amp;gt;뿌슝타로에 많은 관심 보내주셔서 감사합니다!&amp;lt;/p&amp;gt;
          &amp;lt;/&amp;gt;
        )}

        &amp;lt;button
          onClick={() =&amp;gt; router.back()}
          className=&quot;mt-6 w-[210px] rounded-full bg-[#6B5CFF] py-3 text-lg font-semibold text-white hover:bg-[#5A52EE] md:w-full&quot;
        &amp;gt;
          확인했어요
        &amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default function Modal() {
  return (
    &amp;lt;Suspense&amp;gt;
      &amp;lt;ModalContent /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&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;/li&gt;
&lt;li&gt;&lt;b&gt;Suspense 활용&lt;/b&gt;: Suspense로 모달 콘텐츠를 감싸 클라이언트 컴포넌트 초기화 과정에서의 지연을 처리한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공통 UI 재사용&lt;/b&gt;: 모달의 기본 구조(배경, 컨테이너, 닫기 버튼 등)를 재사용하면서 내용만 조건부로 변경한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-03-18 오후 4.18.58.png&quot; data-origin-width=&quot;3012&quot; data-origin-height=&quot;1812&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SW9aN/btsMOuNpqFU/hY7H3mcJN9jfJqnLuHp5P0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SW9aN/btsMOuNpqFU/hY7H3mcJN9jfJqnLuHp5P0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SW9aN/btsMOuNpqFU/hY7H3mcJN9jfJqnLuHp5P0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSW9aN%2FbtsMOuNpqFU%2FhY7H3mcJN9jfJqnLuHp5P0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3012&quot; height=&quot;1812&quot; data-filename=&quot;스크린샷 2025-03-18 오후 4.18.58.png&quot; data-origin-width=&quot;3012&quot; data-origin-height=&quot;1812&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이번에는 ?resourceExhausted=true로 모달이 잘 생기는 것을 확인할 수 있다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식으로 모달을 열 때는 다음과 같이 다양한 모달을 쉽게 활성화할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1742282638887&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 중복 전화번호 모달 열기
router.push('/?duplicate=true');

// 자원 소진 모달 열기
router.push('/?resourceExhausted=true');&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Suspense가 필요한 이유: 빌드 시 useSearchParams 오류 해결&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모달 컴포넌트를 개발하면서 useSearchParams를 사용하다가 에러가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 환경에서는 괜찮았는데 빌드할 때 다음과 같은 오류가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 Next.js에서 클라이언트 컴포넌트에서 useSearchParams를 사용할 때 발생하는 특정 문제 때문이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Error: useSearchParams() should be wrapped in a suspense boundary at the same level as the component that uses it.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 오류는 Next.js 13 이상에서 App Router를 사용할 때, useSearchParams 훅이 서스펜스(Suspense) 경계 내에서 사용되어야 한다는 것을 알려준다.&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;: Next.js는 서버에서 초기 렌더링을 수행한 후 클라이언트에서 하이드레이션을 수행한다. 이 과정에서 useSearchParams는 클라이언트에서만 사용 가능한 정보에 의존한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스트리밍 렌더링 지원&lt;/b&gt;: Next.js 13부터는 스트리밍 렌더링을 지원하는데, Suspense를 사용하면 이 기능을 최대한 활용할 수 있다. useSearchParams가 준비되기 전에도 나머지 UI를 렌더링할 수 있게 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클라이언트-서버 불일치 방지&lt;/b&gt;: Suspense는 클라이언트와 서버 간의 렌더링 불일치를 방지하는 데 도움을 준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1742283245496&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function Modal() {
  return (
    &amp;lt;Suspense&amp;gt; // suspense 추가
      &amp;lt;ModalContent /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;suspense를 추가해줘서 에러를 해결했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;'use client'와 Suspense의 차이&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의문이 생겼다. 'use client' 지시문을 추가했는데도 왜 Suspense가 필요한지에 대한 것이다.&amp;nbsp;&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;b&gt;'use client' 지시문&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;이는 해당 컴포넌트와 그 하위 컴포넌트들이 클라이언트 측에서 실행됨을 Next.js에&lt;/span&gt;&amp;nbsp;알려준다.&lt;/li&gt;
&lt;li&gt;클라이언트 컴포넌트라고 표시해도!! Next.js는 여전히 서버에서 이 컴포넌트의 초기 HTML을 생성한 다음, 클라이언트에서 하이드레이션한다.&lt;/li&gt;
&lt;li&gt;이걸 써도 Next.js는 여전히 서버에서 &lt;b&gt;초기 HTML을 생성하려고 시도&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;'use client'는 단순히 &quot;이 컴포넌트는 클라이언트에서 상호작용이 필요하다&quot;라고 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;알려주는&lt;/span&gt;&amp;nbsp;것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Suspense boundary&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Suspense는 비동기 작업이 진행되는 동안 로딩 상태를 처리할 수 있게 해주는 React 기능이다.&lt;/li&gt;
&lt;li&gt;Next.js App Router에서 useSearchParams()는 서버에서는 사용할 수 없고 클라이언트 하이드레이션이 완료된 후에만 올바른 값을 반환한다.&lt;/li&gt;
&lt;li&gt;Suspense가 없으면, Next.js는 전체 페이지를 클라이언트 측에서만 렌더링하도록 &quot;강제로 전환&quot;한다. =&amp;gt; 이는 클라이언트 측 자바스크립트가 로드될 때까지 페이지가 비어 있을 수 있기 때문에 오류가 났다고 하는 것!&lt;/li&gt;
&lt;li&gt;Suspense를 사용하면 &quot;이 특정 부분만 클라이언트에서 채워질 때까지 기다리고, 나머지 페이지는 정상적으로 서버에서 렌더링하라&quot;고 알려준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;useSearchParams()와 Suspense의 관계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useSearchParams()는 특별한 훅이다. 이 훅은 브라우저의 URL에서만 정보를 가져올 수 있기 때문에 서버에서는 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; useEffect와 다름!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Suspense 없이 useSearchParams()를 사용한다면?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 컴포넌트는 서버에서 렌더링할 수 없는 클라이언트 데이터(URL 파라미터)에 의존하는데?&lt;/li&gt;
&lt;li&gt;그렇다면 어떻게 해야 하지? 일단 서버 렌더링은 포기하고 전체를 클라이언트로 미루자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Suspense를 사용한다면?&lt;/b&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정리하자면&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;'use client'&lt;/b&gt;&amp;nbsp;= &quot;이 코드는 클라이언트 기능이 필요해&quot; (클라이언트 컴포넌트로 표시)&lt;br /&gt;&lt;b&gt;Suspense&lt;/b&gt;&amp;nbsp;= &quot;이 부분은 서버에서 즉시 렌더링할 수 없으니, 나중에 클라이언트에서 채워줘&quot; (렌더링 전략 제어)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;use client와 suspense를 둘 다 쓰는 이유도 이것 때문이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;'use client'로 이벤트 핸들러, hooks 등을 사용 가능하게 하고&lt;/li&gt;
&lt;li&gt;Suspense로 useSearchParams()가 클라이언트에서만 실행되도록 하면서도 페이지의 나머지 부분은 서버 렌더링의 이점을 누릴 수 있게 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nextjs.org/docs/app/api-reference/functions/use-search-params&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://nextjs.org/docs/app/api-reference/functions/use-search-params&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1742283487243&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;Functions: useSearchParams | Next.js&quot; data-og-description=&quot;API Reference for the useSearchParams hook.&quot; data-og-host=&quot;nextjs.org&quot; data-og-source-url=&quot;https://nextjs.org/docs/app/api-reference/functions/use-search-params&quot; data-og-url=&quot;https://nextjs.org/docs/app/api-reference/functions/use-search-params&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gKB4l/hyYrSJg54v/3epEeqkkYbu5nLQuAq95C0/img.png?width=843&amp;amp;height=441&amp;amp;face=0_0_843_441,https://scrap.kakaocdn.net/dn/blWXRk/hyYqTWmDZT/B6rKxJLagFFm8psNB2G9n1/img.png?width=843&amp;amp;height=441&amp;amp;face=0_0_843_441&quot;&gt;&lt;a href=&quot;https://nextjs.org/docs/app/api-reference/functions/use-search-params&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nextjs.org/docs/app/api-reference/functions/use-search-params&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gKB4l/hyYrSJg54v/3epEeqkkYbu5nLQuAq95C0/img.png?width=843&amp;amp;height=441&amp;amp;face=0_0_843_441,https://scrap.kakaocdn.net/dn/blWXRk/hyYqTWmDZT/B6rKxJLagFFm8psNB2G9n1/img.png?width=843&amp;amp;height=441&amp;amp;face=0_0_843_441');&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;Functions: useSearchParams | Next.js&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;API Reference for the useSearchParams hook.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nextjs.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;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js 문서에는 &lt;b&gt;useSearchParams&lt;/b&gt;, &lt;b&gt;usePathname&lt;/b&gt;, &lt;b&gt;useRouter&lt;/b&gt;와 같은 특정 네비게이션 관련 훅들이 &lt;b&gt;Suspense&lt;/b&gt;와 함께 사용되어야 한다고 명시되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, useState, useEffect 등 일반적인 React 훅들은 단순히 'use client' 지시문만 있으면 사용 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반 React 훅(useState, useEffect 등) = 'use client'만 필요&lt;/li&gt;
&lt;li&gt;특정 Next.js 네비게이션 훅(useSearchParams 등) = 'use client' + Suspense가 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 생각하면 될 것 같다!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <category>Next</category>
      <category>react</category>
      <category>모달</category>
      <category>병렬라우트</category>
      <category>상태관리</category>
      <category>프론트</category>
      <author>solfa</author>
      <guid isPermaLink="true">https://5ffthewall.tistory.com/136</guid>
      <comments>https://5ffthewall.tistory.com/136#entry136comment</comments>
      <pubDate>Tue, 18 Mar 2025 16:39:49 +0900</pubDate>
    </item>
    <item>
      <title>Next.js 프로젝트를 Vercel로 배포 자동화 + Route 53으로 도메인 변경</title>
      <link>https://5ffthewall.tistory.com/135</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. Vercel 배포를 선택한 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동아리 내에서 팀 organization을 사용하고 있다. 원래는 AWS S3 버킷을 생성하고 배포하는 방식으로 진행하려 했다. 하지만 이번 프로젝트는 &lt;b&gt;Next.js&lt;/b&gt;를 사용하고 있으며, 특히 &lt;b&gt;API Route&lt;/b&gt; 기능을 활용하고 있다. 이러한 특성상 단순한 정적 빌드가 어렵다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js의 SSR(Server Side Rendering) 및 API Route를 포함한 프로젝트는 기본적으로 서버 환경이 필요하다. AWS에서 이를 운영하려면 Lambda나 EC2 같은 추가적인 설정이 필요하지만, 이는 유지보수 비용과 복잡도를 증가시킨다. 하는 방법이 없진 않지만!! 빠른 배포가 우선이었기에 빠르고 간편한 Vercel 배포를 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel은 &lt;b&gt;Next.js의 공식 배포 플랫폼&lt;/b&gt;으로, Next.js의 모든 기능을 손쉽게 지원한다. 추가적인 설정 없이도 SSR과 API Route를 지원하고, 배포 속도가 빠르며 무료 플랜에서도 기본적인 기능을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유로 팀 레포에서 직접 배포하는 대신, &lt;b&gt;내 개인 레포로 포크한 후 Vercel에서 배포&lt;/b&gt;하는 방식으로 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐면... 팀 레포에서 버셀 배포는 비용이 들기 때문... 근데 이렇게 개인 레포로 포크해서 하는게 합법적인 불법 (?) 인 것 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㅎ...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 배포 자동화를 위한 GitHub Actions 설정&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;GitHub Actions를 이용한 레포 동기화 및 배포 자동화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 레포에서 배포를 진행하다 보니, &lt;b&gt;팀 레포와의 코드 동기화&lt;/b&gt;가 필요했다. 이를 위해 GitHub Actions를 활용하여 &lt;b&gt;팀 레포(main 브랜치)&lt;/b&gt;에 변경 사항이 생기면 자동으로 내 개인 레포에 반영되도록 설정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 .github/workflows/deploy.yml 파일을 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 워크플로우는 하나의 GitHub 저장소에서 다른 저장소로 코드를 자동으로 동기화하고 배포하는 자동화 과정이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;deploy.yml 코드 설명&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;name: Auto-Sync and Deploy to Personal Repo
on:
  push:
    branches: ['main']

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          persist-credentials: false
      - name: Create and copy files
        run: |
          mkdir -p output
          cp -R ./* ./.[^.]* output/ 2&amp;gt;/dev/null || :
          rm -rf output/output
      - name: Pushes to another repository
        uses: cpina/github-action-push-to-another-repository@main
        env:
          API_TOKEN_GITHUB: ${{ secrets.AUTO_ACTIONS }}
        with:
          source-directory: 'output'
          destination-github-username: ssolfa
          destination-repository-name: ppusyung-tarot
          user-email: ${{ secrets.EMAIL }}
          user-name: ssolfa
          commit-message: ${{ github.event.commits[0].message }}
          target-branch: main
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;주요 내용&lt;/b&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;push 이벤트 감지&lt;/b&gt;&lt;br /&gt;main 브랜치에 push가 발생하면 이 workflow가 실행된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드 체크아웃&lt;/b&gt;&lt;br /&gt;actions/checkout@v4을 사용하여 팀 레포의 최신 코드를 가져온다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일 복사 및 준비&lt;/b&gt;&lt;br /&gt;mkdir -p output을 통해 output 디렉토리를 생성하고, 프로젝트 파일을 복사한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개인 레포로 코드 푸시&lt;/b&gt;&lt;br /&gt;cpina/github-action-push-to-another-repository@main 액션을 사용하여, 내 개인 레포(ppusyung-tarot)로 코드를 푸시한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미래의 나같은 사람들을 위해... 진짜 자세히 설명을 해보겠다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;트리거 조건&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;on:
  push:
    branches: ['main']
&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;이 워크플로우는 'main' 브랜치에 코드가 푸시될 때마다 자동으로 실행된다.&lt;/li&gt;
&lt;li&gt;즉, 메인 브랜치에 변경사항이 생길 때마다 동기화 작업이 시작된다는 뜻!&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;작업 구성&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;jobs:
  build:
    runs-on: ubuntu-latest
&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;'build'라는 하나의 작업을 정의하며, 최신 Ubuntu 환경에서 실행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;단계별 실행 과정&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 코드 체크아웃&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;- name: Checkout code
  uses: actions/checkout@v4
  with:
    fetch-depth: 0
    persist-credentials: false
&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;actions/checkout@v4: GitHub의 공식 체크아웃 액션을 사용하여 현재 저장소의 코드를 워크플로우 환경으로 가져온다.&lt;/li&gt;
&lt;li&gt;fetch-depth: 0: 전체 커밋 기록을 가져온다. 완전한 저장소 복사본을 위해 필요하다. depth를 자유롭게 1로 할 수도 있다!
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fetch-depth: 1로 설정하면&amp;nbsp;&lt;b&gt;최신 커밋만 가져오게 된다. &lt;/b&gt;전체 Git 이력 대신 가장 최근 커밋만 체크아웃된다! 어차피 처음에 개인 레포로 포크 해올 때 전에 파일들은 전부 가져온 거나 다름이 없고! 현재 워크플로우에서는 단순히 파일을 복사하여 다른 저장소로 푸시하는 것이 목적이므로, fetch-depth: 1로 설정해도 기능적으로는 문제가 없 다. 오히려 이게 더 효율적일수도?!&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;persist-credentials: false: GitHub 기본 인증 정보를 유지하지 않는다. 이는 나중에 다른 인증 정보로 다른 저장소에 푸시하기 위함이다.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;우리는 지금 두 개의 다른 저장소를 다루고 있다. 따라서 두 개의 다른 인증 정보가 필요하다. 기본 인증 대신 ${{ secrets.AUTO_ACTIONS }} 토큰을 사용하기 때문에 이걸 false로 설정해준다. true로 해도 되는지는... 안해봐서 모르겠다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 파일 복사 및 준비&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;- name: Create and copy files
  run: |
    mkdir -p output
    cp -R ./* ./.[^.]* output/ 2&amp;gt;/dev/null || :
    rm -rf output/output
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;output&lt;span&gt;&amp;nbsp;&lt;/span&gt;디렉토리를 만들고 모든 파일을 여기로 복사해서 이&lt;span&gt;&amp;nbsp;&lt;/span&gt;output&lt;span&gt;&amp;nbsp;&lt;/span&gt;디렉토리의 내용을&lt;span&gt;&amp;nbsp;&lt;/span&gt;ssolfa/ppusyung-tarot&lt;span&gt;&amp;nbsp;&lt;/span&gt;저장소로 푸시할거다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mkdir -p output: 'output'이라는 디렉토리를 생성한다.&lt;/li&gt;
&lt;li&gt;cp -R ./* ./.[^.]* output/ 2&amp;gt;/dev/null || ::
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 디렉토리의 모든 파일과 숨김 파일(.로 시작하는 파일)을 output 디렉토리로 복사한다.&lt;/li&gt;
&lt;li&gt;2&amp;gt;/dev/null || :: 오류 메시지를 무시하고, 명령이 실패해도 워크플로우가 계속 진행되도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;rm -rf output/output: 중첩된 output 디렉토리를 제거하여 무한 복사를 방지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;복사 과정이 필요한 이유&lt;/b&gt;&lt;/p&gt;
&lt;div&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;&amp;nbsp;&lt;/span&gt;cpina/github-action-push-to-another-repository&lt;span&gt;&amp;nbsp;&lt;/span&gt;액션은 특정 디렉토리 내용만 다른 저장소로 푸시하도록 설계되었다. 따라서 푸시할 내용을 별도 디렉토리에 준비해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;선택적&amp;nbsp;파일&amp;nbsp;제외&amp;nbsp;가능&lt;/b&gt;:&amp;nbsp;복사&amp;nbsp;단계에서&amp;nbsp;특정&amp;nbsp;파일이나&amp;nbsp;디렉토리를&amp;nbsp;제외할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;현재&amp;nbsp;스크립트는&amp;nbsp;단순히&amp;nbsp;모든&amp;nbsp;것을&amp;nbsp;복사하지만,&amp;nbsp;필요하다면&amp;nbsp;여기서&amp;nbsp;.git&amp;nbsp;폴더나&amp;nbsp;특정&amp;nbsp;설정&amp;nbsp;파일을&amp;nbsp;제외할&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;-&amp;gt;&amp;nbsp;귀찮아서&amp;nbsp;안했지만&amp;nbsp;ㅠ&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 다른 저장소로 푸시&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;- name: Pushes to another repository
  uses: cpina/github-action-push-to-another-repository@main
  env:
    API_TOKEN_GITHUB: ${{ secrets.AUTO_ACTIONS }}
  with:
    source-directory: 'output'
    destination-github-username: ssolfa
    destination-repository-name: ppusyung-tarot
    user-email: ${{ secrets.EMAIL }}
    user-name: ssolfa
    commit-message: ${{ github.event.commits[0].message }}
    target-branch: main
&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;cpina/github-action-push-to-another-repository@main: 다른 저장소로 코드를 푸시하기 위한 커뮤니티 액션을 사용한다.&lt;/li&gt;
&lt;li&gt;환경 변수:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API_TOKEN_GITHUB: ${{ secrets.AUTO_ACTIONS }}: 대상 저장소에 접근하는 데 필요한 GitHub 액세스 토큰을 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;설정 매개변수:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;source-directory: 'output': 이전 단계에서 준비한 (복사한) 디렉토리!&lt;/li&gt;
&lt;li&gt;destination-github-username: ssolfa: 대상 저장소의 소유자 -&amp;gt; 포크한 사람의 깃허브 닉네임&lt;/li&gt;
&lt;li&gt;destination-repository-name: ppusyung-tarot: 동기화할 대상 저장소의 이름 -&amp;gt; 포크한 레포 이름&lt;/li&gt;
&lt;li&gt;user-email &amp;amp; user-name: 커밋을 생성할 사용자의 정보 -&amp;gt; email도 명시해도 되는데 ㅜㅜ 이것만 개인정보라 가렸음 위에서 닉네임, 레포 이름 전부 시크릿 처리 해도 됨!&lt;/li&gt;
&lt;li&gt;commit-message: ${{ github.event.commits[0].message }}: 원본 커밋 메시지를 그대로 사용한다.&lt;/li&gt;
&lt;li&gt;target-branch: main: 대상 저장소의 어떤 브랜치에 푸시할지 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 따라서 이 워크플로우가 팀 레포의 'main' 브랜치에서 변경이 일어날 때마다 모든 파일을 'ssolfa/ppusyung-tarot' 저장소의 'main' 브랜치로 자동으로 복사해준다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;GitHub Actions Secrets 설정하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 workflow에서 secrets.AUTO_ACTIONS와 secrets.EMAIL을 사용하고 있다. 이를 설정하는 방법은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;GitHub에서 개인 레포의 &lt;b&gt;Settings &amp;rarr; Secrets and variables &amp;rarr; Actions&lt;/b&gt;로 이동한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;New repository secret&lt;/b&gt; 버튼을 클릭하여 아래의 값을 추가한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AUTO_ACTIONS: GitHub Personal Access Token (PAT)&lt;/li&gt;
&lt;li&gt;EMAIL: 커밋할 때 사용할 이메일 주소&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AUTO_ACTIONS 발급 방법
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GitHub &lt;b&gt;Settings &amp;rarr; Developer settings &amp;rarr; Personal access tokens&lt;/b&gt;에서 &lt;b&gt;Fine-grained tokens&lt;/b&gt;를 생성한다.&lt;/li&gt;
&lt;li&gt;repo 권한을 부여하여 &lt;b&gt;team repo에서 개인 repo로 코드 푸시가 가능&lt;/b&gt;하도록 설정한다.&lt;/li&gt;
&lt;li&gt;생성된 토큰을 secrets.AUTO_ACTIONS에 등록한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 팀 레포에서 main 브랜치에 변경이 발생하면 &lt;b&gt;자동으로 내 개인 레포에도 반영&lt;/b&gt;되며, 개인 레포에서 Vercel로 배포가 진행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;⬇️ 코드 전문 확인하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/yourssu/ppusyung-tarot/commit/74be5a7503993b3e9faeaeb1908d06a833ffe561&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/yourssu/ppusyung-tarot/commit/74be5a7503993b3e9faeaeb1908d06a833ffe561&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1741415311249&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;feat: 배포 자동화 yml 작성 &amp;middot; yourssu/ppusyung-tarot@74be5a7&quot; data-og-description=&quot;ssolfa committed Feb 26, 2025&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/yourssu/ppusyung-tarot/commit/74be5a7503993b3e9faeaeb1908d06a833ffe561&quot; data-og-url=&quot;https://github.com/yourssu/ppusyung-tarot/commit/74be5a7503993b3e9faeaeb1908d06a833ffe561&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/xkDcl/hyYmPsKqmn/Cp01BkCyTv0VkJl9gg9od0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/Yia9M/hyYmYwpGXX/YZMES82T8Q71Rum59nrxak/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/yourssu/ppusyung-tarot/commit/74be5a7503993b3e9faeaeb1908d06a833ffe561&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/yourssu/ppusyung-tarot/commit/74be5a7503993b3e9faeaeb1908d06a833ffe561&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/xkDcl/hyYmPsKqmn/Cp01BkCyTv0VkJl9gg9od0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/Yia9M/hyYmYwpGXX/YZMES82T8Q71Rum59nrxak/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;feat: 배포 자동화 yml 작성 &amp;middot; yourssu/ppusyung-tarot@74be5a7&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;ssolfa committed Feb 26, 2025&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&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;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Route 53을 통한 도메인 연결&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포까지 했으면 도메인 연결이 필요하다. 기본적으로 Vercel이 제공하는 `project-name.vercel.app` 도메인 대신 우리 동아리 도메인을 사용해 볼 예정이다. 짜치는 도메인을 예쁘게 만들어보쟈&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Vercel에서 도메인 추가하기&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.15.55.png&quot; data-origin-width=&quot;3018&quot; data-origin-height=&quot;1790&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXvDIx/btsMFmn3YSM/ZZ7SoOhN0yyq7tWXkXuR21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXvDIx/btsMFmn3YSM/ZZ7SoOhN0yyq7tWXkXuR21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXvDIx/btsMFmn3YSM/ZZ7SoOhN0yyq7tWXkXuR21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXvDIx%2FbtsMFmn3YSM%2FZZ7SoOhN0yyq7tWXkXuR21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3018&quot; height=&quot;1790&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.15.55.png&quot; data-origin-width=&quot;3018&quot; data-origin-height=&quot;1790&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포된 프로젝트의 Vercel 대시보드로 이동한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.16.01.png&quot; data-origin-width=&quot;3020&quot; data-origin-height=&quot;1718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IxK6V/btsMEb2srmV/D9Ba86npUdZ797pkDnkHaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IxK6V/btsMEb2srmV/D9Ba86npUdZ797pkDnkHaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IxK6V/btsMEb2srmV/D9Ba86npUdZ797pkDnkHaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIxK6V%2FbtsMEb2srmV%2FD9Ba86npUdZ797pkDnkHaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3020&quot; height=&quot;1718&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.16.01.png&quot; data-origin-width=&quot;3020&quot; data-origin-height=&quot;1718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 설정(Settings)에서 Domains 탭을 클릭한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.16.09.png&quot; data-origin-width=&quot;3012&quot; data-origin-height=&quot;1712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bguWav/btsMFzAFByd/m51TRlAuM84B7ASHmylWPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bguWav/btsMFzAFByd/m51TRlAuM84B7ASHmylWPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bguWav/btsMFzAFByd/m51TRlAuM84B7ASHmylWPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbguWav%2FbtsMFzAFByd%2Fm51TRlAuM84B7ASHmylWPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3012&quot; height=&quot;1712&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.16.09.png&quot; data-origin-width=&quot;3012&quot; data-origin-height=&quot;1712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Add&quot; 버튼을 클릭하고 원하는 서브도메인을 `[원하는이름].yourssu.com` 형태로 입력한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.16.31.png&quot; data-origin-width=&quot;1556&quot; data-origin-height=&quot;1374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhqHxP/btsMD1ZKlNR/t5LMuIsVAt7GKqhp6cX5OK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhqHxP/btsMD1ZKlNR/t5LMuIsVAt7GKqhp6cX5OK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhqHxP/btsMD1ZKlNR/t5LMuIsVAt7GKqhp6cX5OK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhqHxP%2FbtsMD1ZKlNR%2Ft5LMuIsVAt7GKqhp6cX5OK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1556&quot; height=&quot;1374&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.16.31.png&quot; data-origin-width=&quot;1556&quot; data-origin-height=&quot;1374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- 예: `tarot.yourssu.com` 또는 `ppusyung.yourssu.com`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.17.20.png&quot; data-origin-width=&quot;2062&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dPdgo2/btsMEm3A2PL/d9WTRizchTtwksIxZLg1kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dPdgo2/btsMEm3A2PL/d9WTRizchTtwksIxZLg1kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dPdgo2/btsMEm3A2PL/d9WTRizchTtwksIxZLg1kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdPdgo2%2FbtsMEm3A2PL%2Fd9WTRizchTtwksIxZLg1kk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2062&quot; height=&quot;904&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.17.20.png&quot; data-origin-width=&quot;2062&quot; data-origin-height=&quot;904&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인을 추가하면 &quot;Invalid Configuration&quot; 메시지가 표시된다. 다행히~ 정상이다! DNS 레코드가 아직 설정되지 않았기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel이&amp;nbsp;제공하는&amp;nbsp;CNAME&amp;nbsp;값(일반적으로&amp;nbsp;`cname.vercel-dns.com.`&amp;nbsp;형식)을 사용해서 밑에서 도메인을 연결할 거다.&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;Route 53에서 CNAME 레코드 추가하기&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통은 A 레코드와 CNAME 레코드를 함께 추가해야 하지만, yourssu.com 도메인은 이미 존재하므로 &lt;b&gt;A 레코드는 설정하지 않고 CNAME만 추가&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;도메인 설정의 기본 개념&lt;/b&gt;&lt;br /&gt;일반적인 도메인 설정에서는 두 가지 DNS 레코드가 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;color: #333333;&quot;&gt;&lt;b&gt;A 레코드&lt;/b&gt;: 특정 IP 주소에 도메인을 매핑하는 역할 - 도메인(example.com)을 서버의 IP 주소에 직접 연결&lt;/li&gt;
&lt;li style=&quot;color: #333333;&quot;&gt;&lt;b&gt;CNAME 레코드&lt;/b&gt;: 기존 도메인을 다른 도메인으로 연결하는 역할 - 서브도메인(project.example.com)을 다른 도메인으로 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동아리에서는 이미 yourssu.com 도메인의 A 레코드가 설정되어 있다! 따라서 우리는 CNAME 레코드만 추가하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;a 레코드 추가하는 방법은 이 걸 참고하면 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@jangsebari/AWS-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%A0%95%EC%A0%81-%EC%9B%B9-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%97%B0%EA%B2%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@jangsebari/AWS-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%A0%95%EC%A0%81-%EC%9B%B9-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%97%B0%EA%B2%B0&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1741418073722&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;AWS 를 이용한 정적 웹 사이트 도메인 연결&quot; data-og-description=&quot;들어가며 개발자로 지내는 동안 aws 를 사용하여 프로젝트를 빌드해서 배포하고 서버와 함께 운영해 본 경험이 있었다. 우여곡절 끝에 어쩌다보니 실행이 됐다며 기뻐했던 기억이 나는데... 좋아&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@jangsebari/AWS-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%A0%95%EC%A0%81-%EC%9B%B9-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%97%B0%EA%B2%B0&quot; data-og-url=&quot;https://velog.io/@jangsebari/AWS-를-이용한-정적-웹-사이트-도메인-연결&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fv8M9/hyYqMnzlJD/CPnvAjgMHNJMlszzmphbt1/img.png?width=760&amp;amp;height=311&amp;amp;face=0_0_760_311,https://scrap.kakaocdn.net/dn/bF0nV3/hyYqTmG8JY/KwCUJo8CKMawQkiwJwSd60/img.png?width=760&amp;amp;height=311&amp;amp;face=0_0_760_311,https://scrap.kakaocdn.net/dn/fUcrD/hyYmWZImHT/p0dBq6bzdyDu97SUHRw5Wk/img.png?width=1645&amp;amp;height=423&amp;amp;face=0_0_1645_423&quot;&gt;&lt;a href=&quot;https://velog.io/@jangsebari/AWS-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%A0%95%EC%A0%81-%EC%9B%B9-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%97%B0%EA%B2%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@jangsebari/AWS-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%A0%95%EC%A0%81-%EC%9B%B9-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%97%B0%EA%B2%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fv8M9/hyYqMnzlJD/CPnvAjgMHNJMlszzmphbt1/img.png?width=760&amp;amp;height=311&amp;amp;face=0_0_760_311,https://scrap.kakaocdn.net/dn/bF0nV3/hyYqTmG8JY/KwCUJo8CKMawQkiwJwSd60/img.png?width=760&amp;amp;height=311&amp;amp;face=0_0_760_311,https://scrap.kakaocdn.net/dn/fUcrD/hyYmWZImHT/p0dBq6bzdyDu97SUHRw5Wk/img.png?width=1645&amp;amp;height=423&amp;amp;face=0_0_1645_423');&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;AWS 를 이용한 정적 웹 사이트 도메인 연결&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며 개발자로 지내는 동안 aws 를 사용하여 프로젝트를 빌드해서 배포하고 서버와 함께 운영해 본 경험이 있었다. 우여곡절 끝에 어쩌다보니 실행이 됐다며 기뻐했던 기억이 나는데... 좋아&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&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;
&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;Route 53에서 레코드 생성하기&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.14.13.png&quot; data-origin-width=&quot;3022&quot; data-origin-height=&quot;1718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwucis/btsMEUZMU0I/uTmKkwKqQpzLxkR2Q6m720/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwucis/btsMEUZMU0I/uTmKkwKqQpzLxkR2Q6m720/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwucis/btsMEUZMU0I/uTmKkwKqQpzLxkR2Q6m720/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcwucis%2FbtsMEUZMU0I%2FuTmKkwKqQpzLxkR2Q6m720%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3022&quot; height=&quot;1718&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.14.13.png&quot; data-origin-width=&quot;3022&quot; data-origin-height=&quot;1718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AWS&amp;nbsp;콘솔에&amp;nbsp;접속하여&amp;nbsp;Route&amp;nbsp;53&amp;nbsp;서비스로&amp;nbsp;이동한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이미 도메인을 등록해둔 상태, dns 관리를 클릭해서 호스팅 영역으로 들어간다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.14.33.png&quot; data-origin-width=&quot;3018&quot; data-origin-height=&quot;1722&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVnj2g/btsMEDKUSfe/bjlsqo3Da9eOHs6oNrGVZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVnj2g/btsMEDKUSfe/bjlsqo3Da9eOHs6oNrGVZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVnj2g/btsMEDKUSfe/bjlsqo3Da9eOHs6oNrGVZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVnj2g%2FbtsMEDKUSfe%2Fbjlsqo3Da9eOHs6oNrGVZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3018&quot; height=&quot;1722&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.14.33.png&quot; data-origin-width=&quot;3018&quot; data-origin-height=&quot;1722&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기존 호스팅 영역(yourssu.com)을 선택한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.14.41.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1722&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m5xsD/btsME7YYbpy/dKovqnvV71fMG0AzzAG65K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m5xsD/btsME7YYbpy/dKovqnvV71fMG0AzzAG65K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m5xsD/btsME7YYbpy/dKovqnvV71fMG0AzzAG65K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm5xsD%2FbtsME7YYbpy%2FdKovqnvV71fMG0AzzAG65K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1722&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.14.41.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1722&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;레코드 생성&quot; 버튼을 클릭한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.14.52.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wP5Yb/btsMFyV2L4K/dYsocNpMolWvjbhKD0griK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wP5Yb/btsMFyV2L4K/dYsocNpMolWvjbhKD0griK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wP5Yb/btsMFyV2L4K/dYsocNpMolWvjbhKD0griK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwP5Yb%2FbtsMFyV2L4K%2FdYsocNpMolWvjbhKD0griK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1718&quot; data-filename=&quot;스크린샷 2025-03-08 오후 3.14.52.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;레코드 이름은 버셀에서 설정한 원하는&amp;nbsp;서브도메인&amp;nbsp;(예:&amp;nbsp;tarot)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;레코드 유형은 &lt;b&gt;CNAME 레코드&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;값은 버셀에서 준 value &lt;/b&gt;&lt;/b&gt;-&amp;gt; cname.vercel.-dns.com. (마지막 점 까지 입력)&lt;br /&gt;TTL은 기본 300으로 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단순 라우팅(Simple Routing) 선택&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 입력한 도메인과 Vercel에서 제공하는 도메인을 연결한다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-03-08 오후 4.16.36.png&quot; data-origin-width=&quot;1892&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zxHo6/btsMEQ4eQ2c/GEhhUBFVPRbaYhVxQuWoJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zxHo6/btsMEQ4eQ2c/GEhhUBFVPRbaYhVxQuWoJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zxHo6/btsMEQ4eQ2c/GEhhUBFVPRbaYhVxQuWoJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzxHo6%2FbtsMEQ4eQ2c%2FGEhhUBFVPRbaYhVxQuWoJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1892&quot; height=&quot;702&quot; data-filename=&quot;스크린샷 2025-03-08 오후 4.16.36.png&quot; data-origin-width=&quot;1892&quot; data-origin-height=&quot;702&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레코드를 생성하면&amp;nbsp;&lt;b&gt;Vercel에서 설정한 도메인으로 정상적으로 접근 가능&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;하다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;버셀 프로젝트 도메인에 들어가보면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Vercel의&amp;nbsp;도메인&amp;nbsp;상태가&amp;nbsp;&quot;Invalid&quot;에서&amp;nbsp;&quot;Valid&quot;로&amp;nbsp;변경되는&amp;nbsp;것도 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 원하는 서브 도메인으로 Vercel에 배포한 프로젝트에 접속할 수 있다!&lt;/p&gt;</description>
      <category>React</category>
      <category>route53</category>
      <category>Vercel</category>
      <category>yml</category>
      <category>깃허브액션</category>
      <category>도메인설정</category>
      <category>배포</category>
      <category>배포자동화</category>
      <category>빌드</category>
      <category>웹사이트</category>
      <category>자동화</category>
      <author>solfa</author>
      <guid isPermaLink="true">https://5ffthewall.tistory.com/135</guid>
      <comments>https://5ffthewall.tistory.com/135#entry135comment</comments>
      <pubDate>Sat, 8 Mar 2025 16:18:56 +0900</pubDate>
    </item>
    <item>
      <title>[React] ky로 로그인 유지 구현하기 (with. JWT 인증)</title>
      <link>https://5ffthewall.tistory.com/134</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;API 요청을 할 때, 401 Unauthorized 응답이 오면 보통 액세스 토큰을 갱신(Refresh)하는 로직을 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 ky 라이브러리를 활용하면 beforeRetry 훅을 사용해 비교적 간결하게 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;일단 ky에서 제공하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;BeforeRetryHook&lt;/b&gt;을 살펴보면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-18 오후 6.47.01.png&quot; data-origin-width=&quot;2338&quot; data-origin-height=&quot;876&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kA7am/btsMnOZjeAe/j1SRdgmVUUKaUSRIv5ViH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kA7am/btsMnOZjeAe/j1SRdgmVUUKaUSRIv5ViH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kA7am/btsMnOZjeAe/j1SRdgmVUUKaUSRIv5ViH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkA7am%2FbtsMnOZjeAe%2Fj1SRdgmVUUKaUSRIv5ViH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2338&quot; height=&quot;876&quot; data-filename=&quot;스크린샷 2025-02-18 오후 6.47.01.png&quot; data-origin-width=&quot;2338&quot; data-origin-height=&quot;876&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BeforeRetryHook&lt;/b&gt;에는 다음과 같은 타입이 정의되어 있다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;nbsp;[ky&amp;nbsp;공식&amp;nbsp;문서](&lt;a href=&quot;https://www.npmjs.com/package/ky#:~:text=users%27)%3B-,hooks.beforeRetry,-Type%3A%20Function)&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.npmjs.com/package/ky#:~:text=users%27)%3B-,hooks.beforeRetry,-Type%3A%20Function)&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1739874780464&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;ky&quot; data-og-description=&quot;Tiny and elegant HTTP client based on the Fetch API. Latest version: 1.7.5, last published: 6 days ago. Start using ky in your project by running &amp;#96;npm i ky&amp;#96;. There are 698 other projects in the npm registry using ky.&quot; data-og-host=&quot;www.npmjs.com&quot; data-og-source-url=&quot;https://www.npmjs.com/package/ky#:~:text=users%27)%3B-,hooks.beforeRetry,-Type%3A%20Function)&quot; data-og-url=&quot;https://www.npmjs.com/package/ky&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/TnCXw/hyYjnBhtfg/pjGOttW4yRKoDK9GNgrce0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/ky#:~:text=users%27)%3B-,hooks.beforeRetry,-Type%3A%20Function)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.npmjs.com/package/ky#:~:text=users%27)%3B-,hooks.beforeRetry,-Type%3A%20Function)&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/TnCXw/hyYjnBhtfg/pjGOttW4yRKoDK9GNgrce0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&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;ky&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Tiny and elegant HTTP client based on the Fetch API. Latest version: 1.7.5, last published: 6 days ago. Start using ky in your project by running `npm i ky`. There are 698 other projects in the npm registry using ky.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.npmjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에서 제공하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;retryCount&lt;/b&gt;를 사용 하냐 마냐가 오늘의 주제이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;retryCount&lt;/b&gt;는 ky에서 자동으로 0부터 시작해 증가하는 값이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;a href=&quot;https://devpluto.tistory.com/entry/React-ky%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9C%A0%EC%A7%80-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-with-JWT-%EC%9D%B8%EC%A6%9D&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://devpluto.tistory.com/entry/React-ky%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9C%A0%EC%A7%80-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-with-JWT-%EC%9D%B8%EC%A6%9D&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739871309333&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[React] ky로 로그인 유지 구현하기 (with. JWT 인증)&quot; data-og-description=&quot;들어가며 ky는 fetch와 ESM을 기반으로 하는 HTTP Client를 제공하는 라이브러리입니다. 주로 사용되는 Axios의 interceptors와 비슷하게 ky는 응답을 intercept 하여 커스텀하게 조작이 가능한 AfterResponse, Befor&quot; data-og-host=&quot;devpluto.tistory.com&quot; data-og-source-url=&quot;https://devpluto.tistory.com/entry/React-ky%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9C%A0%EC%A7%80-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-with-JWT-%EC%9D%B8%EC%A6%9D&quot; data-og-url=&quot;https://devpluto.tistory.com/entry/React-ky%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9C%A0%EC%A7%80-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-with-JWT-%EC%9D%B8%EC%A6%9D&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bkx9DL/hyYjwLKWvj/tDrfTmQEdiK1R1E2ZNcuSK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/QTyvc/hyYf013QIg/sHUykLdmM075DjckYV7tnK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/KRHav/hyYf4wCSwh/zYecLc95YkSNwxmb1W0es0/img.jpg?width=750&amp;amp;height=500&amp;amp;face=0_0_750_500&quot;&gt;&lt;a href=&quot;https://devpluto.tistory.com/entry/React-ky%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9C%A0%EC%A7%80-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-with-JWT-%EC%9D%B8%EC%A6%9D&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devpluto.tistory.com/entry/React-ky%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9C%A0%EC%A7%80-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-with-JWT-%EC%9D%B8%EC%A6%9D&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bkx9DL/hyYjwLKWvj/tDrfTmQEdiK1R1E2ZNcuSK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/QTyvc/hyYf013QIg/sHUykLdmM075DjckYV7tnK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/KRHav/hyYf4wCSwh/zYecLc95YkSNwxmb1W0es0/img.jpg?width=750&amp;amp;height=500&amp;amp;face=0_0_750_500');&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;[React] ky로 로그인 유지 구현하기 (with. JWT 인증)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며 ky는 fetch와 ESM을 기반으로 하는 HTTP Client를 제공하는 라이브러리입니다. 주로 사용되는 Axios의 interceptors와 비슷하게 ky는 응답을 intercept 하여 커스텀하게 조작이 가능한 AfterResponse, Befor&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;devpluto.tistory.com&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이 글을 참고해서 비슷하게 구현하려고 했고 내가 이해한 글의 코드처럼 마지막 재시도 전에 로그아웃을 수행하도록 구현했다.&lt;/p&gt;
&lt;pre id=&quot;code_1739871410103&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const DEFAULT_API_RETRY_LIMIT = 2;

const handleTokenRefresh: BeforeRetryHook = async ({ error, retryCount }) =&amp;gt; {
  // retryCount가 limit-1일 때(마지막 재시도 전) 로그아웃
  if (retryCount === DEFAULT_API_RETRY_LIMIT - 1) {
    authService.logout();
    return ky.stop;
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나... 모각코 시간에 이게 무슨 로직이냐는 질문에 난 대답을 할 수 없었고 ㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;retry.limit으로 이미 재시도 횟수를 제한하고 있는데, retryCount 체크가 필요할까?&lt;br /&gt;코드처럼 마지막 재시도 전에 로그아웃하는 것이 좋은 방법일까?&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㅎ&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;두&amp;nbsp;가지&amp;nbsp;접근&amp;nbsp;방식&amp;nbsp;비교&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 기존 코드였던 (retryCount === DEFAULT_API_RETRY_LIMIT - 1) 이렇게 마지막 재시도 전을 체크하는 방식을 알아보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅ 첫 번째 방식 : retryCount를 체크해 마지막 재시도 전을 감지&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;sas&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;const handleTokenRefresh: BeforeRetryHook = async ({ error, retryCount }) =&amp;gt; {
  const httpError = error as HTTPError;

  if (httpError.response.status !== 401) {
    return ky.stop;
  }

  if (retryCount === DEFAULT_API_RETRY_LIMIT - 1) {
    authService.logout();
    return ky.stop;
  }

  const refreshToken = tokenService.getRefreshToken();
  if (!refreshToken) {
    authService.logout();
    return ky.stop;
  }

  try {
    await authService.refreshToken(refreshToken);
  } catch (error) {
    console.error(&quot;Token refresh 실패, 로그아웃&quot;, error);
    authService.logout();
    return ky.stop;
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이 방식의 특징&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;retryCount === DEFAULT_API_RETRY_LIMIT - 1 체크를 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;마지막 재시도 전에 로그아웃을 수행한다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;마지막 요청을 수행하지 않고 미리 로그아웃하여 불필요한 요청을 방지한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 흐름은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ 첫 번째 API 요청 실패 (`401` 에러) &amp;rarr; `retryCount = 0`&amp;nbsp;&amp;nbsp;&lt;br /&gt;2️⃣&amp;nbsp;`refreshToken`으로&amp;nbsp;액세스&amp;nbsp;토큰&amp;nbsp;갱신&amp;nbsp;시도&amp;nbsp;&amp;nbsp;&lt;br /&gt;3️⃣&amp;nbsp;두&amp;nbsp;번째&amp;nbsp;시도&amp;nbsp;직전(`retryCount&amp;nbsp;=&amp;nbsp;1`)&amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- `retryCount === DEFAULT_API_RETRY_LIMIT - 1 (1 === 2-1)`이므로 조건이 참이 되고 로그아웃 실행&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;마지막&amp;nbsp;요청을&amp;nbsp;보내지&amp;nbsp;않고&amp;nbsp;중단&amp;nbsp;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;마지막 시도를 생략하고 미리 로그아웃하는 것이 이 방식의 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론적으로 DEFAULT_API_RETRY_LIMIT를 쓰는 이유는&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;=&amp;gt; 더 시도해봤자 실패할 거 같으니까 미리 포기하고 로그아웃 한다는 의미~ 라고 이해하면 되겠다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;주의할 점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ky.limit을 쓰지 않고 일부러 retryCount를 체크하는 의도는 이해했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 시도 전을 감지해서 사용자 경험을 더 좋게 하려는 건데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;limit 횟수가 3-4번도 아니고 2번일 거면 한 번만 시도하고 로그아웃하나 마지막 재시도 전을 감지해서 중지하나 똑같으니 최소 횟수를 4 이상으로 설정하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 코드처럼&amp;nbsp;&lt;b&gt;DEFAULT_API_RETRY_LIMIT&lt;/b&gt;가 2인 이상 retryCount를 체크하는 방식이 불필요하게 복잡함 대신&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DEFAULT_API_RETRY_LIMIT = 4이면 &lt;b&gt;3번까지는 시도해보고, 4번째 요청은 막으니까 &lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이런 로직에 좋음&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;refreshToken이 만료된 경우,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;마지막 재시도를 하지 않고 즉시 로그아웃하니까&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션이 만료되었음을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;빠르게 반영&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr; 사용자 경험(UX) 개선이 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅ 두 번째 방식: ky의 기본 동작을 따르는 방식&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import ky, { type BeforeRetryHook, HTTPError } from &quot;ky&quot;;
import { authService } from &quot;./auth.service&quot;;
import { tokenService } from &quot;./token.service&quot;;

const DEFAULT_API_RETRY_LIMIT = 2;

const handleTokenRefresh: BeforeRetryHook = async ({ error }) =&amp;gt; {
  const httpError = error as HTTPError;

  if (httpError.response.status !== 401) {
    return ky.stop;
  }

  const refreshToken = tokenService.getRefreshToken();
  if (!refreshToken) {
    authService.logout();
    return ky.stop;
  }

  try {
    await authService.refreshToken(refreshToken);
  } catch (error) {
    console.error(&quot;Token refresh 실패, 로그아웃&quot;, error);
    authService.logout();
    return ky.stop;
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이 방식의 특징&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;retryCount를 따로 체크하지 않음 &amp;rarr; ky의 기본 재시도 로직을 따름&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ky의 기본적인 retry.limit 설정을 활용하여 &lt;b&gt;불필요한 로직이 없음&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;refreshToken이 만료된 경우, &lt;b&gt;마지막 재시도를 수행한 후에야 로그아웃&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;즉, 마지막 요청이 의미 없이 실행될 가능성이 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 단점 때문에 처음 방식 (&lt;b&gt;DEFAULT_API_RETRY_LIMIT을 체크하는 방식&lt;/b&gt;)을 고려하지 않았을까 싶다!&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;그래서 결론은 : 마지막 재시도 전에 로그아웃을 하는 것이 나을까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 경우에는 &lt;b&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;ky의 기본적인 retry.limit&lt;span&gt; 만 체크하는 것&lt;/span&gt;&lt;/span&gt;이&lt;/b&gt;&amp;nbsp;더 깔끔하고 직관적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`retryCount`를 체크하지 않아도 `ky.retry.limit`이 이미 요청 횟수를 관리하기도 하니까. (물론 재시도 전을 감지하냐 마냐 차이가 있긴 하지만)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 UX적으로 &lt;b&gt;즉각적인 로그아웃이 필요하다면 앞서 말한 첫 번째 방식도 고려할 수 있다.&lt;/b&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;느낀점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ky 관련 예제나 코드가 많이 없어서 코드를 복붙해서 썼는데 어떤 로직인지 제대로 파악하지 않은채 쓴 건 실수였다.&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;/p&gt;</description>
      <category>React</category>
      <category>axios</category>
      <category>HTTP</category>
      <category>jwt</category>
      <category>KY</category>
      <category>토큰</category>
      <author>solfa</author>
      <guid isPermaLink="true">https://5ffthewall.tistory.com/134</guid>
      <comments>https://5ffthewall.tistory.com/134#entry134comment</comments>
      <pubDate>Tue, 18 Feb 2025 19:48:19 +0900</pubDate>
    </item>
    <item>
      <title>[React/리액트] ky를 활용한 토큰 인증 시스템 구현하기 - Google OAuth와 Token Refresh</title>
      <link>https://5ffthewall.tistory.com/133</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 통신에는 항상 axios만 사용해왔는데 요즘에는 오히려 fetch만 쓰는게 덜 무겁다는 의견도 많이 나오고 axios는 과하다는 이야기가 많아서 이번 프로젝트에서는 fetch 기반의 ky 라이브러리를 사용해서 데이터 통신을 해보고자 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ky란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/ky&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.npmjs.com/package/ky&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739433739510&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;ky&quot; data-og-description=&quot;Tiny and elegant HTTP client based on the Fetch API. Latest version: 1.7.5, last published: a day ago. Start using ky in your project by running &amp;#96;npm i ky&amp;#96;. There are 693 other projects in the npm registry using ky.&quot; data-og-host=&quot;www.npmjs.com&quot; data-og-source-url=&quot;https://www.npmjs.com/package/ky&quot; data-og-url=&quot;https://www.npmjs.com/package/ky&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bIUrFR/hyYfIMGF7n/PgZ09P05ub9mKyyffhHKgK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/ky&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.npmjs.com/package/ky&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bIUrFR/hyYfIMGF7n/PgZ09P05ub9mKyyffhHKgK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&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;ky&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Tiny and elegant HTTP client based on the Fetch API. Latest version: 1.7.5, last published: a day ago. Start using ky in your project by running `npm i ky`. There are 693 other projects in the npm registry using ky.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.npmjs.com&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/ky&quot;&gt;ky&lt;/a&gt;는 &lt;a href=&quot;https://sindresorhus.com/&quot;&gt;Sindre Sorhus&lt;/a&gt;가 만든 경량 HTTP 클라이언트 라이브러리로, fetch와 ESM을 기반으로 동작하며 사용성을 개선한 것이 특징이다. 기본적으로 JSON 변환, 타임아웃 처리, 간결한 API 인터페이스를 제공하여 번거로운 설정 없이도 효율적인 네트워크 요청을 수행할 수 있다. &amp;rArr; axios보다 훨씬 가볍다는 소리!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;axios vs fetch vs ky 비교&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;axios: 기능이 많지만 번들 사이즈가 큼&lt;/li&gt;
&lt;li&gt;fetch: 가볍지만 기본적인 기능만 제공&lt;/li&gt;
&lt;li&gt;ky: fetch의 장점(가벼움)을 가져오면서 편리한 기능들을 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ky 사용법&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용법은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ky를 설치하려면 다음 명령어를 실행한다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install ky
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ky는 요청 인터셉터, 재시도(retry) 기능, 헤더 설정 등을 지원하며, 필요에 따라 확장하여 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 요청 예제를 알아가보며 공부해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;GET 요청 보내기&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import ky from 'ky';

const fetchData = async () =&amp;gt; {
  try {
    const data = await ky.get('&amp;lt;https://jsonplaceholder.typicode.com/posts/1&amp;gt;').json();
    console.log(data);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
};

fetchData();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 .json()을 호출하면 response.json()을 자동으로 처리해준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;POST 요청 보내기&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import ky from 'ky';

const postData = async () =&amp;gt; {
  try {
    const response = await ky.post('&amp;lt;https://jsonplaceholder.typicode.com/posts&amp;gt;', {
      json: {
        title: 'foo',
        body: 'bar',
        userId: 1,
      },
    }).json();

    console.log(response);
  } catch (error) {
    console.error('Error posting data:', error);
  }
};

postData();
&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;json 옵션을 사용하면 자동으로 Content-Type: application/json 헤더가 추가된다.&lt;/li&gt;
&lt;li&gt;응답도 JSON 형태로 자동 변환해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 axios에서의 응답 처리와 비교해보면&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;import axios from 'axios';

const fetchData = async () =&amp;gt; {
  const response = await axios.get('&amp;lt;https://jsonplaceholder.typicode.com/posts/1&amp;gt;');
  console.log(response.data); // JSON으로 자동 변환된 데이터
};

fetchData();
&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;axios는 response.data에 JSON 데이터가 자동으로 변환되어 들어온다.&lt;/li&gt;
&lt;li&gt;즉, const data = response.data;를 사용해야 JSON을 추출할 수 있음.&lt;/li&gt;
&lt;/ul&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;라이브러리 JSON 변환 방식&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;ky&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;.json()을 직접 호출해야 JSON 변환됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;axios&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;응답 객체(response.data)에 JSON 변환된 데이터가 자동으로 들어감&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;토큰 저장 및 API 인스턴스 관리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 인증이 필요한 API를 호출할 때, &lt;b&gt;토큰을 헤더에 추가&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 매번 설정하지 않고, &lt;b&gt;ky 인스턴스를 만들어 관리&lt;/b&gt;하면 편리하다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import ky from 'ky';

const api = ky.create({
  prefixUrl: '&amp;lt;https://api.example.com&amp;gt;', // API 기본 URL 설정
  headers: {
    Authorization: `Bearer ${localStorage.getItem('token')}`, // 저장된 토큰 사용
    'Content-Type': 'application/json',
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 인스턴스를 재사용 하기 위해서는 이렇게 하면 된다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;const fetchUserData = async () =&amp;gt; {
  try {
    const data = await api.get('user/profile').json();
    console.log(data);
  } catch (error) {
    console.error('Error fetching user data:', error);
  }
};

fetchUserData();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ ky.create()를 사용하면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API의 &lt;b&gt;기본 URL&lt;/b&gt;을 설정할 수 있다.&lt;/li&gt;
&lt;li&gt;headers를 설정해서 &lt;b&gt;토큰을 자동으로 포함&lt;/b&gt;할 수 있다.&lt;/li&gt;
&lt;li&gt;필요하면 같은 설정을 공유하는 여러 개의 API 인스턴스를 만들 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;요청 인터셉터 (Request Hook)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 요청을 보낼 때 &lt;b&gt;토큰을 동적으로 설정하거나, 요청을 변형할 때&lt;/b&gt; 사용한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const api = ky.create({
  prefixUrl: '&amp;lt;https://api.example.com&amp;gt;',
  hooks: {
    beforeRequest: [
      (request) =&amp;gt; {
        const token = localStorage.getItem('token');
        if (token) {
          request.headers.set('Authorization', `Bearer ${token}`);
        }
      },
    ],
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ beforeRequest Hook을 사용하면&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;b&gt;토큰이 있을 때만 Authorization 헤더를 추가&lt;/b&gt;할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;응답 인터셉터 (Response Hook)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 응답을 받은 후, &lt;b&gt;에러 처리나 응답 변형&lt;/b&gt;을 할 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;const api = ky.create({
  prefixUrl: '&amp;lt;https://api.example.com&amp;gt;',
  hooks: {
    afterResponse: [
      async (request, options, response) =&amp;gt; {
        if (response.status === 401) {
          console.error('Unauthorized! Redirecting to login...');
          window.location.href = '/login';
        }
      },
    ],
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ afterResponse Hook을 사용하면&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;401 (Unauthorized) 발생 시, &lt;b&gt;자동으로 로그인 페이지로 리디렉트&lt;/b&gt;할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;재시도 (Retry) 기능&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 일시적으로 응답하지 않을 때, 자동으로 &lt;b&gt;재시도&lt;/b&gt;하게 만들 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const api = ky.create({
  prefixUrl: '&amp;lt;https://api.example.com&amp;gt;',
  retry: {
    limit: 3, // 최대 3번 재시도
    methods: ['get', 'post'], // GET과 POST 요청만 재시도
    statusCodes: [408, 500, 502, 503, 504], // 재시도할 HTTP 상태 코드
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ retry 옵션을 사용하면&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;b&gt;HTTP 상태 코드가 발생했을 때만 재시도&lt;/b&gt;할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;헤더 설정 (Headers)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 요청에 추가적인 헤더를 포함하고 싶을 때 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;const api = ky.create({
  prefixUrl: '&amp;lt;https://api.example.com&amp;gt;',
  headers: {
    'X-Custom-Header': 'MyCustomValue',
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청을 보낼 때마다 X-Custom-Header: MyCustomValue가 자동으로 추가된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;쿼리 파라미터 쉽게 추가하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ky는 URL에 &lt;b&gt;자동으로 쿼리 파라미터를 추가하는 기능&lt;/b&gt;을 제공한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const response = await ky.get('&amp;lt;https://api.example.com/users&amp;gt;', {
  searchParams: {
    page: 1,
    limit: 10,
  },
}).json();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ searchParams를 사용하면&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;URL을 직접 조작하지 않아도&lt;/b&gt; 쿼리 파라미터를 쉽게 추가할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://api.example.com/users?page=1&amp;amp;limit=10&quot;&gt;https://api.example.com/users?page=1&amp;amp;limit=10&lt;/a&gt; 처럼 자동 변환된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 방법&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;기본 GET 요청&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;ky.get(url).json();&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;POST 요청&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;ky.post(url, { json: { ... } }).json();&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;토큰 저장 &amp;amp; API 관리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;ky.create({ headers: { Authorization: 'Bearer ...' } })&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;요청 인터셉터&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;beforeRequest: [ (request) =&amp;gt; { ... } ]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;응답 인터셉터&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;afterResponse: [ (request, options, response) =&amp;gt; { ... } ]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;재시도 (Retry)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;retry: { limit: 3, statusCodes: [...] }&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;헤더 설정&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;headers: { 'X-Custom-Header': 'value' }&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;쿼리 파라미터 추가&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;searchParams: { key: value }&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론을 알았으니 실제로 도입해보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;ky를 사용한 토큰 관리 + 구글 로그인 구현하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구현하고자 하는 것은 ky를 사용한 토큰 관리 + 구글 로그인 구현이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꼭 소셜 로그인이 아니어도 로그인 후 토큰 받아오는 로직은 다른 로그인들도 동일하니까 중간은 생략하고 나머지를 참고하면 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 흐름은 이렇다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 구글 로그인 &amp;rarr; 토큰을 받아옴&lt;br /&gt;2. 토큰을 저장&lt;br /&gt;3. 이후 토큰을 포함하여 api 요청 (인스턴스 생성하기)&lt;br /&gt;4. retry로 토큰 재발급하기&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 기본 Url을 설정하기 위한 인스턴스를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 기본 인스턴스 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { API_CONFIG } from &quot;@/constants/config&quot;;
import ky from &quot;ky&quot;;

export const api = ky.create({
  prefixUrl: API_CONFIG.BASE_URL,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API_CONFIG에는 BASE_URL과 구글 로그인에 필요한 GOOGLE_REDIRECT_URI를 넣어두었다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export const API_CONFIG = {
  BASE_URL: import.meta.env.VITE_API_BASE_URL,
  GOOGLE_REDIRECT_URI: `${window.location.origin}/oauth/callback/google`,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Google OAuth 페이지로 리다이렉션을 도와주는 authservice를 만들기&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import { API_CONFIG } from &quot;@/constants/config&quot;;
import { api } from &quot;./api&quot;;

export interface GoogleLoginResponse {
  accessToken: string;
  refreshToken: string;
}

export const authService = {
  // ... 코드 생략

  initiateGoogleLogin() {
    const redirectUrl = `${API_CONFIG.BASE_URL}/oauth2/google?redirect_uri=${API_CONFIG.GOOGLE_REDIRECT_URI}`;
    window.location.href = redirectUrl;
  },
};

&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;initiateGoogleLogin 함수는 다음과 같은 URL을 생성한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;${API_CONFIG.BASE_URL}/oauth2/google?redirect_uri=${API_CONFIG.GOOGLE_REDIRECT_URI}&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;사용자는 이 URL로 리다이렉션되어 Google 로그인 페이지를 보게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Google 인증 후 콜백 처리를 위한 컴포넌트 제작&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { authService } from &quot;@/apis/auth.service.api&quot;;
import { useEffect, useRef } from &quot;react&quot;;
import { useNavigate } from &quot;react-router&quot;;

export const GoogleCallback = () =&amp;gt; {
  const navigate = useNavigate();
  const processedRef = useRef(false);

  useEffect(() =&amp;gt; {
    const handleGoogleCallback = async () =&amp;gt; {
      const searchParams = new URLSearchParams(window.location.search);
      const code = searchParams.get(&quot;code&quot;);

      if (code &amp;amp;&amp;amp; !processedRef.current) {
        processedRef.current = true;

        try {
          const { accessToken, refreshToken } = await authService.googleLogin(
            code
          );
          localStorage.setItem(&quot;accessToken&quot;, accessToken);
          localStorage.setItem(&quot;refreshToken&quot;, refreshToken);
          navigate(&quot;/&quot;);
        } catch (error) {
          console.error(&quot;Login failed:&quot;, error);
          navigate(&quot;/&quot;);
        }
      }
    };

    handleGoogleCallback();
  }, [navigate]);

  return &amp;lt;div&amp;gt;로그인 처리중&amp;lt;/div&amp;gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 Google에서 로그인을 완료하면, Google은 지정된 콜백 URL로 인증 코드와 함께 리다이렉션한다.&lt;/li&gt;
&lt;li&gt;GoogleCallback 컴포넌트가 마운트되면 useEffect가 실행된다.&lt;/li&gt;
&lt;li&gt;URL에서 인증 코드(code 파라미터)를 추출한다.&lt;/li&gt;
&lt;li&gt;processedRef를 사용하여 콜백이 한 번만 처리되도록 보장한다.&lt;/li&gt;
&lt;li&gt;&amp;rArr; 소셜 로그인에서 code를 추출해서 서버로 보내는 건 한 번만! 해야 된다. 따라서 stricemode가 켜져있거나 그런 상황에서는 오류가 발생할 수 있기 때문에 이렇게 ref를 사용해서 횟수를 감지하거나 useOnceEffect와 같은 한 번만 실행되는 useEffect를 사용하면 된다. useOnceEffect는 인터넷에 구현 코드가 많으니 찾으면 바로 나온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 백엔드 인증 처리 (auth.service.api.ts)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import { API_CONFIG } from &quot;@/constants/config&quot;;
import { api } from &quot;./api&quot;;

export interface GoogleLoginResponse {
  accessToken: string;
  refreshToken: string;
}

export const authService = {
// 아까 위에서 생략됐던 코드!
  async googleLogin(code: string): Promise&amp;lt;GoogleLoginResponse&amp;gt; {
    try {
      const response = await api
        .post(&quot;oauth2/login/google&quot;, {
          json: {
            authorizationCode: code,
          },
          throwHttpErrors: false,
        })
        .json&amp;lt;GoogleLoginResponse&amp;gt;();

      if (!response) {
        throw new Error(&quot;Login failed&quot;);
      }

      return response;
    } catch (error) {
      console.error(&quot;Google login error:&quot;, error);
      throw error;
    }
  },

  initiateGoogleLogin() {
    const redirectUrl = `${API_CONFIG.BASE_URL}/oauth2/google?redirect_uri=${API_CONFIG.GOOGLE_REDIRECT_URI}`;
    window.location.href = redirectUrl;
  },
};

&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;추출된 코드로 authService.googleLogin(code)를 호출한다.&lt;/li&gt;
&lt;li&gt;백엔드 API의 oauth2/login/google 엔드포인트로 POST 요청을 보낸다.&lt;/li&gt;
&lt;li&gt;&amp;rarr; 보내면 서버가 토큰을 발급해줌!&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. &lt;/b&gt;&lt;b&gt;인증 완료 및 토큰 저장&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { authService } from &quot;@/apis/auth.service.api&quot;;
import { useEffect, useRef } from &quot;react&quot;;
import { useNavigate } from &quot;react-router&quot;;

export const GoogleCallback = () =&amp;gt; {
  const navigate = useNavigate();
  const processedRef = useRef(false);

  useEffect(() =&amp;gt; {
    const handleGoogleCallback = async () =&amp;gt; {
      const searchParams = new URLSearchParams(window.location.search);
      const code = searchParams.get(&quot;code&quot;);

      if (code &amp;amp;&amp;amp; !processedRef.current) {
        processedRef.current = true;

        try {
          const { accessToken, refreshToken } = await authService.googleLogin(
            code
          );
          localStorage.setItem(&quot;accessToken&quot;, accessToken);
          localStorage.setItem(&quot;refreshToken&quot;, refreshToken);
          navigate(&quot;/&quot;);
        } catch (error) {
          console.error(&quot;Login failed:&quot;, error);
          navigate(&quot;/&quot;);
        }
      }
    };

    handleGoogleCallback();
  }, [navigate]);

  return &amp;lt;div&amp;gt;로그인 처리중&amp;lt;/div&amp;gt;;
};

&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;백엔드에서 성공적으로 응답을 받으면 accessToken과 refreshToken을 반환받는다.&lt;/li&gt;
&lt;li&gt;이 토큰들을 localStorage에 저정하면 구현 끝~&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 기본 소셜 로그인 구현 로직이라 이 외의 로그인을 구현하고 있다면 토큰 받아온 이후부터 밑에 부분을 참고하면 좋을 것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Retry로 access token 재발급 로직 구현하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 말했듯이 ky는 &lt;b&gt;beforeRetry&lt;/b&gt; 훅을 사용해 &lt;b&gt;API fetch retry&lt;/b&gt;시 처리할 동작을 구현하는 것이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현하고자 하는 상황은 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. status code가 401이 아닌 경우 retry를 중지한다.&lt;br /&gt;2. access token 가져오는 것을 2번 이상 실패한다면 토큰 만료인 경우로 판단하여 로그아웃 처리를 한다. (token 삭제 등)&lt;br /&gt;3. 1번과 2번 경우가 아닐 때는 refresh token을 사용해 access token을 가져오고 저장한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 먼저 토큰을 관리하는 service를 만들어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. tokenService 만들기&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;moonscript&quot;&gt;&lt;code&gt;export const tokenService = {
  getAccessToken: () =&amp;gt; localStorage.getItem(&quot;accessToken&quot;),

  getRefreshToken: () =&amp;gt; localStorage.getItem(&quot;refreshToken&quot;),

  setTokens: (accessToken: string, refreshToken: string) =&amp;gt; {
    localStorage.setItem(&quot;accessToken&quot;, accessToken);
    localStorage.setItem(&quot;refreshToken&quot;, refreshToken);
  },

  clearTokens: () =&amp;gt; {
    localStorage.removeItem(&quot;accessToken&quot;);
    localStorage.removeItem(&quot;refreshToken&quot;);
  },

  hasTokens: () =&amp;gt; {
    const accessToken = localStorage.getItem(&quot;accessToken&quot;);
    const refreshToken = localStorage.getItem(&quot;refreshToken&quot;);
    return !!(accessToken &amp;amp;&amp;amp; refreshToken);
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 토큰을 관리하는 service를 만들었으니, 토큰 재발급 로직을 구현해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 토큰을 재발급 하는 로직 만들기&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;const DEFAULT_API_RETRY_LIMIT = 2;

const handleTokenRefresh: BeforeRetryHook = async ({ error, retryCount }) =&amp;gt; {
  const httpError = error as HTTPError;

  if (httpError.response.status !== 401) {
    return ky.stop;
  }

  if (retryCount === DEFAULT_API_RETRY_LIMIT - 1) {
    authService.logout();
    return ky.stop;
  }

  try {
    const refreshToken = tokenService.getRefreshToken();
    if (!refreshToken) {
      throw new Error(&quot;refreshToken이 없음&quot;);
    }
    await authService.refreshToken(refreshToken);
  } catch (error) {
    console.error(&quot;Token refresh 실패, 로그아웃&quot;, error);
    authService.logout();
    return ky.stop;
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만든 handleTokenRefresh를 ky 인스턴스에 등록하면 된다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;export const api = ky.create({
  prefixUrl: API_CONFIG.BASE_URL,
  retry: {
    limit: DEFAULT_API_RETRY_LIMIT,
  },
  hooks: {
    beforeRequest: [setAuthHeader],
    beforeRetry: [handleTokenRefresh],
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 토큰이 만료되었을 때 자동으로 재발급을 시도하고, 실패하면 로그아웃되는 로직이 구현되었다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구현한 auth 로직은 다음과 같은 특징을 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;401 에러 발생시 자동으로 토큰을 재발급한다.&lt;/li&gt;
&lt;li&gt;토큰 재발급 실패시 자동으로 로그아웃된다.&lt;/li&gt;
&lt;li&gt;API 요청시 자동으로 토큰이 헤더에 포함된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이 인스턴스를 사용해서 안전하게 API를 호출할 수 있다 ㅎㅎ&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;로그아웃 구현하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그아웃은 간단하다. 토큰을 삭제하고 홈으로 리다이렉트하면 된다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;logout: async () =&amp;gt; {
  try {
    await api.post(&quot;logout&quot;, {
      json: { refreshToken: `${tokenService.getAccessToken()}` },
    });
  } catch (error) {
    console.error(&quot;Logout failed:&quot;, error);
  } finally {
    tokenService.clearTokens();
    window.location.href = &quot;/&quot;;
  }
},
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구현한 auth 로직을 실제로 사용해보자. 네비게이션 바에 로그인/로그아웃 버튼을 추가해보자.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { authService } from &quot;@/apis/auth.service.api&quot;;
import { tokenService } from &quot;@/apis/token.service&quot;;
import { StyledContainer, StyledProfileImage } from &quot;./Navigation.style&quot;;

const Navigation = () =&amp;gt; {
  const handleClick = () =&amp;gt; {
    if (tokenService.hasTokens()) {
      authService.logout();
    } else {
      authService.initiateGoogleLogin();
    }
  };

  return (
    &amp;lt;StyledContainer&amp;gt;
      &amp;lt;StyledProfileImage onClick={handleClick}&amp;gt;
        &amp;lt;p&amp;gt;{tokenService.hasTokens() ? &quot;로그인됨&quot; : &quot;로그인필요&quot;}&amp;lt;/p&amp;gt;
      &amp;lt;/StyledProfileImage&amp;gt;
    &amp;lt;/StyledContainer&amp;gt;
  );
};

export default Navigation;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰이 있으면 &quot;로그인됨&quot;이 표시되고 클릭하면 로그아웃되며, 토큰이 없으면 &quot;로그인필요&quot;가 표시되고 클릭하면 Google OAuth 페이지로 리다이렉트된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결과 화면&lt;/b&gt;&lt;/h3&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/452993977&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/IBgpR/hyYfQRv1Jl/dVeP9v2q0LKU3DvDL7xPR0/img.jpg?width=1814&amp;amp;height=1080&amp;amp;face=0_0_1814_1080,https://scrap.kakaocdn.net/dn/b4xfr9/hyYf1eqYCL/juz7xuwNug728tfOSODeEK/img.jpg?width=1814&amp;amp;height=1080&amp;amp;face=0_0_1814_1080&quot; data-video-width=&quot;860&quot; data-video-height=&quot;512&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;512&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/452993977?service=daum_tistory&quot; width=&quot;860&quot; height=&quot;512&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글 로그인과 토큰 저장 로직이 잘 작동하는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ky 괜찮은 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재밌다~~ axios랑 비슷해서 넘어오기 부담스럽지 않을 듯 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 코드는 밑에서 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/yourssu/Yourssu-Scouter-Frontend/pull/9&quot;&gt;https://github.com/yourssu/Yourssu-Scouter-Frontend/pull/9&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739434128524&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;feat: 구글 로그인 및 토큰 인증 서비스 구현 by ssolfa &amp;middot; Pull Request #9 &amp;middot; yourssu/Yourssu-Scouter-Frontend&quot; data-og-description=&quot;1️⃣ 어떤 작업을 했나요? (Summary) resolved feat: 구글 로그인 구현&amp;nbsp;#8 2025-02-13.3.06.03.mov API 통신을 위한 ky 인스턴스 구현 ky의 prefixUrl, retry, hooks 옵션을 활용하여 base URL 설정, 재시도 로직,...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/yourssu/Yourssu-Scouter-Frontend/pull/9&quot; data-og-url=&quot;https://github.com/yourssu/Yourssu-Scouter-Frontend/pull/9&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/babXIV/hyYcd1X0Qc/G60qzhdUY2opbnsnqm9rp0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/37J7Y/hyYad8LPEg/Lq0MwyZ1y569AT6sM1mPPK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/yourssu/Yourssu-Scouter-Frontend/pull/9&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/yourssu/Yourssu-Scouter-Frontend/pull/9&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/babXIV/hyYcd1X0Qc/G60qzhdUY2opbnsnqm9rp0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/37J7Y/hyYad8LPEg/Lq0MwyZ1y569AT6sM1mPPK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;feat: 구글 로그인 및 토큰 인증 서비스 구현 by ssolfa &amp;middot; Pull Request #9 &amp;middot; yourssu/Yourssu-Scouter-Frontend&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1️⃣ 어떤 작업을 했나요? (Summary) resolved feat: 구글 로그인 구현&amp;nbsp;#8 2025-02-13.3.06.03.mov API 통신을 위한 ky 인스턴스 구현 ky의 prefixUrl, retry, hooks 옵션을 활용하여 base URL 설정, 재시도 로직,...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&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>React</category>
      <category>api인터셉터</category>
      <category>axios</category>
      <category>fetch</category>
      <category>HTTP</category>
      <category>KY</category>
      <category>구글로그인</category>
      <category>소셜로그인</category>
      <category>토큰관리</category>
      <category>토큰저장</category>
      <author>solfa</author>
      <guid isPermaLink="true">https://5ffthewall.tistory.com/133</guid>
      <comments>https://5ffthewall.tistory.com/133#entry133comment</comments>
      <pubDate>Thu, 13 Feb 2025 17:11:35 +0900</pubDate>
    </item>
  </channel>
</rss>