<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>free islet</title>
    <link>https://freeislet.tistory.com/</link>
    <description>게임 개발(Engine/Client), 웹 개발(BE/FE), 데이터 엔지니어</description>
    <language>ko</language>
    <pubDate>Fri, 7 Aug 2026 16:00:26 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>깨비아빠0</managingEditor>
    <image>
      <title>free islet</title>
      <url>https://tistory1.daumcdn.net/tistory/5800312/attach/17be4e1228c8443582d855376dc8ab3a</url>
      <link>https://freeislet.tistory.com</link>
    </image>
    <item>
      <title>Gemini API Playground (Next.js)</title>
      <link>https://freeislet.tistory.com/54</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://freeislet.tistory.com/51&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Gemini API 테스트&lt;/a&gt;에서 테스트한 Gemini API를 &lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://freeislet.tistory.com/50&quot;&gt;Next.js 홈페이지 만들기&lt;/a&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;3024&quot; data-origin-height=&quot;1522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVCinj/btsEvdGbnBL/rZyldxtdxhxDzoJklCx6xK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVCinj/btsEvdGbnBL/rZyldxtdxhxDzoJklCx6xK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVCinj/btsEvdGbnBL/rZyldxtdxhxDzoJklCx6xK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVCinj%2FbtsEvdGbnBL%2FrZyldxtdxhxDzoJklCx6xK%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;800&quot; height=&quot;403&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1522&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;h2 data-ke-size=&quot;size26&quot;&gt;Vercel AI SDK 연동&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sdk.vercel.ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Vercel AI SDK&lt;/a&gt;에서 제공하는 useChat 훅을 이용하면 Gemini API 테스트 시 직접 구현했던 message 히스토리 관리를 편하게 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 &lt;a href=&quot;https://sdk.vercel.ai/docs/guides/providers/google&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Google 연동 매뉴얼&lt;/a&gt;을 따라서 간단하게 gemini 챗봇을 연동해 본 화면과 주요 소스코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;pre id=&quot;code_1707105512130&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client'

import { useChat } from 'ai/react'

export default function GeminiChatbotPage() {
  const { messages, input, handleInputChange, handleSubmit } = useChat({ api: '/api/gemini/chat' })

  return (
    &amp;lt;div&amp;gt;
      {messages.map((m) =&amp;gt; (
        &amp;lt;div key={m.id}&amp;gt;
          {m.role === 'user' ? 'User: ' : 'AI: '}
          {m.content}
        &amp;lt;/div&amp;gt;
      ))}

      &amp;lt;form onSubmit={handleSubmit}&amp;gt;
        &amp;lt;input value={input} placeholder=&quot;Say something...&quot; onChange={handleInputChange} /&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;Next.js Chatbot 템플릿 검토&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://vercel.com/templates/next.js/nextjs-ai-chatbot&quot;&gt;Next.js Chatbot 템플릿&lt;/a&gt;을 검토해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 데모 페이지를 살펴보았는데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://chat.vercel.ai/&quot;&gt;Chatbot Demo&lt;/a&gt;의 모습은 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2228&quot; data-origin-height=&quot;1138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cU6jBo/btsCzD1ZS1i/D0ue8osh1xlCRpRqfG7ZGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cU6jBo/btsCzD1ZS1i/D0ue8osh1xlCRpRqfG7ZGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cU6jBo/btsCzD1ZS1i/D0ue8osh1xlCRpRqfG7ZGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcU6jBo%2FbtsCzD1ZS1i%2FD0ue8osh1xlCRpRqfG7ZGk%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;2228&quot; height=&quot;1138&quot; data-origin-width=&quot;2228&quot; data-origin-height=&quot;1138&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2228&quot; data-origin-height=&quot;1134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxtbyG/btsCyIW0Koe/VR1K4hN8aagBfbHe0nQNOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxtbyG/btsCyIW0Koe/VR1K4hN8aagBfbHe0nQNOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxtbyG/btsCyIW0Koe/VR1K4hN8aagBfbHe0nQNOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxtbyG%2FbtsCyIW0Koe%2FVR1K4hN8aagBfbHe0nQNOk%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;2228&quot; height=&quot;1134&quot; data-origin-width=&quot;2228&quot; data-origin-height=&quot;1134&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 style=&quot;color: #0070d1;&quot; href=&quot;https://github.com/vercel/ai-chatbot&quot;&gt;github 페이지&lt;/a&gt;에서 코드를 살펴보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/vercel/ai-chatbot/blob/main/components/chat.tsx&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Chat&lt;/a&gt;, &lt;a href=&quot;https://github.com/vercel/ai-chatbot/blob/main/components/chat-list.tsx&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ChatList&lt;/a&gt;, &lt;a href=&quot;https://github.com/vercel/ai-chatbot/blob/main/components/chat-message.tsx&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ChatMessage&lt;/a&gt; 등의 컴포넌트를 통해 주요 챗봇 UI가 구현되었고,&amp;nbsp;Vercel AI SDK, &lt;a href=&quot;https://github.com/remarkjs/react-markdown&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;react-markdown&lt;/a&gt;, &lt;a href=&quot;https://github.com/react-syntax-highlighter/react-syntax-highlighter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;react-syntax-highlighter&lt;/a&gt; 등의 패키지를 사용하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 코드들을 불필요한 내용만 정리하여 &lt;a href=&quot;https://github.com/freeislet/home/blob/main/src/components/chat.tsx&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Chat 컴포넌트&lt;/a&gt;&amp;nbsp;등으로 옮겨 왔다.&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;/h2&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;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>웹 개발</category>
      <category>GEMINI</category>
      <category>Gemini API</category>
      <category>Gemini Pro</category>
      <category>Next.js Chatbot</category>
      <category>react-markdown</category>
      <category>react-syntax-highlighter</category>
      <category>Vercel AI SDK</category>
      <author>깨비아빠0</author>
      <guid isPermaLink="true">https://freeislet.tistory.com/54</guid>
      <comments>https://freeislet.tistory.com/54#entry54comment</comments>
      <pubDate>Tue, 6 Feb 2024 14:43:02 +0900</pubDate>
    </item>
    <item>
      <title>SVG path 스케일링 (for responsive object-path)</title>
      <link>https://freeislet.tistory.com/60</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;914&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uTEbs/btsEbDeDINH/z2YhDmIimjlTyXYzXrHq40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uTEbs/btsEbDeDINH/z2YhDmIimjlTyXYzXrHq40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uTEbs/btsEbDeDINH/z2YhDmIimjlTyXYzXrHq40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuTEbs%2FbtsEbDeDINH%2Fz2YhDmIimjlTyXYzXrHq40%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;1516&quot; height=&quot;914&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;914&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;h2 data-ke-size=&quot;size26&quot;&gt;CSS path 애니메이션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS에서 offset-path와 offset-distance 속성을 사용하여 path 애니메이션을 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;offset-path 속성은 SVG path 엘리먼트의 d 속성에 사용하는 path 문자열을 path 함수에 넣어 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;offset-distance 속성은 &quot;0%&quot;, &quot;100%&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;다음 코드는&amp;nbsp;&lt;a href=&quot;https://www.framer.com/motion/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Framer motion&lt;/a&gt; 라이브러리를 통해 offset-distance를 점차 증가시켜서 path 애니메이션을 만드는 간략한 예시이다.&lt;/p&gt;
&lt;pre id=&quot;code_1706666932586&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;motion.div
  style={{
    offsetPath: path(
      &quot;M 239 17 C 142 17 48.5 103 48.5 213.5 C 48.5 324 126 408 244 408 ...&quot;
    );
  }}
  initial={{ offsetDistance: &quot;0%&quot; }}
  animate={{ offsetDistance: &quot;100%&quot; }}
/&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;이런 방식으로 만든 path 애니메이션 예제를 code sandbox에서 찾을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;iframe src=&quot;https://codesandbox.io/embed/41i3v?view=preview&amp;amp;module=%2Fsrc%2FApp.js&quot; width=&quot;100%&quot; height=&quot;600&quot;&gt;&lt;/iframe&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;offset-path 스케일링 이슈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;offset-path를 설정할 때 스케일을 지정할 수 없는 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서 보면 애니메이션 되는 크기가 고정되어 있으며, 크기를 바꾸려 해도 (그려지는 SVG path의 크기는 바꿀 수 있지만) 애니메이션 경로의 크기는 변경할 수 없다.&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;path 스케일링 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;offset-path 스케일링이 불가능한 문제를 해결하기 위해 path 문자열을 직접 파싱하여 스케일링하는 코드를 &lt;a href=&quot;https://stackoverflow.com/a/75060752&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;stack overflow&lt;/a&gt;에서 찾을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 정리하여 PathScaler 클래스를 만들었으며, 다음과 같이 사용하도록 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1706668520739&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const pathScaler = new PathScaler(path, {원래 width}, {원래 height});
const scaledPath = pathScaler.getScaledPath({변환할 width}, {변환할 height})&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;PathScaler 클래스의 전체 코드는 &lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://github.com/freeislet/home/blob/d1c142fe5629c4c6daf912fddcfba746aec4fa59/src/lib/path-scaler.ts&quot;&gt;github&lt;/a&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;Responsive path 애니메이션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PathScaler를 활용하여 앞서 발견한 예제를 responsive하게 수정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 창 크기에 맞춰서 SVG path와 흰 상자의 애니메이션 경로(offset-path)가 스케일링된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(전체 코드는 &lt;a href=&quot;https://codesandbox.io/p/sandbox/framer-motion-motion-along-a-path-forked-responsive-object-path-scaling-7k8k56?file=%2Fsrc%2FApp.js%3A33%2C30&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;code sandbox&lt;/a&gt;에서 확인할 수 있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;iframe src=&quot;https://codesandbox.io/embed/7k8k56?view=preview&amp;amp;module=%2Fsrc%2FApp.js&quot; width=&quot;100%&quot; height=&quot;600&quot;&gt;&lt;/iframe&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;다음은 실제 홈페이지에서 path 애니메이션을 사용한 모습이다.&lt;br /&gt;첫 화면에서 스크롤을 내리면, 비행기가 경로를 따라 스크롤 위치만큼 이동하는 모습을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;iframe src=&quot;https://freeislet.vercel.app&quot; width=&quot;100%&quot; height=&quot;600&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;</description>
      <category>웹 개발</category>
      <category>framer motion</category>
      <category>offset-distance</category>
      <category>offset-path</category>
      <category>path animation</category>
      <category>path scaling</category>
      <category>react</category>
      <category>responsive offset-path</category>
      <author>깨비아빠0</author>
      <guid isPermaLink="true">https://freeislet.tistory.com/60</guid>
      <comments>https://freeislet.tistory.com/60#entry60comment</comments>
      <pubDate>Wed, 31 Jan 2024 11:47:37 +0900</pubDate>
    </item>
    <item>
      <title>Lucia 인증 연동 (Next.js, PlanetScale, Kysely, OAuth)</title>
      <link>https://freeislet.tistory.com/59</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #3d62ce; text-align: start;&quot; href=&quot;https://freeislet.tistory.com/55&quot;&gt;Next.js 프로젝트 스택 검토 (2023)&lt;/a&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;대로 홈페이지를 다시 만들기 위한 세 번째 단계로&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Lucia&lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;를 사용하여&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;인증을&amp;nbsp;&lt;/span&gt;&lt;/span&gt;구현했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHMSqW/btsDkAPtW32/WMKbZEYioQuNznTp4r72w1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHMSqW/btsDkAPtW32/WMKbZEYioQuNznTp4r72w1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHMSqW/btsDkAPtW32/WMKbZEYioQuNznTp4r72w1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHMSqW%2FbtsDkAPtW32%2FWMKbZEYioQuNznTp4r72w1%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;700&quot; height=&quot;300&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1294&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;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;GitHub OAuth App 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub OAuth 연동을 위해 &lt;a href=&quot;https://github.com/settings/developers&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/settings/developers&lt;/a&gt;에서 OAuth app을 생성한다. (&lt;a href=&quot;https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Create a GitHub OAuth app&lt;/a&gt; 참고)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2554&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpyyHZ/btsDiRjYTlH/szN7QuR1gdqBMyZMvejlyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpyyHZ/btsDiRjYTlH/szN7QuR1gdqBMyZMvejlyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpyyHZ/btsDiRjYTlH/szN7QuR1gdqBMyZMvejlyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpyyHZ%2FbtsDiRjYTlH%2FszN7QuR1gdqBMyZMvejlyk%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;700&quot; height=&quot;155&quot; data-origin-width=&quot;2554&quot; data-origin-height=&quot;564&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;Application name, Homepage URL을 적절히 채우고, Authorization callback URL에는 &quot;http://localhost:3000/login/github/callback&quot;을 입력한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app의 Client ID와 Secret을 다음과 같이 .env.local에 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1704869174307&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GITHUB_CLIENT_ID=&quot;...&quot;
GITHUB_CLIENT_SECRET=&quot;...&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;참고로 방금 생성한 app은 로컬 개발 시 사용하고, 웹 배포에 사용할 app은 별도로 생성하여 URL을 주소에 맞게 입력해야 한다. Client ID와 Secret도 배포할 플랫폼의 환경변수로 따로 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;DB 테이블 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증에 필요한 테이블들을 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://freeislet.tistory.com/57&quot;&gt;PlanetScale + Kysely 데이터베이스 연동 (Next.js)&lt;/a&gt;에서 연동한 PlanetScale에 접속하여 다음 쿼리를 실행하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1704866958966&quot; class=&quot;typescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;CREATE TABLE auth_user (
    id VARCHAR(15) NOT NULL PRIMARY KEY,
    username VARCHAR(255) NOT NULL, # optional
    INDEX username (username)
);

CREATE TABLE user_key (
    id VARCHAR(255) NOT NULL PRIMARY KEY,
    user_id VARCHAR(15) NOT NULL,
    hashed_password VARCHAR(255),
    FOREIGN KEY (user_id) REFERENCES auth_user(id)
);

CREATE TABLE user_session (
    id VARCHAR(127) NOT NULL PRIMARY KEY,
    user_id VARCHAR(15) NOT NULL,
    active_expires BIGINT UNSIGNED NOT NULL,
    idle_expires BIGINT UNSIGNED NOT NULL,
    FOREIGN KEY (user_id) REFERENCES auth_user(id)
);&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://lucia-auth.com/database-adapters/planetscale-serverless/#mysql-schema&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://lucia-auth.com/database-adapters/planetscale-serverless/#mysql-schema&lt;/a&gt;,&lt;br /&gt;&lt;a href=&quot;https://lucia-auth.com/database-adapters/mysql2/#mysql-schema&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://lucia-auth.com/database-adapters/mysql2/#mysql-schema&lt;/a&gt; 참고&lt;/blockquote&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;auth_user 테이블은 id 컬럼만 필수이고, 추가로 필요한 사용자 정보를 임의로 추가할 수 있다.&lt;/li&gt;
&lt;li&gt;FOREIGN KEY는 필수는 아니다. PlanetScale은 현재 FOREIGN KEY가 베타 중이므로, FOREIGN KEY를 사용하려면 설정에서 활성화시켜야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;패키지 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 명령으로 패키지를 설치한다. PlanetScale DB와 OAuth를 위한 추가 패키지도 함께 설치한다.&lt;/p&gt;
&lt;pre id=&quot;code_1704866958972&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;npm install lucia @lucia-auth/adapter-mysql @lucia-auth/oauth&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인증 모듈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;auth/lucia.ts 파일을 추가한다. 아래 코드는 다음 내용을 포함하고 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;lucia 모듈 (auth)&lt;/li&gt;
&lt;li&gt;github OAuth 인증 모듈 (githubAuth)&lt;/li&gt;
&lt;li&gt;session 조회 도움 함수 (getPageSession)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1704868416168&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { lucia } from &quot;lucia&quot;
import { nextjs_future } from &quot;lucia/middleware&quot;
import { github } from &quot;@lucia-auth/oauth/providers&quot;
import { planetscale } from &quot;@lucia-auth/adapter-mysql&quot;
import { connect } from &quot;@planetscale/database&quot;
import { dbConfig } from &quot;../db/kysely&quot;
import { cache } from &quot;react&quot;
import * as context from &quot;next/headers&quot;

export const auth = lucia({
  adapter: planetscale(
    connect(dbConfig),
    {
      user: &quot;auth_user&quot;,
      key: &quot;user_key&quot;,
      session: &quot;user_session&quot;,
    },
  ),
  middleware: nextjs_future(),
  env: process.env.NODE_ENV === &quot;development&quot; ? &quot;DEV&quot; : &quot;PROD&quot;,
  sessionCookie: {
    expires: false,
  },

  getUserAttributes: (data) =&amp;gt; {
    return {
      githubUsername: data.username
    }
  },
})

export type Auth = typeof auth

export const githubAuth = github(
  auth,
  {
    clientId: process.env.GITHUB_CLIENT_ID ?? &quot;&quot;,
    clientSecret: process.env.GITHUB_CLIENT_SECRET ?? &quot;&quot;,
    scope: undefined,
  }
)

export const getPageSession = cache(() =&amp;gt; {
  const authRequest = auth.handleRequest(&quot;GET&quot;, context)
  return authRequest.validate()
})&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;PlanetScale connect에 사용된 dbConfig는 DB 접속 정보를 담고 있는 object로서, Kysely 연동 코드에서 가져와서 사용한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Sign in 페이지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app/login/page.tsx 파일에 Sign in 페이지 코드를 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 로그인된 경우에는 root로 redirect시키고, 아니면 GitHub Sign in 링크를 보여준다.&lt;/p&gt;
&lt;pre id=&quot;code_1704869298680&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { getPageSession } from &quot;@/auth/lucia&quot;
import { redirect } from &quot;next/navigation&quot;

const Page = async () =&amp;gt; {
  const session = await getPageSession()
  if (session) redirect(&quot;/&quot;)
  
  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;Sign in&amp;lt;/h1&amp;gt;
      &amp;lt;a href=&quot;/login/github&quot;&amp;gt;Sign in with GitHub&amp;lt;/a&amp;gt;
    &amp;lt;/&amp;gt;
  )
}

export default Page&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인증 handler&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app/login/github/route.ts에 GitHub 인증 route handler를 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/login/github GET 요청 시 GitHub 인증 URL이 생성되어 사용자 인증에 사용된다.&lt;/p&gt;
&lt;pre id=&quot;code_1704869999506&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { githubAuth } from &quot;@/auth/lucia&quot;
import * as context from &quot;next/headers&quot;
import type { NextRequest } from &quot;next/server&quot;

export const GET = async (request: NextRequest) =&amp;gt; {
  const [url, state] = await githubAuth.getAuthorizationUrl()

  // store state
  context.cookies().set(&quot;github_oauth_state&quot;, state, {
    httpOnly: true,
    secure: process.env.NODE_ENV === &quot;production&quot;,
    path: &quot;/&quot;,
    maxAge: 60 * 60
  })

  return new Response(null, {
    status: 302,
    headers: {
      Location: url.toString()
    }
  })
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인증 callback&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app/login/github/callback/route.ts에 인증 callback 구현을 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 결과가 전달되며, 인증 성공 시 새로운 세션이 만들어진다.&lt;/p&gt;
&lt;pre id=&quot;code_1704870358508&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { auth, githubAuth } from &quot;@/auth/lucia&quot;
import { OAuthRequestError } from &quot;@lucia-auth/oauth&quot;
import { cookies, headers } from &quot;next/headers&quot;
import type { NextRequest } from &quot;next/server&quot;

export const GET = async (request: NextRequest) =&amp;gt; {
  const storedState = cookies().get(&quot;github_oauth_state&quot;)?.value
  const url = new URL(request.url)
  const state = url.searchParams.get(&quot;state&quot;)
  const code = url.searchParams.get(&quot;code&quot;)

  // validate state
  if (!storedState || !state || storedState !== state || !code) {
    return new Response(null, {
      status: 400
    })
  }
  
  try {
    const { getExistingUser, githubUser, createUser } =
      await githubAuth.validateCallback(code)

    const getUser = async () =&amp;gt; {
      const existingUser = await getExistingUser()
      if (existingUser) return existingUser
      const user = await createUser({
        attributes: {
          username: githubUser.login
        }
      })
      return user
    }

    const user = await getUser()
    const session = await auth.createSession({
      userId: user.userId,
      attributes: {}
    })
    const authRequest = auth.handleRequest(request.method, {
      cookies,
      headers
    })
    authRequest.setSession(session)

    return new Response(null, {
      status: 302,
      headers: {
        Location: &quot;/&quot; // redirect to profile page
      }
    })
  } catch (e) {
    if (e instanceof OAuthRequestError) {
      // invalid code
      return new Response(null, {
        status: 400
      })
    }
    
    return new Response(null, {
      status: 500
    })
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로필 표시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 페이지(app/page.tsx)에 사용자 정보를 표시하도록 다음과 같이 수정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 정보가 없는 경우 로그인 페이지(/login)로 redirect시킨다.&lt;/p&gt;
&lt;pre id=&quot;code_1704870620147&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { getPageSession } from &quot;@/auth/lucia&quot;
import { redirect } from &quot;next/navigation&quot;
import Form from &quot;@/components/form&quot;

...

  return (
    ...
        &amp;lt;h1&amp;gt;Profile&amp;lt;/h1&amp;gt;
        &amp;lt;p&amp;gt;User id: {session.user.userId}&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;Username: {session.user.githubUsername}&amp;lt;/p&amp;gt;
        &amp;lt;Form action=&quot;/api/logout&quot;&amp;gt;
          &amp;lt;input type=&quot;submit&quot; value=&quot;Sign out&quot; /&amp;gt;
        &amp;lt;/Form&amp;gt;
  ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Form 컴포넌트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redirect를 manual 방식으로 처리하는 Form 클라이언트 컴포넌트를 추가한다. (components/form.tsx)&lt;/p&gt;
&lt;pre id=&quot;code_1704870735901&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;use client&quot;

import { useRouter } from &quot;next/navigation&quot;

const Form = ({
  children,
  action
}: {
  children: React.ReactNode
  action: string
}) =&amp;gt; {
  const router = useRouter()
  
  return (
    &amp;lt;form
      action={action}
      method=&quot;post&quot;
      onSubmit={async (e) =&amp;gt; {
        e.preventDefault()
        const formData = new FormData(e.currentTarget)
        const response = await fetch(action, {
          method: &quot;POST&quot;,
          body: formData,
          redirect: &quot;manual&quot;
        })

        if (response.status === 0) {
          // redirected
          // when using `redirect: &quot;manual&quot;`, response status 0 is returned
          return router.refresh()
        }
      }}
    &amp;gt;
      {children}
    &amp;lt;/form&amp;gt;
  )
}

export default Form&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Sign out 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app/api/logout/route.ts 파일을 추가하여 api/logout 요청을 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sign out을 위해서 Auth.invalidateSession(sessionId) 및 AuthRequest.setSession(null) 함수를 호출한다.&lt;/p&gt;
&lt;pre id=&quot;code_1704871057199&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { auth } from &quot;@/auth/lucia&quot;
import * as context from &quot;next/headers&quot;
import type { NextRequest } from &quot;next/server&quot;

export const POST = async (request: NextRequest) =&amp;gt; {
  const authRequest = auth.handleRequest(request.method, context)

  // check if user is authenticated
  const session = await authRequest.validate()
  if (!session) {
    return new Response(null, {
      status: 401
    })
  }

  // make sure to invalidate the current session!
  await auth.invalidateSession(session.sessionId)

  // delete session cookie
  authRequest.setSession(null)
  
  return new Response(null, {
    status: 302,
    headers: {
      Location: &quot;/login&quot; // redirect to login page
    }
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인증 연동 완료&lt;/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;사이트 접속 시 세션 확인, 없으면 Sign in 페이지 이동&lt;/li&gt;
&lt;li&gt;Sign in 버튼으로 GitHub 인증 페이지 이동&lt;/li&gt;
&lt;li&gt;메인 페이지에 사용자 정보 표시&lt;/li&gt;
&lt;li&gt;Sign Out 기능&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 인증 완료 후 사용자정보와 Sign Out 링크가 표시된 모습이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1146&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4IpvX/btsDhU87w8J/Ph0oAC0JIfkHfhOgxKPN20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4IpvX/btsDhU87w8J/Ph0oAC0JIfkHfhOgxKPN20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4IpvX/btsDhU87w8J/Ph0oAC0JIfkHfhOgxKPN20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4IpvX%2FbtsDhU87w8J%2FPh0oAC0JIfkHfhOgxKPN20%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;500&quot; height=&quot;159&quot; data-origin-width=&quot;1146&quot; data-origin-height=&quot;364&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://freeislet.vercel.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://freeislet.vercel.app/&lt;/a&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://lucia-auth.com/guidebook/github-oauth/nextjs-app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://lucia-auth.com/guidebook/github-oauth/nextjs-app/&lt;/a&gt; (Next.js App Router OAuth 연동 가이드)&lt;/p&gt;
&lt;figure id=&quot;og_1704867026901&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;GitHub OAuth in Next.js App Router&quot; data-og-description=&quot;Learn the basic of Lucia and the OAuth integration by implementing GitHub OAuth&quot; data-og-host=&quot;lucia-auth.com&quot; data-og-source-url=&quot;https://lucia-auth.com/guidebook/github-oauth/nextjs-app/&quot; data-og-url=&quot;https://lucia-auth.com/guidebook/github-oauth/nextjs-app/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/vniet/hyU2nlsFNj/KUkNemB24T77mAkj4lUZ11/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://lucia-auth.com/guidebook/github-oauth/nextjs-app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://lucia-auth.com/guidebook/github-oauth/nextjs-app/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/vniet/hyU2nlsFNj/KUkNemB24T77mAkj4lUZ11/img.jpg?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;GitHub OAuth in Next.js App Router&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Learn the basic of Lucia and the OAuth integration by implementing GitHub OAuth&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;lucia-auth.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://lucia-auth.com/database-adapters/planetscale-serverless/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://lucia-auth.com/database-adapters/planetscale-serverless/&lt;/a&gt; (PlanetScale 연동)&lt;/p&gt;
&lt;figure id=&quot;og_1704867070832&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;PlanetScale serverless adapter&quot; data-og-description=&quot;Learn how to use the PlanetScale serverless driver with Lucia&quot; data-og-host=&quot;lucia-auth.com&quot; data-og-source-url=&quot;https://lucia-auth.com/database-adapters/planetscale-serverless/&quot; data-og-url=&quot;https://lucia-auth.com/database-adapters/planetscale-serverless/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pA0x4/hyU2gNqiZC/fsoKAPnrJpL5KYutMiG7HK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://lucia-auth.com/database-adapters/planetscale-serverless/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://lucia-auth.com/database-adapters/planetscale-serverless/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pA0x4/hyU2gNqiZC/fsoKAPnrJpL5KYutMiG7HK/img.jpg?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;PlanetScale serverless adapter&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Learn how to use the PlanetScale serverless driver with Lucia&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;lucia-auth.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://lucia-auth.com/guidebook/kysely/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://lucia-auth.com/guidebook/kysely/&lt;/a&gt; (Kysely 연동)&lt;/p&gt;
&lt;figure id=&quot;og_1704867102152&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;Using Kysely&quot; data-og-description=&quot;Learn how to use Kysely with Lucia&quot; data-og-host=&quot;lucia-auth.com&quot; data-og-source-url=&quot;https://lucia-auth.com/guidebook/kysely/&quot; data-og-url=&quot;https://lucia-auth.com/guidebook/kysely/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/BKUAx/hyU2qia3AV/MZxXptWu7KyUckZmoIB1UK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://lucia-auth.com/guidebook/kysely/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://lucia-auth.com/guidebook/kysely/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/BKUAx/hyU2qia3AV/MZxXptWu7KyUckZmoIB1UK/img.jpg?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;Using Kysely&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Learn how to use Kysely with Lucia&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;lucia-auth.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>웹 개발</category>
      <category>Kysely</category>
      <category>Lucia</category>
      <category>lucia-auth</category>
      <category>next.js</category>
      <category>OAuth</category>
      <category>planetscale</category>
      <author>깨비아빠0</author>
      <guid isPermaLink="true">https://freeislet.tistory.com/59</guid>
      <comments>https://freeislet.tistory.com/59#entry59comment</comments>
      <pubDate>Wed, 10 Jan 2024 16:23:51 +0900</pubDate>
    </item>
    <item>
      <title>PlanetScale + Kysely 데이터베이스 연동 (Next.js)</title>
      <link>https://freeislet.tistory.com/57</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #3d62ce; text-align: start;&quot; href=&quot;https://freeislet.tistory.com/55&quot;&gt;Next.js 프로젝트 스택 검토 (2023)&lt;/a&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;대로 홈페이지를 다시 만들기 위한 두 번째 단계로 &lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;Kysely를 사용하여 &lt;/span&gt;PlanetScale 데이터베이스를 연동했다.&lt;/span&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;800&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nM36B/btsCUy6ZMGV/J8tsxkKhI4sM97qHaBeK40/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nM36B/btsCUy6ZMGV/J8tsxkKhI4sM97qHaBeK40/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nM36B/btsCUy6ZMGV/J8tsxkKhI4sM97qHaBeK40/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnM36B%2FbtsCUy6ZMGV%2FJ8tsxkKhI4sM97qHaBeK40%2Fimg.jpg&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;700&quot; height=&quot;368&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YBeHP/btsC38yTfWa/WKKaPgus0bpvLbKePrX8vK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YBeHP/btsC38yTfWa/WKKaPgus0bpvLbKePrX8vK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YBeHP/btsC38yTfWa/WKKaPgus0bpvLbKePrX8vK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYBeHP%2FbtsC38yTfWa%2FWKKaPgus0bpvLbKePrX8vK%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;700&quot; height=&quot;344&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1488&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;h2 data-ke-size=&quot;size26&quot;&gt;PlanetScale 데이터베이스 셋업&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://planetscale.com/docs/tutorials/planetscale-quick-start-guide&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://planetscale.com/docs/tutorials/planetscale-quick-start-guide&lt;/a&gt;를 따라하면 정말 쉽게 새 데이터베이스를 만들 수 있다. 데이터베이스 생성이 끝나면 대시보드에서 DB 상태를 비롯해 각종 정보를 볼 수 있다.&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;3022&quot; data-origin-height=&quot;1512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clC5mp/btsCTX0ky5l/0PVlKrcgNieWb6bLm0bSu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clC5mp/btsCTX0ky5l/0PVlKrcgNieWb6bLm0bSu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clC5mp/btsCTX0ky5l/0PVlKrcgNieWb6bLm0bSu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclC5mp%2FbtsCTX0ky5l%2F0PVlKrcgNieWb6bLm0bSu1%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;700&quot; height=&quot;350&quot; data-origin-width=&quot;3022&quot; data-origin-height=&quot;1512&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;우측의 Connect 버튼을 누르면 username 등의 DB 접속 정보를 확인할 수 있는데, Connect with에서 MySQL CLI를 선택하면 나오는 명령을 참고하여 일반적인 DB 클라이언트에서 접속 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1778&quot; data-origin-height=&quot;1046&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/busfba/btsC4ey7WWY/yUSn1ybBpj69B3cxkK2w2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/busfba/btsC4ey7WWY/yUSn1ybBpj69B3cxkK2w2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/busfba/btsC4ey7WWY/yUSn1ybBpj69B3cxkK2w2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbusfba%2FbtsC4ey7WWY%2FyUSn1ybBpj69B3cxkK2w2k%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;600&quot; height=&quot;353&quot; data-origin-width=&quot;1778&quot; data-origin-height=&quot;1046&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;다음은 MySQLWorkbench에서 방금 만든 데이터베이스에 gcp.connect.psdb.cloud:3306 주소로 접속하여, &lt;a href=&quot;https://planetscale.com/docs/tutorials/planetscale-quick-start-guide&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Quick Guide&lt;/a&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;1916&quot; data-origin-height=&quot;1466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5gvvR/btsC37fIL8r/lCc0UfYAwyV5gm1rKiYxb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5gvvR/btsC37fIL8r/lCc0UfYAwyV5gm1rKiYxb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5gvvR/btsC37fIL8r/lCc0UfYAwyV5gm1rKiYxb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5gvvR%2FbtsC37fIL8r%2FlCc0UfYAwyV5gm1rKiYxb1%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;700&quot; height=&quot;536&quot; data-origin-width=&quot;1916&quot; data-origin-height=&quot;1466&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;h2 data-ke-size=&quot;size26&quot;&gt;Kysely 셋업&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패키지 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 명령으로 &lt;a href=&quot;https://kysely.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;kysely&lt;/a&gt;, &lt;a href=&quot;https://github.com/depot/kysely-planetscale&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PlanetScale dialect&lt;/a&gt;, &lt;a href=&quot;https://github.com/RobinBlomberg/kysely-codegen&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;kysely-codegen(스키마 코드 생성기)&lt;/a&gt;를 설치한다.&lt;/p&gt;
&lt;pre id=&quot;code_1704266633691&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;npm install kysely kysely-planetscale @planetscale/database
npm install --save-dev kysely-codegen mysql2&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;mysql2는 kysely-codegen에서 DB에 접속할 때만 사용하므로 --save-dev로 설치한다.&lt;/blockquote&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;데이터베이스 환경변수 추가&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Connect 정보 화면에서 Connect with @planetscale/database를 선택하면 DB 접속에 필요한 host, username을 얻을 수 있다. (password는 최초 생성시에만 조회 가능)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DATABASE_HOST, DATABASE_USERNAME, DATABASE_PASSWORD 환경 변수를 .env.local 파일에 저장한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKq7Ul/btsC4CUL1h1/J3u5cmuih2V30A2YM0kRX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKq7Ul/btsC4CUL1h1/J3u5cmuih2V30A2YM0kRX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKq7Ul/btsC4CUL1h1/J3u5cmuih2V30A2YM0kRX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKq7Ul%2FbtsC4CUL1h1%2FJ3u5cmuih2V30A2YM0kRX0%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;600&quot; height=&quot;141&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 환경변수는 비밀번호를 포함하고 있으므로, git에 올리는 대신 Vercel 플랫폼 환경변수 세팅에도 추가한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Settings &amp;gt; Environment Variables 페이지에서 아래와 같이 DATABASE_HOST, DATABASE_USERNAME, DATABASE_PASSWORD 환경 변수를 추가하고, 하단의 Save 버튼을 누르면 저장된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1325&quot; data-origin-height=&quot;991&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ben5um/btsCWJA9NGA/5mUMRNBXeChyzhfJ2VID9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ben5um/btsCWJA9NGA/5mUMRNBXeChyzhfJ2VID9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ben5um/btsCWJA9NGA/5mUMRNBXeChyzhfJ2VID9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fben5um%2FbtsCWJA9NGA%2F5mUMRNBXeChyzhfJ2VID9K%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;700&quot; height=&quot;524&quot; data-origin-width=&quot;1325&quot; data-origin-height=&quot;991&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;h3 data-ke-size=&quot;size23&quot;&gt;DB 스키마 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kysely 연동을 위해 먼저 kysely-codegen으로 스키마 인터페이스 정의 파일을 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kysely-codegen을 실행하려면 DATABASE_URL 환경 변수가 필요한데, PlanetScale Connect 정보 화면에서 Connect with Node.js를 선택하여 확인할 수 있다. (********을 비밀번호로 변경해줘야 함)&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;1666&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvFxX0/btsC0ZcuVjZ/W3vizZpkWLvaB6GesZS3PK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvFxX0/btsC0ZcuVjZ/W3vizZpkWLvaB6GesZS3PK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvFxX0/btsC0ZcuVjZ/W3vizZpkWLvaB6GesZS3PK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvFxX0%2FbtsC0ZcuVjZ%2FW3vizZpkWLvaB6GesZS3PK%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;600&quot; height=&quot;115&quot; data-origin-width=&quot;1666&quot; data-origin-height=&quot;320&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;DATABASE_URL 환경 변수를 .env.local에 추가한 후에 아래와 같이 명령을 실행하면,&lt;/p&gt;
&lt;pre id=&quot;code_1704269557056&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npx kysely-codegen --env-file .env.local --out-file lib/db-schema.d.ts&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;다음과 같이 lib 폴더에 db-schema.d.ts 파일이 생성된다.&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;1032&quot; data-origin-height=&quot;473&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddvoo3/btsC38TTGKv/3N3ZJ9uP6gR8XiDapIbZO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddvoo3/btsC38TTGKv/3N3ZJ9uP6gR8XiDapIbZO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddvoo3/btsC38TTGKv/3N3ZJ9uP6gR8XiDapIbZO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fddvoo3%2FbtsC38TTGKv%2F3N3ZJ9uP6gR8XiDapIbZO1%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;860&quot; height=&quot;394&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;473&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;h3 data-ke-size=&quot;size23&quot;&gt;데이터베이스 접속 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lib/database.ts 파일에 다음과 같이 Kysely 인스턴스를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 추가한 DATABASE_xxx 환경 변수들을 PlanetScaleDialect config에 설정해주고, DB 스키마는 kysely-codegen으로 생성한 스키마 정의를 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1704364472981&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import 'server-only'

import { Kysely } from 'kysely'
import { PlanetScaleDialect } from 'kysely-planetscale'
import { DB } from './db-schema'
 
export const db = new Kysely&amp;lt;DB&amp;gt;({
  dialect: new PlanetScaleDialect({
    host: process.env.DATABASE_HOST,
    username: process.env.DATABASE_USERNAME,
    password: process.env.DATABASE_PASSWORD,
  }),
})&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;server-only는 서버에서만 실행되어야 하는 코드임을 구분해주는 패키지로서, &quot;npm install server-only&quot; 명령으로 설치해야 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Repository 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리 테스트를 위해 repository/products.ts 파일에 샘플로 만들었던 Products 테이블의 Repository 클래스를 추가하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1704365055189&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { db } from '@/lib/database'
import { Products } from '@/lib/db-schema'

export class ProductsRepository {
  async all() {
    return await db.selectFrom('products')
      .selectAll()
      .execute()
  }

  async findById(id: number) {
    return await db.selectFrom('products')
      .where('id', '=', id)
      .selectAll()
      .executeTakeFirst()
  }
}

export const productsRepository = new ProductsRepository()&lt;/code&gt;&lt;/pre&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 page.tsx에 DB 조회 및 HTML 코드를 추가했다.&lt;/p&gt;
&lt;pre id=&quot;code_1704365193292&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default async function Home() {
  const products = await productsRepository.all()

  return (
    &amp;lt;main ...&amp;gt;
      &amp;lt;ul&amp;gt;
        {products.map((product, index) =&amp;gt; (
          &amp;lt;li key={index}&amp;gt;
            {product.id}: {product.name} (category: {product.category_id})
          &amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&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;아래와 같이 products 테이블 레코드들이 정상적으로 출력된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4sEZa/btsCZ3y5HlQ/0cd9fRENf8WMpdbBVqGdHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4sEZa/btsCZ3y5HlQ/0cd9fRENf8WMpdbBVqGdHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4sEZa/btsCZ3y5HlQ/0cd9fRENf8WMpdbBVqGdHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4sEZa%2FbtsCZ3y5HlQ%2F0cd9fRENf8WMpdbBVqGdHK%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;700&quot; height=&quot;139&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;208&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;h3 data-ke-size=&quot;size23&quot;&gt;Vercel 배포 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub에 코드를 push하면 자동으로 빌드가 시작된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드가 완료된 후 &lt;a href=&quot;https://freeislet.vercel.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;홈페이지&lt;/a&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;1107&quot; data-origin-height=&quot;522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpI23Y/btsC3Zpcado/wvwzf3h9k1oDm1df6JH4kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpI23Y/btsC3Zpcado/wvwzf3h9k1oDm1df6JH4kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpI23Y/btsC3Zpcado/wvwzf3h9k1oDm1df6JH4kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdpI23Y%2FbtsC3Zpcado%2Fwvwzf3h9k1oDm1df6JH4kk%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;700&quot; height=&quot;330&quot; data-origin-width=&quot;1107&quot; data-origin-height=&quot;522&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://kysely.dev/docs/getting-started?dialect=mysql&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kysely.dev/docs/getting-started?dialect=mysql&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1704266096365&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;Getting started | Kysely&quot; data-og-description=&quot;Installation&quot; data-og-host=&quot;kysely.dev&quot; data-og-source-url=&quot;https://kysely.dev/docs/getting-started?dialect=mysql&quot; data-og-url=&quot;https://kysely.dev/docs/getting-started&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://kysely.dev/docs/getting-started?dialect=mysql&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kysely.dev/docs/getting-started?dialect=mysql&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Getting started | Kysely&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Installation&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kysely.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.ayoubkhial.com/blog/build-fully-typed-web-app-using-next.js-planetscale-drizzle-and-kysely&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.ayoubkhial.com/blog/build-fully-typed-web-app-using-next.js-planetscale-drizzle-and-kysely&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1704264459749&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;Build a fully typed web app using Next.js, PlanetScale, Drizzle, and Kysely&quot; data-og-description=&quot;Harness the power of PlanetScale, Drizzle ORM, and Kysely Query Builder to develop a scalable fully-typed web app.&quot; data-og-host=&quot;www.ayoubkhial.com&quot; data-og-source-url=&quot;https://www.ayoubkhial.com/blog/build-fully-typed-web-app-using-next.js-planetscale-drizzle-and-kysely&quot; data-og-url=&quot;https://ayoubkhial.com/blog/build-fully-typed-web-app-using-next.js-planetscale-drizzle-and-kysely&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/s5oxY/hyUXKuankk/MBGtGoE5u604haILgrbXLk/img.jpg?width=4800&amp;amp;height=2520&amp;amp;face=0_0_4800_2520&quot;&gt;&lt;a href=&quot;https://www.ayoubkhial.com/blog/build-fully-typed-web-app-using-next.js-planetscale-drizzle-and-kysely&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.ayoubkhial.com/blog/build-fully-typed-web-app-using-next.js-planetscale-drizzle-and-kysely&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/s5oxY/hyUXKuankk/MBGtGoE5u604haILgrbXLk/img.jpg?width=4800&amp;amp;height=2520&amp;amp;face=0_0_4800_2520');&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;Build a fully typed web app using Next.js, PlanetScale, Drizzle, and Kysely&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Harness the power of PlanetScale, Drizzle ORM, and Kysely Query Builder to develop a scalable fully-typed web app.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.ayoubkhial.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>웹 개발</category>
      <category>Database</category>
      <category>Kysely</category>
      <category>next.js</category>
      <category>planetscale</category>
      <author>깨비아빠0</author>
      <guid isPermaLink="true">https://freeislet.tistory.com/57</guid>
      <comments>https://freeislet.tistory.com/57#entry57comment</comments>
      <pubDate>Thu, 4 Jan 2024 20:38:01 +0900</pubDate>
    </item>
    <item>
      <title>shadcn/ui (Next.js)</title>
      <link>https://freeislet.tistory.com/56</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://freeislet.tistory.com/55&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Next.js 프로젝트 스택 검토 (2023)&lt;/a&gt; 대로 홈페이지를 다시 만들기 위한 첫 번째 단계로 shadcn/ui를 추가했다.&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;1280&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CfJNk/btsC35a7PPM/qxSv55A0yaF9KvkfkJVxhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CfJNk/btsC35a7PPM/qxSv55A0yaF9KvkfkJVxhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CfJNk/btsC35a7PPM/qxSv55A0yaF9KvkfkJVxhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCfJNk%2FbtsC35a7PPM%2FqxSv55A0yaF9KvkfkJVxhk%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;1280&quot; height=&quot;618&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;618&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;h2 data-ke-size=&quot;size26&quot;&gt;shadcn/ui 셋업&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ui.shadcn.com/docs/installation/next&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ui.shadcn.com/docs/installation/next&lt;/a&gt;의 가이드를 참고하여 shadcn/ui를 셋업한다.&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;다음 명령으로 shadcn-ui를 초기화한다. 설정을 위한 질문에는 모두 디폴트값을 선택한다.&lt;/p&gt;
&lt;pre id=&quot;code_1704169856134&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npx shadcn-ui@latest init

Would you like to use TypeScript (recommended)? no / yes
Which style would you like to use? &amp;rsaquo; Default
Which color would you like to use as base color? &amp;rsaquo; Slate
Where is your global CSS file? &amp;rsaquo; &amp;rsaquo; app/globals.css
Do you want to use CSS variables for colors? &amp;rsaquo; no / yes
Where is your tailwind.config.js located? &amp;rsaquo; tailwind.config.js
Configure the import alias for components: &amp;rsaquo; @/components
Configure the import alias for utils: &amp;rsaquo; @/lib/utils
Are you using React Server Components? &amp;rsaquo; no / yes&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;b&gt;components.json&lt;/b&gt;: shadcn/ui 설정을 담은 components.json 파일이 생성된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Rggbt/btsC00awOao/hY0vE0aRtpX6aQcL6q1TGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Rggbt/btsC00awOao/hY0vE0aRtpX6aQcL6q1TGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Rggbt/btsC00awOao/hY0vE0aRtpX6aQcL6q1TGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRggbt%2FbtsC00awOao%2FhY0vE0aRtpX6aQcL6q1TGK%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;400&quot; height=&quot;303&quot; data-origin-width=&quot;878&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;b&gt;global.css&lt;/b&gt;: 아래와 같이 root에 기본 variable들이 추가된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7IbW8/btsCTuxb220/M1Kf1FBmfRuQg9K8rrkI51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7IbW8/btsCTuxb220/M1Kf1FBmfRuQg9K8rrkI51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7IbW8/btsCTuxb220/M1Kf1FBmfRuQg9K8rrkI51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7IbW8%2FbtsCTuxb220%2FM1Kf1FBmfRuQg9K8rrkI51%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;400&quot; height=&quot;534&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1186&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;3. &lt;b&gt;lib/util.ts&lt;/b&gt;: 유틸 파일이 추가된다. 여기에는 class 이름 조합을 도와주는 cn 함수가 구현되어 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;812&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NT0CT/btsC0YDMQ7m/sUfikecjKlWd5aovcyuVo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NT0CT/btsC0YDMQ7m/sUfikecjKlWd5aovcyuVo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NT0CT/btsC0YDMQ7m/sUfikecjKlWd5aovcyuVo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNT0CT%2FbtsC0YDMQ7m%2FsUfikecjKlWd5aovcyuVo1%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;400&quot; height=&quot;130&quot; data-origin-width=&quot;812&quot; data-origin-height=&quot;264&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;4. &lt;b&gt;tailwind.config.js&lt;/b&gt;: tailwind config 파일이 추가된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기존 tailwind.config.ts 파일과 합치는 것이 좋을지 검토 필요함&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컴포넌트 추가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트를 추가하려면 명령줄에서 &quot;npx shadcn-ui@latest add&quot; 명령을 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, Button 컴포넌트를 추가하려면, 터미널에서 다음 명령을 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1704259081139&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npx shadcn-ui@latest add button&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;그러면, 아래와 같이 component/ui 디렉토리에 button.tsx 파일이 생성되며,&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;740&quot; data-origin-height=&quot;676&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sznjh/btsC1nwvsr0/qzd5LyLD0EPU33yNbdixGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sznjh/btsC1nwvsr0/qzd5LyLD0EPU33yNbdixGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sznjh/btsC1nwvsr0/qzd5LyLD0EPU33yNbdixGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsznjh%2FbtsC1nwvsr0%2Fqzd5LyLD0EPU33yNbdixGK%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;300&quot; height=&quot;274&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;676&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;메인 page.tsx에 &amp;lt;Button&amp;gt; 컴포넌트를 삽입하면 화면에 Click me 버튼이 나오는 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bA94OQ/btsCTW1hkrA/7jptUQtGG1eKqdUO611ggk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bA94OQ/btsCTW1hkrA/7jptUQtGG1eKqdUO611ggk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bA94OQ/btsCTW1hkrA/7jptUQtGG1eKqdUO611ggk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbA94OQ%2FbtsCTW1hkrA%2F7jptUQtGG1eKqdUO611ggk%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;700&quot; height=&quot;143&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2082&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9arkL/btsCRe2oICf/Exz9uHkwfVPrJ9J2M0Hkbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9arkL/btsCRe2oICf/Exz9uHkwfVPrJ9J2M0Hkbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9arkL/btsCRe2oICf/Exz9uHkwfVPrJ9J2M0Hkbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9arkL%2FbtsCRe2oICf%2FExz9uHkwfVPrJ9J2M0Hkbk%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;700&quot; height=&quot;144&quot; data-origin-width=&quot;2082&quot; data-origin-height=&quot;428&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;shadcn/ui의 모든 컴포넌트 목록은 &lt;a href=&quot;https://ui.shadcn.com/docs/components&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ui.shadcn.com/docs/components&lt;/a&gt;에서 볼 수 있다.&lt;/p&gt;</description>
      <category>웹 개발</category>
      <category>next.js</category>
      <category>shadcn/ui</category>
      <author>깨비아빠0</author>
      <guid isPermaLink="true">https://freeislet.tistory.com/56</guid>
      <comments>https://freeislet.tistory.com/56#entry56comment</comments>
      <pubDate>Wed, 3 Jan 2024 14:38:43 +0900</pubDate>
    </item>
    <item>
      <title>Next.js 프로젝트 스택 검토 (2023)</title>
      <link>https://freeislet.tistory.com/55</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://freeislet.tistory.com/47&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Next.js 프로젝트를 GitHub pages에 배포하기&lt;/a&gt;에서 사용한&amp;nbsp;Static Site Generation(SSG) 방식으로는 홈페이지를 만드는데 한계가 있어서, 서버 호스팅을 포함해 웹앱 개발 및 배포에 사용할 기술스택을 살펴보았다. 2023년 기준 스택이고, 무료 호스팅만 검토했다.&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;a href=&quot;https://dev.to/livecycle/how-to-host-your-side-projects-for-free-in-2023-from-auth-to-database-42im&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.to/livecycle/how-to-host-your-side-projects-for-free-in-2023-from-auth-to-database-42im&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1703915167488&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;How to host your side-projects for free in 2023: from Auth to Database&quot; data-og-description=&quot;TL;DR In this article, we'll look at how you can host your side project for free in 2023....&quot; data-og-host=&quot;dev.to&quot; data-og-source-url=&quot;https://dev.to/livecycle/how-to-host-your-side-projects-for-free-in-2023-from-auth-to-database-42im&quot; data-og-url=&quot;https://dev.to/livecycle/how-to-host-your-side-projects-for-free-in-2023-from-auth-to-database-42im&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/RQCUD/hyUTGeSVJD/jPHm4vg0MFV6kUfxUVM511/img.jpg?width=1000&amp;amp;height=500&amp;amp;face=0_0_1000_500,https://scrap.kakaocdn.net/dn/TSvMo/hyUTBkmZPl/iXKAUH4brEP3F8JZelSxtk/img.jpg?width=1000&amp;amp;height=420&amp;amp;face=0_0_1000_420,https://scrap.kakaocdn.net/dn/c5I3kz/hyUXYkDFCj/OdstUsVlsxv8oZuYTo0rcK/img.jpg?width=800&amp;amp;height=420&amp;amp;face=0_0_800_420&quot;&gt;&lt;a href=&quot;https://dev.to/livecycle/how-to-host-your-side-projects-for-free-in-2023-from-auth-to-database-42im&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dev.to/livecycle/how-to-host-your-side-projects-for-free-in-2023-from-auth-to-database-42im&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/RQCUD/hyUTGeSVJD/jPHm4vg0MFV6kUfxUVM511/img.jpg?width=1000&amp;amp;height=500&amp;amp;face=0_0_1000_500,https://scrap.kakaocdn.net/dn/TSvMo/hyUTBkmZPl/iXKAUH4brEP3F8JZelSxtk/img.jpg?width=1000&amp;amp;height=420&amp;amp;face=0_0_1000_420,https://scrap.kakaocdn.net/dn/c5I3kz/hyUXYkDFCj/OdstUsVlsxv8oZuYTo0rcK/img.jpg?width=800&amp;amp;height=420&amp;amp;face=0_0_800_420');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;How to host your side-projects for free in 2023: from Auth to Database&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;TL;DR In this article, we'll look at how you can host your side project for free in 2023....&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dev.to&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;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;&lt;b&gt;Framework&lt;/b&gt;: Next.js (&lt;a href=&quot;https://nextjs.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://nextjs.org/&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CSS&lt;/b&gt;: Tailwind CSS (&lt;a href=&quot;https://tailwindcss.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://tailwindcss.com/&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UI&lt;/b&gt;: shadcn/ui (&lt;a href=&quot;https://ui.shadcn.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ui.shadcn.com/&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인증&lt;/b&gt;: Lucia (&lt;a href=&quot;https://lucia-auth.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://lucia-auth.com/&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;웹호스팅&lt;/b&gt;: Vercel (&lt;a href=&quot;https://vercel.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://vercel.com/&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터베이스&lt;/b&gt;: PlanetScale (&lt;a href=&quot;https://planetscale.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://planetscale.com/&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SQL&lt;/b&gt;: Kysely (&lt;a href=&quot;https://kysely.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kysely.dev/&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비고&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;shadcn/ui&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1462&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1aD0f/btsCSBQwQQ3/xQLDdQ1cQ7dZEKNu9fRxNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1aD0f/btsCSBQwQQ3/xQLDdQ1cQ7dZEKNu9fRxNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1aD0f/btsCSBQwQQ3/xQLDdQ1cQ7dZEKNu9fRxNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1aD0f%2FbtsCSBQwQQ3%2FxQLDdQ1cQ7dZEKNu9fRxNK%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;600&quot; height=&quot;290&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1462&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;Next.js, React UI 라이브러리를 검색해봤더니 정말 많은 것들이 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 라이브러리들을 살펴본 끝에 각종 포스트의 댓글에서 인기가 많은 &lt;a href=&quot;https://ui.shadcn.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;shadcn/ui&lt;/a&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;Next.js 연동은 &lt;a href=&quot;https://ui.shadcn.com/docs/installation/next&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ui.shadcn.com/docs/installation/next&lt;/a&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;Lucia&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBl58L/btsDhAprnzH/ZggzmNj2N5CN4kRVjEQlUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBl58L/btsDhAprnzH/ZggzmNj2N5CN4kRVjEQlUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBl58L/btsDhAprnzH/ZggzmNj2N5CN4kRVjEQlUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBl58L%2FbtsDhAprnzH%2FZggzmNj2N5CN4kRVjEQlUk%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;600&quot; height=&quot;257&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1294&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://clerk.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;clerk&lt;/a&gt;을 추천하고 있지만, 그렇게 제대로 된 회원관리가 필요할 것 같지는 않아서 &lt;a href=&quot;https://lucia-auth.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Lucia&lt;/a&gt;를 쓰기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Auth.js(구 NextAuth.js)를 검토했지만, 문서화가 너무 부족하고 연동 과정도 매끄럽지 않아서 대안으로 lucia를 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lucia는 문서화도 잘 되어 있고, 연동도 짧은 시간 안에 쉽게 끝낼 수 있었다.&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;PlanetScale&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3sfOC/btsCQqAKLQ4/fzzt31rCdt1PvkXnxP9YU1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3sfOC/btsCQqAKLQ4/fzzt31rCdt1PvkXnxP9YU1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3sfOC/btsCQqAKLQ4/fzzt31rCdt1PvkXnxP9YU1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3sfOC%2FbtsCQqAKLQ4%2Ffzzt31rCdt1PvkXnxP9YU1%2Fimg.jpg&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;600&quot; height=&quot;315&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 비해 쓸만한 무료 데이터베이스도 참 많아진 것 같다. 그 중에서 PlanetScale은 &lt;a href=&quot;https://vitess.io/&quot;&gt;vitess&lt;/a&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기술을 기반으로 스케일, 성능, 안정성을 제공하는&amp;nbsp;&lt;/span&gt;MySQL 데이터베이스이다.&amp;nbsp;&lt;a href=&quot;https://pilcrow.vercel.app/blog/serverless-database-latency&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://pilcrow.vercel.app/blog/serverless-database-latency&lt;/a&gt;의 벤치마킹 결과를 참고하여 PlanetScale을 선택했다.&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;Kysely&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js 템플릿이나 예제를 보면 거의 Prisma ORM 라이브러리를 쓰고 있는데, 곳곳에서 느린 부분을 지적하는 글이 보였다. 파이썬 서버에서도 SQLAlchemy를 쓸 때 성능 이슈가 있었는데, ORM 라이브러리를 쓸 때 주의해야 할 듯하다. 사실 SQLAlchemy는 성능 이외에도 복잡한 서브쿼리나 조인을 사용하는 쿼리를 옮길 때 alias가 생각처럼 이뤄지지 않는 등 불편한 부분이 있었는데, Prisma도 사용하다 보면 비슷한 일들이 있지 않을까 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 Kysely는 쿼리 구문과 1:1 대응되게 코드를 짜면 돼서 훨씬 편할 것 같다. 예를 들어, Kysely에서 &lt;a href=&quot;https://kysely.dev/docs/examples/JOIN/subquery-join&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;서브쿼리를 조인하는 예제&lt;/a&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;1786&quot; data-origin-height=&quot;920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fv80J/btsCTx0i9a0/V5kgkDvBhBfiYQqNP0MRr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fv80J/btsCTx0i9a0/V5kgkDvBhBfiYQqNP0MRr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fv80J/btsCTx0i9a0/V5kgkDvBhBfiYQqNP0MRr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFv80J%2FbtsCTx0i9a0%2FV5kgkDvBhBfiYQqNP0MRr0%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;800&quot; height=&quot;412&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;920&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;</description>
      <category>웹 개발</category>
      <category>2023 web stack</category>
      <category>Auth.js</category>
      <category>Kysely</category>
      <category>next.js</category>
      <category>NextAuth.js</category>
      <category>planetscale</category>
      <category>shadcn/ui</category>
      <category>tailwind css</category>
      <category>Vercel</category>
      <author>깨비아빠0</author>
      <guid isPermaLink="true">https://freeislet.tistory.com/55</guid>
      <comments>https://freeislet.tistory.com/55#entry55comment</comments>
      <pubDate>Sat, 30 Dec 2023 16:04:39 +0900</pubDate>
    </item>
    <item>
      <title>Gemini API 테스트</title>
      <link>https://freeislet.tistory.com/51</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;2023년 12월 구글 Gemini의 확장된 버전이 공개되었다.&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;Gemini 소개글: &lt;a href=&quot;https://korea.googleblog.com/2023/12/blog-post_652.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://korea.googleblog.com/2023/12/blog-post_652.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1703557780091&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;가장 유능하고 범용적인 AI 모델 제미나이(Gemini)를 소개합니다&quot; data-og-description=&quot;모든 사람들에게 더욱 유용한 AI를 만듭니다 이 블로그는 구글 The Keyword 블로그( 영문 )에서도 확인하실 수 있습니다. 순다 피차이(Sundar Pichai) 구글 및 알파벳 CEO 기술의 변화는 과학의 발전, 인류&quot; data-og-host=&quot;korea.googleblog.com&quot; data-og-source-url=&quot;https://korea.googleblog.com/2023/12/blog-post_652.html&quot; data-og-url=&quot;https://korea.googleblog.com/2023/12/blog-post_652.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/O8sAk/hyUTvXy4jQ/i0k0iI8DLT9ErgnJAn2wJK/img.png?width=72&amp;amp;height=72&amp;amp;face=0_0_72_72,https://scrap.kakaocdn.net/dn/VDBbC/hyUTEUwlVQ/7zNiAkdMA9D9OGMi1vbslk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bbmoyV/hyUTBpXJxQ/pcXgUhTl0SvBmrMxB1S2nK/img.png?width=72&amp;amp;height=72&amp;amp;face=0_0_72_72&quot;&gt;&lt;a href=&quot;https://korea.googleblog.com/2023/12/blog-post_652.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://korea.googleblog.com/2023/12/blog-post_652.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/O8sAk/hyUTvXy4jQ/i0k0iI8DLT9ErgnJAn2wJK/img.png?width=72&amp;amp;height=72&amp;amp;face=0_0_72_72,https://scrap.kakaocdn.net/dn/VDBbC/hyUTEUwlVQ/7zNiAkdMA9D9OGMi1vbslk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bbmoyV/hyUTBpXJxQ/pcXgUhTl0SvBmrMxB1S2nK/img.png?width=72&amp;amp;height=72&amp;amp;face=0_0_72_72');&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;가장 유능하고 범용적인 AI 모델 제미나이(Gemini)를 소개합니다&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;모든 사람들에게 더욱 유용한 AI를 만듭니다 이 블로그는 구글 The Keyword 블로그( 영문 )에서도 확인하실 수 있습니다. 순다 피차이(Sundar Pichai) 구글 및 알파벳 CEO 기술의 변화는 과학의 발전, 인류&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;korea.googleblog.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;Gemini로 간단하게 프롬프트를 테스트해본 내용과 API 테스트 결과를 정리해둔다.&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;Gemini 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://makersuite.google.com/app/prompts/new_freeform&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Google AI Studio&lt;/a&gt;에서 gemini를 체험할 수 있으며, UI는 ChatGPT와 유사하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 &quot;Gemini와 ChatGPT를 비교해줘&quot;라는 질문을 던져본 모습이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdFGN1/btsCya6Z0z9/brrKIh6wjKQMEHBnnYMQN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdFGN1/btsCya6Z0z9/brrKIh6wjKQMEHBnnYMQN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdFGN1/btsCya6Z0z9/brrKIh6wjKQMEHBnnYMQN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdFGN1%2FbtsCya6Z0z9%2FbrrKIh6wjKQMEHBnnYMQN1%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;1522&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1522&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;Gemini Pro가 아닌 Gemini 1.0에 대한 답변이 나와서, &quot;Gemini Pro처럼 확장된 버전에 대해서 비교설명해줄래&quot;로 다시 질문해 보았다. ChatGPT 학습에 사용한 파라미터가 1750억 개인데 비해, Gemini Pro는 10억 개의 파라미터를 사용했다는 내용을 볼 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceZ393/btsCyw22uky/pPaBnfkreYLGXFHsw1qJRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceZ393/btsCyw22uky/pPaBnfkreYLGXFHsw1qJRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceZ393/btsCyw22uky/pPaBnfkreYLGXFHsw1qJRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceZ393%2FbtsCyw22uky%2FpPaBnfkreYLGXFHsw1qJRk%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;1522&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1522&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;h2 data-ke-size=&quot;size26&quot;&gt;Gemini API key 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google AI Studio 화면 좌측의 &quot;Get API key&quot; 페이지에서 API key를 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Create API key in new project&quot;를 누르면 아래와 같이 생성된 API key를 보여준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3022&quot; data-origin-height=&quot;1466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbdb9l/btsCJleihnK/qobxtANDD8EAfZNgww0b2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbdb9l/btsCJleihnK/qobxtANDD8EAfZNgww0b2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbdb9l/btsCJleihnK/qobxtANDD8EAfZNgww0b2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbbdb9l%2FbtsCJleihnK%2FqobxtANDD8EAfZNgww0b2K%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;1466&quot; data-origin-width=&quot;3022&quot; data-origin-height=&quot;1466&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;GCP 콘솔을 확인해보면 아래와 같이 새로 생성된 &quot;Generative Language Client&quot; 프로젝트에 Generative Language API가 활성화된 것을 볼 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;API는 굳이 Google AI Studio를 거치지 않고 GCP 콘솔에서 직접 활성화할 수 있다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GAUVn/btsCAju6Dkl/QgosOTNETQQiqWkFoqvnF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GAUVn/btsCAju6Dkl/QgosOTNETQQiqWkFoqvnF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GAUVn/btsCAju6Dkl/QgosOTNETQQiqWkFoqvnF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGAUVn%2FbtsCAju6Dkl%2FQgosOTNETQQiqWkFoqvnF1%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;600&quot; height=&quot;229&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1156&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdA1Vp/btsCBMqibGJ/0P8xhHAHKkmFh9NHrYBI90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdA1Vp/btsCBMqibGJ/0P8xhHAHKkmFh9NHrYBI90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdA1Vp/btsCBMqibGJ/0P8xhHAHKkmFh9NHrYBI90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdA1Vp%2FbtsCBMqibGJ%2F0P8xhHAHKkmFh9NHrYBI90%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;600&quot; height=&quot;234&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1178&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Gemini API 테스트 (REST API)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;curl을 이용해서 아래와 같이 REST API 버전의 gemini를 테스트할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1703565404245&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl \
  -H 'Content-Type: application/json' \
  -d '{&quot;contents&quot;:[{&quot;parts&quot;:[{&quot;text&quot;:&quot;Write a story about a magic backpack&quot;}]}]}' \
  -X POST https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=YOUR_API_KEY&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;URL 마지막의 YOUR_API_KEY에 생성된 key를 넣고, text 파라미터를 적절히 바꿔서 API 요청을 보내면, 아래와 같이 프롬프트에 대한 응답을 얻게 된다. (아래 스크린샷은 curl 대신 insomnia 툴로 테스트한 화면)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2428&quot; data-origin-height=&quot;1484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eerxwP/btsCEJteH8L/kCl1Kv7wqebmf6kxMSRdUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eerxwP/btsCEJteH8L/kCl1Kv7wqebmf6kxMSRdUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eerxwP/btsCEJteH8L/kCl1Kv7wqebmf6kxMSRdUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeerxwP%2FbtsCEJteH8L%2FkCl1Kv7wqebmf6kxMSRdUK%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;2428&quot; height=&quot;1484&quot; data-origin-width=&quot;2428&quot; data-origin-height=&quot;1484&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;h2 data-ke-size=&quot;size26&quot;&gt;Node.js 애플리케이션을 위한 Gemini API 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 연동을 위해 Node.js 환경에서 Gemini API를 사용해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 테스트해볼 목적으로 웹 프로젝트 대신 노트북 방식을 사용했다. (&lt;a href=&quot;https://freeislet.tistory.com/53&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Node.js Notebooks (REPL) 플러그인&lt;/a&gt; 사용)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1462&quot; data-origin-height=&quot;1236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFzU37/btsCF7np3rF/ez8isyfc1XkSdX7WLVKCK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFzU37/btsCF7np3rF/ez8isyfc1XkSdX7WLVKCK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFzU37/btsCF7np3rF/ez8isyfc1XkSdX7WLVKCK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFzU37%2FbtsCF7np3rF%2Fez8isyfc1XkSdX7WLVKCK0%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;600&quot; height=&quot;507&quot; data-origin-width=&quot;1462&quot; data-origin-height=&quot;1236&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;h3 data-ke-size=&quot;size23&quot;&gt;SDK 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 API 사용을 위해 SDK를 설치한다.&lt;/p&gt;
&lt;pre id=&quot;code_1703580915233&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install @google/generative-ai&lt;/code&gt;&lt;/pre&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텍스트 생성과 채팅에 적합한 gemini-pro 모델을 초기화한다.&lt;/p&gt;
&lt;pre id=&quot;code_1703581716523&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const { GoogleGenerativeAI } = require(&quot;@google/generative-ai&quot;);

const genAI = new GoogleGenerativeAI('생성된_API_KEY');
const model = genAI.getGenerativeModel({ model: &quot;gemini-pro&quot;});&lt;/code&gt;&lt;/pre&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 REST API로 테스트했던 것과 비슷하게 프롬프트 응답 결과를 받아 보려면, 다음과 같이 generateContent 함수를 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1703582148954&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function run() {
  const prompt = &quot;2024년 대한민국 출생률은 어떤 추세를 보일지 예측해줘&quot;

  console.log('processing...');

  const result = await model.generateContent(prompt);
  const response = await result.response;
  const text = response.text();

  console.log(text);
}

run();&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;processing...&lt;br /&gt;2024년 대한민국 출생률은 여러 가지 요인에 따라 달라질 수 있습니다. 이러한 요인에는 경제 상황, 주택 시장 조건, 출산 정책, 사회 문화적 변화 등이 포함됩니다.&lt;br /&gt;&lt;br /&gt;* **경제 상황:** 경제가 호황일 때 사람들은 자녀를 낳을 가능성이 더 높습니다. 이는 그들이 자녀를 부양할 수 있으며 자녀가 성공할 수 있는 미래를 제공할 수 있다고 느끼기 때문입니다. 경기가 침체기에 있을 때는 자녀를 낳을 가능성이 줄어듭니다. 이는 그들이 일자리를 구하기 어려울 것이며 자녀를 부양할 수 없을 것이라고 느끼기 때문입니다.&lt;br /&gt;* **주택 시장 조건:** 주택 시장이 부동산 가격이 저렴하고 대출 금리가 낮을 때 사람들은 집을 사고 가족을 시작할 가능성이 더 높습니다. 주택 시장이 불안정하고 대출 금리가 높을 때는 자녀를 낳을 가능성이 줄어듭니다. 이는 그들이 집을 살 여유가 없으며 가족을 부양할 수 없다고 느끼기 때문입니다.&lt;br /&gt;* **출산 정책:** 정부가 출산을 장려하는 정책을 시행하면 사람들은 자녀를 낳을 가능성이 더 높습니다. 이러한 정책은 재정 지원, 탁아 지원, 근로자 권리 보호 등을 포함할 수 있습니다. 정부가 출산을 억제하는 정책을 시행하면 자녀를 낳을 가능성이 줄어듭니다. 이러한 정책은 가족 구성에 대한 제한, 대량 학살 또는 강제 중절 등을 포함할 수 있습니다.&lt;br /&gt;* **사회 문화적 변화:** 사람들의 가족에 대한 태도가 바뀌면 자녀를 낳을 가능성이 더 높습니다. 예를 들어, 점점 더 많은 사람들이 결혼하지 않고도 자녀를 갖는 것이 허용 가능하다고 생각하고 있습니다. 점점 더 많은 사람들이 자녀를 갖지 않아도 괜찮다고 생각하고 있습니다. 인류의 종말을 고려하기도 하는데, 이러한 태도의 변화는 출생률에 부정적인 영향을 미칠 수 있습니다.&lt;br /&gt;&lt;br /&gt;전반적으로, 2024년 대한민국 출생률은 경제 상황, 주택 시장 조건, 출산 정책, 사회 문화적 변화 등 여러 가지 요인에 따라 달라질 수 있습니다. 이러한 요인들이 어떻게 전개될지는 예측하기 어렵지만, 출생률은 앞으로도 계속 감소할 가능성이 높습니다.&lt;br /&gt;&lt;br /&gt;2024년 한국의 출생률에 대한 몇 가지 구체적인 예측은 다음과 같습니다.&lt;br /&gt;&lt;br /&gt;* 통계청은 2024년 한국의 합계출산율이 0.89명이 될 것으로 예측했습니다. 이는 여성 한 명당 평생 낳을 것으로 예상되는 자녀 수입니다.&lt;br /&gt;* 유엔은 2024년 한국의 합계출산율이 0.92명이 될 것으로 예측했습니다.&lt;br /&gt;* 세계은행은 2024년 한국의 합계출산율이 0.95명이 될 것으로 예측했습니다.&lt;br /&gt;&lt;br /&gt;이러한 예측은 모두 한국의 출생률이 계속 감소할 것으로 나타났습니다. 이는 한국의 경제 상황, 주택 시장 조건, 출산 정책, 사회 문화적 변화 등 다양한 요인에 따른 것입니다.&lt;/blockquote&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; 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;multi-turn 대화(채팅) 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;챗봇 개발 시 다음과 같이 사용자와 모델의 대화 데이터를 startChat 인자로 넘긴 후 sendMessage 함수를 통해 새로운 응답을 얻을 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1703606577171&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function run() {
  const chat = model.startChat({
    history: [
      {
        role: &quot;user&quot;,
        parts: &quot;Hello, I have 2 dogs in my house.&quot;,
      },
      {
        role: &quot;model&quot;,
        parts: &quot;Great to meet you. What would you like to know?&quot;,
      },
    ],
    generationConfig: {
      maxOutputTokens: 100,
    },
  });

  const msg = &quot;How many paws are in my house?&quot;;

  const result = await chat.sendMessage(msg);
  const response = await result.response;
  const text = response.text();
  console.log(text);
}

run();&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;If you have two dogs in your house, each dog has four paws. Therefore, there are a total of **eight paws** in your house.&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;응답을 받을 때마다 다음 startChat에 넘길 history를 추가해줘야 하므로, 실제 앱 개발시에는 다음과 같은 식으로 사용하면 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;공식 매뉴얼에는 startChat을 사용하면 history를 직접 관리할 필요가 없다고 나오는데, 어떤 걸 말하는건지 찾지 못했다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1703606816809&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const chatHistory: any[] = [];

async function sendMessage(msg: string) {
  const chat = model.startChat({
    history: chatHistory,
    generationConfig: {
      maxOutputTokens: 100,
    },
  });
  
  const result = await chat.sendMessage(msg);
  const response = await result.response;
  const text = response.text();

  chatHistory.push({role: 'user', parts: msg});
  chatHistory.push({role: 'model', parts: text});
  console.log(chatHistory.slice(-2));
}

async function run() {
  await sendMessage('Hello, I have 2 dogs in my house.');
  await sendMessage('How many paws are in my house?');
}

run();&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;[&lt;br /&gt;&amp;nbsp; { role: 'user', parts: 'Hello, I have 2 dogs in my house.' },&lt;br /&gt;&amp;nbsp; {&lt;br /&gt;&amp;nbsp; &amp;nbsp; role: 'model',&lt;br /&gt;&amp;nbsp; &amp;nbsp; parts: 'Having two dogs in your house can be a wonderful and rewarding experience. Here are some tips for providing a happy and healthy home for your canine companions:\n' +&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; '\n' +&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; '1. **Create a Dog-Friendly Environment**:\n' +&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ' &amp;nbsp; - Ensure your house is safe and secure for your dogs.\n' +&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ' &amp;nbsp; - Provide a comfortable and dedicated space for them to rest and sleep.\n' +&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ' &amp;nbsp; - Keep essential items like food, water, and toys easily accessible.\n' +&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ' &amp;nbsp; - Install gates or barriers to prevent access to potentially dangerous'&lt;br /&gt;&amp;nbsp; }&lt;br /&gt;]&lt;br /&gt;[&lt;br /&gt;&amp;nbsp; { role: 'user', parts: 'How many paws are in my house?' },&lt;br /&gt;&amp;nbsp; {&lt;br /&gt;&amp;nbsp; &amp;nbsp; role: 'model',&lt;br /&gt;&amp;nbsp; &amp;nbsp; parts: 'If you have 2 dogs in your house, and each dog has 4 paws, then there are a total of **8 paws** in your house.\n' +&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; '\n' +&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;Here's a simple calculation:\n&quot; +&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; '\n' +&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 'Number of dogs = 2\n' +&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 'Number of paws per dog = 4\n' +&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; '\n' +&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 'Total number of paws = Number of dogs &amp;times; Number of paws per dog\n' +&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 'Total number of paws = 2 &amp;times; 4\n' +&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 'Total number of paws = 8\n' +&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; '\n' +&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 'Therefore, there are 8 paws in your'&lt;br /&gt;&amp;nbsp; }&lt;br /&gt;]&lt;/blockquote&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빠른 상호작용을 위해 모델 응답을 조금씩 스트리밍으로 받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트리밍을 위해서는 generateContentStream 또는 sendMessageStream 함수를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전의 채팅 테스트 코드를 스트리밍 방식으로 바꿔보면 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1703607953032&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const chatHistory: any[] = [];

async function sendMessageStream(msg: string) {
  console.log(`\nuser: ${msg}\n`)

  const chat = model.startChat({
    history: chatHistory,
    generationConfig: {
      maxOutputTokens: 100,
    },
  });
  
  const result = await chat.sendMessageStream(msg);

  let text = '';
  for await (const chunk of result.stream) {
    const chunkText = chunk.text();
    console.log(chunkText);
    text += chunkText;
  }

  chatHistory.push({role: 'user', parts: msg});
  chatHistory.push({role: 'model', parts: text});
}

async function run() {
  await sendMessageStream('Hello, I have 2 dogs in my house.');
  await sendMessageStream('How many paws are in my house?');
}

run();&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;blockquote data-ke-style=&quot;style2&quot;&gt;애석하게도 앞선 테스트와는 달리 결과가 이상하게 나왔다. (개 두 마리가 16개의 발...)&lt;/blockquote&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;gemini-streaming.gif&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;758&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOhv59/btsCMpU3JZP/9loezzIezhTrI49h5w1C51/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOhv59/btsCMpU3JZP/9loezzIezhTrI49h5w1C51/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOhv59/btsCMpU3JZP/9loezzIezhTrI49h5w1C51/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bOhv59/btsCMpU3JZP/9loezzIezhTrI49h5w1C51/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;806&quot; height=&quot;758&quot; data-filename=&quot;gemini-streaming.gif&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;758&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://ai.google.dev/tutorials/node_quickstart?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ai.google.dev/tutorials/node_quickstart?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1703581132201&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;빠른 시작: Node.js 애플리케이션에서 Gemini API 시작하기 &amp;nbsp;|&amp;nbsp; Google AI for Developers&quot; data-og-description=&quot;Node.js와 함께 Gemini API를 사용하여 이미지와 텍스트 프롬프트에서 텍스트를 생성하고 채팅 상호작용을 빌드하는 방법을 알아봅니다.&quot; data-og-host=&quot;ai.google.dev&quot; data-og-source-url=&quot;https://ai.google.dev/tutorials/node_quickstart?hl=ko&quot; data-og-url=&quot;https://ai.google.dev/tutorials/node_quickstart?hl=ko&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://ai.google.dev/tutorials/node_quickstart?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ai.google.dev/tutorials/node_quickstart?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;빠른 시작: Node.js 애플리케이션에서 Gemini API 시작하기 &amp;nbsp;|&amp;nbsp; Google AI for Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Node.js와 함께 Gemini API를 사용하여 이미지와 텍스트 프롬프트에서 텍스트를 생성하고 채팅 상호작용을 빌드하는 방법을 알아봅니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ai.google.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI,머신러닝/Generative AI</category>
      <category>GEMINI</category>
      <category>Gemini API</category>
      <category>Gemini API streaming</category>
      <category>Gemini Pro</category>
      <author>깨비아빠0</author>
      <guid isPermaLink="true">https://freeislet.tistory.com/51</guid>
      <comments>https://freeislet.tistory.com/51#entry51comment</comments>
      <pubDate>Wed, 27 Dec 2023 01:33:16 +0900</pubDate>
    </item>
    <item>
      <title>JavaScript/TypeScript 노트북 vscode 플러그인</title>
      <link>https://freeislet.tistory.com/53</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Jupyter처럼 쓸 수 있는 javascript 용 노트북을 찾다가 &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=donjayamanne.typescript-notebook&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Node.js Notebooks (REPL)&lt;/a&gt;이라는 vscode 플러그인을 찾아서 설치해 보았다.&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;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vscode 플러그인(Cmd + Shift + X) 화면에서 &quot;node.js notebook&quot;을 검색하면, 첫 번째로 Node.js Notebooks (REPL) 플러그인이 뜬다. 이 플러그인을 설치한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2776&quot; data-origin-height=&quot;1192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNmdWu/btsCzHwrDgi/s8qNwQqtuvGoWn2O0Bh1r0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNmdWu/btsCzHwrDgi/s8qNwQqtuvGoWn2O0Bh1r0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNmdWu/btsCzHwrDgi/s8qNwQqtuvGoWn2O0Bh1r0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNmdWu%2FbtsCzHwrDgi%2Fs8qNwQqtuvGoWn2O0Bh1r0%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;2776&quot; height=&quot;1192&quot; data-origin-width=&quot;2776&quot; data-origin-height=&quot;1192&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;h2 data-ke-size=&quot;size26&quot;&gt;노트북 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 코드 셀에서 &lt;code&gt;console.log&lt;/code&gt; 함수를 실행했을 때, 다음과 같이 정상적으로 결과가 출력된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2200&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1jfhW/btsCy6Diuby/5dJZrZdUCkrrGOLU9qk0z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1jfhW/btsCy6Diuby/5dJZrZdUCkrrGOLU9qk0z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1jfhW/btsCy6Diuby/5dJZrZdUCkrrGOLU9qk0z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1jfhW%2FbtsCy6Diuby%2F5dJZrZdUCkrrGOLU9qk0z0%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;2200&quot; height=&quot;454&quot; data-origin-width=&quot;2200&quot; data-origin-height=&quot;454&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;plotly 샘플 코드도 다음과 같이 정상 동작한다. (plotly 패키지는 Node.js Notebook (REPL)에서 자체 지원)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1462&quot; data-origin-height=&quot;1478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWo8yl/btsCEIg12Gh/CtKXbdfVUlYKiXXnsj19AK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWo8yl/btsCEIg12Gh/CtKXbdfVUlYKiXXnsj19AK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWo8yl/btsCEIg12Gh/CtKXbdfVUlYKiXXnsj19AK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWo8yl%2FbtsCEIg12Gh%2FCtKXbdfVUlYKiXXnsj19AK%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;600&quot; height=&quot;607&quot; data-origin-width=&quot;1462&quot; data-origin-height=&quot;1478&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;JavaScript/TypeScript 뿐 아니라 Shell 명령도 실행 가능하다.&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-origin-width=&quot;1714&quot; data-origin-height=&quot;688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQLcMd/btsCANXy5RP/sm9AzfkWgsb4Ah0rPeqxR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQLcMd/btsCANXy5RP/sm9AzfkWgsb4Ah0rPeqxR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQLcMd/btsCANXy5RP/sm9AzfkWgsb4Ah0rPeqxR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQLcMd%2FbtsCANXy5RP%2Fsm9AzfkWgsb4Ah0rPeqxR1%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;700&quot; height=&quot;281&quot; data-origin-width=&quot;1714&quot; data-origin-height=&quot;688&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;h2 data-ke-size=&quot;size26&quot;&gt;외부 패키지 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 패키지를 설치해서 쓰고 싶을 때에는, 일반 프로젝트와 마찬가지로 프로젝트 폴더에 설치해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 clsx 패키지 설치 후에 정상적으로 import가 실행되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2198&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5CrmP/btsCAgrSoX3/dmwdM9eBfNPTKxJKOY7pjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5CrmP/btsCAgrSoX3/dmwdM9eBfNPTKxJKOY7pjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5CrmP/btsCAgrSoX3/dmwdM9eBfNPTKxJKOY7pjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5CrmP%2FbtsCAgrSoX3%2FdmwdM9eBfNPTKxJKOY7pjk%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;2198&quot; height=&quot;496&quot; data-origin-width=&quot;2198&quot; data-origin-height=&quot;496&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;</description>
      <category>개발 기타</category>
      <category>Node.js Notebook (REPL)</category>
      <category>vscode javascript notebook</category>
      <author>깨비아빠0</author>
      <guid isPermaLink="true">https://freeislet.tistory.com/53</guid>
      <comments>https://freeislet.tistory.com/53#entry53comment</comments>
      <pubDate>Tue, 26 Dec 2023 17:35:13 +0900</pubDate>
    </item>
    <item>
      <title>로블록스 맵 만들기 체험</title>
      <link>https://freeislet.tistory.com/52</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2924&quot; data-origin-height=&quot;1902&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf5yxb/btsCy7900Ap/2mVa3TSkPUVLYSji1CUtZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf5yxb/btsCy7900Ap/2mVa3TSkPUVLYSji1CUtZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf5yxb/btsCy7900Ap/2mVa3TSkPUVLYSji1CUtZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf5yxb%2FbtsCy7900Ap%2F2mVa3TSkPUVLYSji1CUtZ0%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;2924&quot; height=&quot;1902&quot; data-origin-width=&quot;2924&quot; data-origin-height=&quot;1902&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;a href=&quot;https://create.roblox.com/docs/ko-kr/studio/setting-up-roblox-studio&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;로블록스 스튜디오를 설치&lt;/a&gt;해서 튜토리얼을 따라해보고, &lt;a href=&quot;https://create.roblox.com/docs/ko-kr/platform&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;가이드 문서&lt;/a&gt;를 훑어본 후에, 기본 템플릿으로 새 맵을 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마켓 플레이스에 공짜 리소스가 많이 있어서 가져다가 맵 꾸미기가 수월했는데, christmas로 검색 후에 크리스마스 트리를 몇 개 배치했다. 배경음악과 눈 파티클, 불꽃놀이 효과도 추가했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2784&quot; data-origin-height=&quot;1990&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJkJ3b/btsCEKy7nJ6/gWe5h4t1nsNphSGn3vL4ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJkJ3b/btsCEKy7nJ6/gWe5h4t1nsNphSGn3vL4ak/img.png&quot; data-alt=&quot;마켓 플레이스에서 christmas로 검색한 모습 (왼쪽 패널)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJkJ3b/btsCEKy7nJ6/gWe5h4t1nsNphSGn3vL4ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJkJ3b%2FbtsCEKy7nJ6%2FgWe5h4t1nsNphSGn3vL4ak%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;2784&quot; height=&quot;1990&quot; data-origin-width=&quot;2784&quot; data-origin-height=&quot;1990&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;마켓 플레이스에서 christmas로 검색한 모습 (왼쪽 패널)&lt;/figcaption&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&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;&quot;와, 크리스마스 트리네! ... 근데 게임은 어딨어?&quot;&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;</description>
      <category>게임 개발</category>
      <category>christmas place</category>
      <category>roblox</category>
      <category>roblox studio</category>
      <author>깨비아빠0</author>
      <guid isPermaLink="true">https://freeislet.tistory.com/52</guid>
      <comments>https://freeislet.tistory.com/52#entry52comment</comments>
      <pubDate>Tue, 26 Dec 2023 15:29:46 +0900</pubDate>
    </item>
    <item>
      <title>Next.js 홈페이지 만들기 - 기본 레이아웃 (3/3)</title>
      <link>https://freeislet.tistory.com/50</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://freeislet.tistory.com/46&quot;&gt;Next.js 프로젝트 생성&lt;/a&gt;&lt;span&gt;&amp;nbsp;글&lt;/span&gt;에서 만든 Next.js 보일러플레이트를 개인 홈페이지로 바꾸기 위해 몇 가지 기본적인 내용을 적용했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://freeislet.tistory.com/49&quot;&gt;Next.js 홈페이지 만들기 - 한글 폰트 (1/3)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://freeislet.tistory.com/48&quot;&gt;Next.js 홈페이지 만들기 - 다크 모드 (2/3)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://freeislet.tistory.com/50&quot;&gt;Next.js 홈페이지 만들기 - 기본 레이아웃 (3/3)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1125&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FZz8K/btsCsp2cmw4/mbhFPGeaJv5Wk9FnsNs781/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FZz8K/btsCsp2cmw4/mbhFPGeaJv5Wk9FnsNs781/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FZz8K/btsCsp2cmw4/mbhFPGeaJv5Wk9FnsNs781/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFZz8K%2FbtsCsp2cmw4%2FmbhFPGeaJv5Wk9FnsNs781%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;1125&quot; height=&quot;528&quot; data-origin-width=&quot;1125&quot; data-origin-height=&quot;528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;웹사이트 구조&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;홈페이지를 포트폴리오 사이트스럽게 만들기 위해 웹사이트 상단에 제목과 네비게이션 바를 포함하는 헤더를 배치하려고 한다. 이를 위해 header.tsx 파일을 추가하였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;세부 페이지는 dynamic route를 사용하지 않고, app 폴더 밑에 직접 페이지용 폴더와 page.tsx 파일을 생성하였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;About 페이지를 위한 about 폴더 하나만 추가했는데, 추후에 &lt;a href=&quot;https://nextra.vercel.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Nextra&lt;/a&gt;, &lt;a href=&quot;https://github.com/jonschlinkert/gray-matter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;gray-matter&lt;/a&gt; 같은 프레임워크 혹은 CMS를 사용하게 되면 프로젝트 구조가 크게 바뀌므로, 우선은 about 하나만 만들었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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-origin-width=&quot;736&quot; data-origin-height=&quot;714&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpfPgv/btsCixG9EAG/VgLS2fTldN0RZ1qqOuC1nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpfPgv/btsCixG9EAG/VgLS2fTldN0RZ1qqOuC1nk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpfPgv/btsCixG9EAG/VgLS2fTldN0RZ1qqOuC1nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpfPgv%2FbtsCixG9EAG%2FVgLS2fTldN0RZ1qqOuC1nk%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;300&quot; height=&quot;291&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;714&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존에 만들어져 있는 다음 코드들도 약간의 수정이 있을 예정이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;app/layout.tsx, page.tsx&lt;/li&gt;
&lt;li&gt;global.css&lt;/li&gt;
&lt;li&gt;tailwind.config.ts&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;웹사이트 헤더&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;63&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p5l1f/btsCodIwdQl/6CSEwbWcLQOvKuSmSuxKQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p5l1f/btsCodIwdQl/6CSEwbWcLQOvKuSmSuxKQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p5l1f/btsCodIwdQl/6CSEwbWcLQOvKuSmSuxKQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp5l1f%2FbtsCodIwdQl%2F6CSEwbWcLQOvKuSmSuxKQk%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;1064&quot; height=&quot;63&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;63&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;웹사이트 상단에 배치할 Header 컴포넌트는 다음 내용을 포함한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;홈페이지 이름&lt;/li&gt;
&lt;li&gt;네비게이션 바&lt;/li&gt;
&lt;li&gt;다크 모드 토글 버튼 (이전 &lt;a style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot; href=&quot;https://freeislet.tistory.com/48&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;다크 모드 글&lt;/a&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;에서 추가한 토글 버튼을 헤더로 옮김)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 요소들의 배치는 responsive 디자인을 위해 flexbox를 사용했고, 네비게이션 바에는 loop 방식과 active link 스타일이 쓰였다.&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;flexbox로 responsive 디자인 적용&lt;/h3&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;1116&quot; data-origin-height=&quot;93&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r7d4c/btsCnHRjefa/hTrkiK7Oc0x3A4cJMKjl31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r7d4c/btsCnHRjefa/hTrkiK7Oc0x3A4cJMKjl31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r7d4c/btsCnHRjefa/hTrkiK7Oc0x3A4cJMKjl31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr7d4c%2FbtsCnHRjefa%2FhTrkiK7Oc0x3A4cJMKjl31%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;1116&quot; height=&quot;93&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;93&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;responsive-header.gif&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u20E2/btsCpd9zcSE/Lsl0tmHt7H46pqA5cKxq2k/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u20E2/btsCpd9zcSE/Lsl0tmHt7H46pqA5cKxq2k/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u20E2/btsCpd9zcSE/Lsl0tmHt7H46pqA5cKxq2k/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/u20E2/btsCpd9zcSE/Lsl0tmHt7H46pqA5cKxq2k/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;352&quot; data-filename=&quot;responsive-header.gif&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;352&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;이러한 디자인은 flex, flex-wrap, justify-between과 ml-auto 클래스로 쉽게 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 최상단 div에 flex, flex-wrap, justify-between을 넣고, 우측 div(네비게이션 바)에 ml-auto를 추가하면 위 애니메이션과 같이 평소에는 양쪽으로 나뉘어 있다가 화면이 좁아지면 우측 영역이 아래로 내려온다.&lt;/p&gt;
&lt;pre id=&quot;code_1703138117263&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    &amp;lt;div className=&quot;flex flex-wrap items-end justify-between ...&quot;&amp;gt;
      &amp;lt;h1 className=&quot;flex-shrink-0 ...&quot;&amp;gt;
        Portfolio&amp;amp;nbsp;
        &amp;lt;span className=&quot;...&quot;&amp;gt;
          under construction
        &amp;lt;/span&amp;gt;
      &amp;lt;/h1&amp;gt;
      &amp;lt;div className=&quot;ml-auto mt-4&quot;&amp;gt;
        ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reusing Styles (loop)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tailwind를 사용하면 class를 지정하는 코드가 길어질 수밖에 없는데, 네비게이션 링크들처럼 태그가 반복될 때에는 수정사항을 모든 태그의 className에 적용해야 하므로 작업이 더욱 귀찮아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제를 해결하기 위해 tailwind에서 제안하는 방법들 중 &lt;a href=&quot;https://tailwindcss.com/docs/reusing-styles#loops&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;loop 방식&lt;/a&gt;을 사용하여 링크 목록을 구현했다. Link 태그를 여러 개 사용하는 대신에, 아래와 같이 [title, url] 배열을 만들어서 map 함수를 사용하는 방식이다.&lt;/p&gt;
&lt;pre id=&quot;code_1703121661534&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export function Header() {
  const navItems = [
    ['Home', '/'],
    ['Portfolio 1', '/portfolio1'],
    ['Portfolio 2', '/portfolio2'],
    ['Portfolio 3', '/portfolio3'],
    ['About', '/about'],
  ]

  return (
    ...
        &amp;lt;nav&amp;gt;
          {navItems.map(([title, url], index) =&amp;gt; (
            &amp;lt;Link href={url} key={index}&amp;gt;{title}&amp;lt;/Link&amp;gt;
          ))}
        &amp;lt;/nav&amp;gt;
    ...
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Active Link&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;활성화된 페이지 링크에 스타일을 적용하려면 usePathname 함수를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 Next.js 공식 문서의&amp;nbsp;&lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#checking-active-links&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;active link 예제&lt;/a&gt;이다.&lt;/p&gt;
&lt;pre id=&quot;code_1703122049253&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client'
 
import { usePathname } from 'next/navigation'
import Link from 'next/link'
 
export function Links() {
  const pathname = usePathname()
 
  return (
    &amp;lt;nav&amp;gt;
      &amp;lt;ul&amp;gt;
        &amp;lt;li&amp;gt;
          &amp;lt;Link className={`link ${pathname === '/' ? 'active' : ''}`} href=&quot;/&quot;&amp;gt;
            Home
          &amp;lt;/Link&amp;gt;
        &amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;
          &amp;lt;Link
            className={`link ${pathname === '/about' ? 'active' : ''}`}
            href=&quot;/about&quot;
          &amp;gt;
            About
          &amp;lt;/Link&amp;gt;
        &amp;lt;/li&amp;gt;
      &amp;lt;/ul&amp;gt;
    &amp;lt;/nav&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;위 예제를 참고하여 활성화된 링크일 경우의 폰트 색상과 weight를 별도로 지정했고, hover 스타일도 추가했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;395&quot; data-origin-height=&quot;54&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VI9zF/btsCtcB8cAz/1KrA82sN0E3kxQ1Sed3eAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VI9zF/btsCtcB8cAz/1KrA82sN0E3kxQ1Sed3eAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VI9zF/btsCtcB8cAz/1KrA82sN0E3kxQ1Sed3eAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVI9zF%2FbtsCtcB8cAz%2F1KrA82sN0E3kxQ1Sed3eAk%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;395&quot; height=&quot;54&quot; data-origin-width=&quot;395&quot; data-origin-height=&quot;54&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1703156731556&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export function Header() {
  const pathname = usePathname()
  const isActive = (url: string) =&amp;gt; pathname === url
  ...
  
  return (
    ...
            &amp;lt;Link
              className={
                `... hover:bg-theme-active/30
                ${isActive(url) ? 'text-theme-active font-bold' : ''}`
              }
              href={url}
              key={index}
            &amp;gt;{title}&amp;lt;/Link&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;theme-active 컬러는&amp;nbsp;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://freeislet.tistory.com/48&quot;&gt;다크 모드 글&lt;/a&gt;&amp;nbsp; &quot;light/dark 모드 공통 색상 class 정의&quot;에 정리한 방식으로 추가한 커스텀 컬러이다.&lt;/p&gt;
&lt;pre id=&quot;code_1703160486282&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// global.css
...

@layer base {
  :root {
    ...
    --theme-active: 48 112 236;
  }

  :root.dark {
    ...
    --theme-active: 55 126 255;
  }
}

...


// tailwind.config.css
import type { Config } from 'tailwindcss'

const config: Config = {
  ...
  theme: {
    extend: {
      colors: {
        ...
        'theme-active': &quot;rgb(var(--theme-active) / &amp;lt;alpha-value&amp;gt;)&quot;,
      },
    },
  },
}
export default config&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 Header 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완성된 Header 컴포넌트 코드는 다음과 같다. (app/header.tsx)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;최상단 div의 header-layout은 global.css에 추가한 커스텀 클래스인데, 아래 &quot;레이아웃을 위한 커스텀 클래스&quot; 섹션에 정리&lt;/blockquote&gt;
&lt;pre id=&quot;code_1703121809869&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client'

import { usePathname } from 'next/navigation'
import Link from 'next/link'
import { ThemeMode } from '@/components/theme-mode'

export function Header() {
  const pathname = usePathname()
  const isActive = (url: string) =&amp;gt; pathname === url
  const navItems = [
    ['Home', '/'],
    ['Portfolio 1', '/portfolio1'],
    ['Portfolio 2', '/portfolio2'],
    ['Portfolio 3', '/portfolio3'],
    ['About', '/about'],
  ]

  return (
    &amp;lt;div className=&quot;header-layout flex flex-wrap items-end justify-between&quot;&amp;gt;
      &amp;lt;h1 className=&quot;flex-shrink-0 pr-6 text-4xl font-extrabold italic&quot;&amp;gt;
        Portfolio&amp;amp;nbsp;
        &amp;lt;span className=&quot;text-3xl font-medium&quot;&amp;gt;
          under construction
        &amp;lt;/span&amp;gt;
      &amp;lt;/h1&amp;gt;
      &amp;lt;div className=&quot;ml-auto mt-4&quot;&amp;gt;
        &amp;lt;nav className=&quot;inline text-sm font-medium&quot;&amp;gt;
          {navItems.map(([title, url], index) =&amp;gt; (
            &amp;lt;Link
              className={
                `py-1 px-2 rounded-sm hover:bg-theme-active/30
                ${isActive(url) ? 'text-theme-active font-bold' : ''}`
              }
              href={url}
              key={index}
            &amp;gt;{title}&amp;lt;/Link&amp;gt;
          ))}
        &amp;lt;/nav&amp;gt;
        &amp;lt;div className=&quot;inline ml-4&quot;&amp;gt;
          &amp;lt;ThemeMode /&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 레이아웃&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;main 영역 100vh 이슈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Header 컴포넌트는 app/layout.tsx에 간단하게 추가할 수 있지만, 헤더로 인해 기존 레이아웃에 이슈가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보일러플레이트의 main 태그에는 min-h-screen 클래스가 쓰이는데, min-height에 100vh(창 전체 크기)를 설정하는 클래스이다. 여기에 flexbox justify-between을 함께 사용하여 아래와 같이 하위 div 2개가 전체 창 상/하단으로 나뉘도록 하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;337&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buSbfm/btsCkm0XQcI/KKQkViGbzSYFhb5nYKNxy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buSbfm/btsCkm0XQcI/KKQkViGbzSYFhb5nYKNxy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buSbfm/btsCkm0XQcI/KKQkViGbzSYFhb5nYKNxy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuSbfm%2FbtsCkm0XQcI%2FKKQkViGbzSYFhb5nYKNxy1%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;1056&quot; height=&quot;337&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;337&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;extra-margin-with-header.gif&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qn3XP/btsCqxsFNl3/wutAhEmYUlUnFfhoJ53mTK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qn3XP/btsCqxsFNl3/wutAhEmYUlUnFfhoJ53mTK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qn3XP/btsCqxsFNl3/wutAhEmYUlUnFfhoJ53mTK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/qn3XP/btsCqxsFNl3/wutAhEmYUlUnFfhoJ53mTK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;780&quot; height=&quot;326&quot; data-filename=&quot;extra-margin-with-header.gif&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;326&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;이를 해결하기 위해 전체 디자인을 flexbox에 넣는 방법을 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 body에 다음과 같이 flexbox 및 min-h-screen 클래스를 적용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1703132151282&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;      &amp;lt;body className={`flex flex-col min-h-screen ${notoSansKr.className}`}&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;그리고, main 태그에서 min-h-screen을 지우고 flex-grow를 적용하면, 아래와 같이 밑으로 넘치는 영역 없이 화면에 딱 맞게 레이아웃이 적용된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1061&quot; data-origin-height=&quot;501&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjjOG2/btsCnIoMbfa/3REgQnPcpGEVFClK4n4ePk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjjOG2/btsCnIoMbfa/3REgQnPcpGEVFClK4n4ePk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjjOG2/btsCnIoMbfa/3REgQnPcpGEVFClK4n4ePk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjjOG2%2FbtsCnIoMbfa%2F3REgQnPcpGEVFClK4n4ePk%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;1061&quot; height=&quot;501&quot; data-origin-width=&quot;1061&quot; data-origin-height=&quot;501&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;h3 data-ke-size=&quot;size23&quot;&gt;전체 레이아웃 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app/layout.tsx의 html 부분 코드는 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;app/page.tsx 전체를 감싸던 &amp;lt;main&amp;gt; 태그를 layout.tsx로 옮겨오는 변화도 있다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1703132400778&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    &amp;lt;html lang=&quot;ko&quot;&amp;gt;
      &amp;lt;body className={`flex flex-col min-h-screen ${notoSansKr.className}`}&amp;gt;
        &amp;lt;Providers&amp;gt;
          &amp;lt;Header /&amp;gt;
          &amp;lt;main className=&quot;flex-grow flex flex-col items-center&quot;&amp;gt;
            {children}
          &amp;lt;/main&amp;gt;
        &amp;lt;/Providers&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;레이아웃을 위한 커스텀 클래스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;About 페이지 등의 세부 페이지들에 공통된 레이아웃이 적용되어야 한다. (너비, 여백 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데, 너비나 여백 스타일을 app/layout.tsx 같은 상위 레이아웃에 적용하게 되면, 세부 페이지에서 상위 프레임보다 큰 영역을 사용하고 싶을 때 음수 마진을 사용해야하는 번거로움이 있다. 따라서, 아래와 같이 세부 페이지 레이아웃을 위한 main-layout 클래스를 만들고, 각각의 페이지에서 사용하도록 하였다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;헤더 영역의 레이아웃도 함께 보기 위해 header-layout 클래스도 추가함&lt;/blockquote&gt;
&lt;pre id=&quot;code_1703158458387&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@layer components {
  .header-layout {
    @apply flex-shrink-0 mx-[5%] p-2
  }

  .main-layout {
    @apply max-w-5xl w-full p-4
  }
}&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;about 페이지에 다음과 같이 main-layout 클래스를 사용하면, 1024px&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;너비&lt;/span&gt;(max-w-5xl)와 16px 여백(p-4)이 적용된다.&lt;/p&gt;
&lt;pre id=&quot;code_1703160731977&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function About() {
  return (
    &amp;lt;div className=&quot;main-layout&quot;&amp;gt;
      &amp;lt;span&amp;gt;About...&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1087&quot; data-origin-height=&quot;139&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HWkmw/btsCoFMA2RG/X1LJAOuYRjbpxUhIPjvTTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HWkmw/btsCoFMA2RG/X1LJAOuYRjbpxUhIPjvTTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HWkmw/btsCoFMA2RG/X1LJAOuYRjbpxUhIPjvTTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHWkmw%2FbtsCoFMA2RG%2FX1LJAOuYRjbpxUhIPjvTTk%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;1087&quot; height=&quot;139&quot; data-origin-width=&quot;1087&quot; data-origin-height=&quot;139&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;h2 data-ke-size=&quot;size26&quot;&gt;결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 내용이 모두 적용된 모습은 다음과 같다. (iframe)&lt;/p&gt;
&lt;div style=&quot;margin-left: min(0px, 50% - 100px - 20vw); margin-right: min(0px, 50% - 100px - 30vw); position: relative; z-index: 9999;&quot;&gt;&lt;iframe src=&quot;https://freeislet.github.io/home-old/&quot; width=&quot;100%&quot; height=&quot;500px&quot;&gt;&lt;/iframe&gt;&lt;/div&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;URL:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://freeislet.github.io/home-old/&quot;&gt;https://freeislet.github.io/home-old/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;소스코드:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/freeislet/home-old&quot;&gt;https://github.com/freeislet/home-old&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>웹 개발</category>
      <category>active link</category>
      <category>flex</category>
      <category>layout</category>
      <category>next.js</category>
      <category>Responsive Design</category>
      <category>reusing tailwind styles</category>
      <author>깨비아빠0</author>
      <guid isPermaLink="true">https://freeislet.tistory.com/50</guid>
      <comments>https://freeislet.tistory.com/50#entry50comment</comments>
      <pubDate>Thu, 21 Dec 2023 21:00:03 +0900</pubDate>
    </item>
  </channel>
</rss>