<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>한 개발자의 노트패드</title>
    <link>https://weaklion1.tistory.com/</link>
    <description>현 프론트엔드 개발자</description>
    <language>ko</language>
    <pubDate>Tue, 21 Jul 2026 17:27:41 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>weaklion</managingEditor>
    <image>
      <title>한 개발자의 노트패드</title>
      <url>https://tistory1.daumcdn.net/tistory/2760935/attach/55e045c087f845018d1311a31a49ff54</url>
      <link>https://weaklion1.tistory.com</link>
    </image>
    <item>
      <title>react로 그림판을 만든 뒤 후기 [0]</title>
      <link>https://weaklion1.tistory.com/58</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;react와 konva를 이용해서 그림판을 만들었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;그림판.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4vDeL/dJMcadtIYkD/grtFNtyh0zUdk9reTABEU1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4vDeL/dJMcadtIYkD/grtFNtyh0zUdk9reTABEU1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4vDeL/dJMcadtIYkD/grtFNtyh0zUdk9reTABEU1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/b4vDeL/dJMcadtIYkD/grtFNtyh0zUdk9reTABEU1/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;500&quot; height=&quot;350&quot; data-filename=&quot;그림판.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;konva는 js를 이용해서 canvas를 2d로 제어할 수 있는 라이브러리입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래는 HTML의 자체 canvas API를 쓸까 했는데 그러면 하나의 기능을 만드는데 너무 긴 시간이 소비될 것 같아서 안 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그냥 canvas API를 쓸 걸 그랬습니다.&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;이번엔 ai와 적극적으로 소통해서 만들었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ai를 이용해서 고작 이 딴 거나 만들었냐? 고 하면 딱히 할 말은 없는데,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서 중요한 건 ai에 의존하는 게 아니라, ai를 통한 코드 퀄리티를 높이는 거라, 우선 코드를 입력한 뒤&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 코드를 리팩터링 할 때 ai를 쓰는 방향으로 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 좀 봐주세요. ai로 코드 찍는 건 양심에 가책이 있어서 안 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 그림판에 구현되어 있는 기능은 총 6가지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리기, 지우개, 텍스트, 선택, 되돌리기, undo, redo.&amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단해 보이는 기능들이고 실제로 대부분은 만드는데 어렵지 않았습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;konva에 있는 데모들에 다 있었던 기능들이고, 따라만 하면 다 나오는 것들이었죠.&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;konva의 공식 홈페이지에 있는 demo는 globalCompositeOperation를 destination-out 하는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;canvas API에서도 지울 때 사용하는 방법이지만 이렇게&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;그림판을 만들면서 참고한 프로젝트는 canva draw와 excalidraw입니다.&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;그리고 내가 그린 도형에 대한 데이터를 저장할 수 없다면, 추후 리팩토링도 불가능할 거라 판단되어, 도형을 지울 땐 destination-out이 아닌 다른 방법으로 지워야만 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그게 뭐냐면...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;프로젝트 구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그전에 프로젝트를 어떤 구조로 만들었는지 잠깐 복기 형식으로 적도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어차피 이 블로그도 제가 공부한 것들을 복기 형식으로 적는 거라, 딱히 불만은 없는 걸로 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 골자는 FSD(Feature-Sliced Design) 아키텍처입니다. 본래 fsd는 대형 프로젝트에 적합하다고 알려져 있고, 면접에서도 그렇게 대답하지만 공식 레퍼런스에선&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;FSD는 다음 조건에 해당하면 도입할 수 있습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #1c1e21; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프런트엔드(웹,&lt;/b&gt; 모바일, 데스크톱 등)를 개발하고 있고&lt;/li&gt;
&lt;li&gt;&lt;b&gt;라이브러리&lt;/b&gt;가 아닌&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;애플리케이션&lt;/b&gt;을 개발하고 있다면&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;언어, UI 프레임워크, 상태 관리 도구에 대한 제약은 없습니다.&lt;br /&gt;Monorepo 환경에서도 사용 가능하며, 프로젝트 구조를 나눠&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;점진적으로 적용&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 나와있네요. 즉, 프런트엔드로 개발하면서, 라이브러리가 아니라면 그냥 써도 된다는 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fsd에 관한 자세한 글은 아래를 참조해 주세요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://weaklion1.tistory.com/53&quot;&gt;기능 분할 설계(Feature-Sliced Design, FSD)란&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765352773052&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;기능 분할 설계(Feature-Sliced Design, FSD)란&quot; data-og-description=&quot;Feature-Sliced Design(FSD)는 프론트엔드 애플리케이션의 구조를 잡는 아키텍처 방법론입니다. FSD는 이름 그대로 기능 단위로 분할하는 것에 초점이 맞춰져 있으며 현재 많은 주목을 받는 아키텍처 중&quot; data-og-host=&quot;weaklion1.tistory.com&quot; data-og-source-url=&quot;https://weaklion1.tistory.com/53&quot; data-og-url=&quot;https://weaklion1.tistory.com/53&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bxKtJb/hyZPifvfOv/vpYMiF0A03RSwfIL24JJH1/img.jpg?width=800&amp;amp;height=445&amp;amp;face=0_0_800_445,https://scrap.kakaocdn.net/dn/ykPJ0/hyZPdL0E22/C9kyvCTk6tfIKziUlGb2a1/img.jpg?width=800&amp;amp;height=445&amp;amp;face=0_0_800_445,https://scrap.kakaocdn.net/dn/Dj0AR/hyZPaobSVr/IqAXqC1RsiFav2vde8Kl5k/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024&quot;&gt;&lt;a href=&quot;https://weaklion1.tistory.com/53&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://weaklion1.tistory.com/53&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bxKtJb/hyZPifvfOv/vpYMiF0A03RSwfIL24JJH1/img.jpg?width=800&amp;amp;height=445&amp;amp;face=0_0_800_445,https://scrap.kakaocdn.net/dn/ykPJ0/hyZPdL0E22/C9kyvCTk6tfIKziUlGb2a1/img.jpg?width=800&amp;amp;height=445&amp;amp;face=0_0_800_445,https://scrap.kakaocdn.net/dn/Dj0AR/hyZPaobSVr/IqAXqC1RsiFav2vde8Kl5k/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024');&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;기능 분할 설계(Feature-Sliced Design, FSD)란&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Feature-Sliced Design(FSD)는 프론트엔드 애플리케이션의 구조를 잡는 아키텍처 방법론입니다. FSD는 이름 그대로 기능 단위로 분할하는 것에 초점이 맞춰져 있으며 현재 많은 주목을 받는 아키텍처 중&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;weaklion1.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fsd에 관해 좀 더 레퍼런스를 찾다 보면 알겠지만, 명확한 정의는 없습니다.&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;가령 feature라는 단어의 뜻만 보고 특정한 entity의 crud만 집어넣어야 한다라고 해석할 수도 있는 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 사실 fsd 홈페이지에서도 명확하게 정의가 돼있지 않아 난감했는데, 최근에 정의된 게 있더라고요.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h4 id=&quot;entity-간-관계&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Entity 간 관계&lt;/h4&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;원칙적으로 Slice끼리는 서로 모르면 좋습니다.&lt;br /&gt;하지만 현실적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;다른 Entity를 포함&lt;/b&gt;하거나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;상호작용&lt;/b&gt;하는 경우가 있습니다.&lt;br /&gt;이때는 로직을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;상위 Layer(Feature/Page)로&lt;/b&gt; 올려 처리하세요.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 한 Entity 데이터에 다른 Entity가 포함된다면,&lt;br /&gt;@x&lt;span&gt;&amp;nbsp;&lt;/span&gt;표기를 사용해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;교차 Public API&lt;/b&gt;로 연결을 명시적으로 드러내세요.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://feature-sliced.design/kr/docs/reference/layers#feature&quot;&gt;Layer | Feature-Sliced Design&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Entity 간에 관계에서도 다른 entity를 포함하거나 상호작용을 한다면 상위 layer에서 처리하라고 레퍼런스에선 이야기를 하고 있었습니다.&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;720&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcsiuW/dJMcaiV58JU/jRZ2s2FdPKMra00hGM4sSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcsiuW/dJMcaiV58JU/jRZ2s2FdPKMra00hGM4sSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcsiuW/dJMcaiV58JU/jRZ2s2FdPKMra00hGM4sSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcsiuW%2FdJMcaiV58JU%2FjRZ2s2FdPKMra00hGM4sSk%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;305&quot; height=&quot;406&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;958&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;이렇게 보니 굳이 fsd로 할 필요가 없긴 합니다. 그냥 편한 대로 하면 되겠습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 어쨌든 저는 fsd라는 아키텍처에 대해 좀 더 공부하고 싶었고, 좋은 계기가 되긴 했습니다.&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;특이한 점은 entites에 entity가 하나에, features에 canvas와 관련된 기능만 있다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엥? 아까 네가 말한 거랑 다른데?라고 할 수 있는데 맞습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 entites에 line과 text, 그리고 eraser 총 3개의 엔티티가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 3개의 엔티티가 상호작용하는 걸 features에서 관리했는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ui만 다르고, 내용은 비슷한&amp;nbsp; 컴포넌트를 나눌 필요가 없다고 생각해서 shape라는 엔티티 안에 ui만 따로 해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리하는 걸로 결정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;features에도 마찬가지로 shape와 엔티티의 기능들을 처리하고 있지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;canvas라는 거대한 종이 안에서 처리되는 기능들이기 때문에 이름을 명시적으로 바꿨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 자세한 건 기능을 알아볼 때 보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지는 딱히 특별한 건 없습니다. 다만 widgets이 없는데, 이 자리엔 canvas가 들어가도 무방합니다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>react</category>
      <author>weaklion</author>
      <guid isPermaLink="true">https://weaklion1.tistory.com/58</guid>
      <comments>https://weaklion1.tistory.com/58#entry58comment</comments>
      <pubDate>Wed, 10 Dec 2025 17:17:35 +0900</pubDate>
    </item>
    <item>
      <title>ai를 이용해 웹 사이트(or 유튜브)를 번역해보자.</title>
      <link>https://weaklion1.tistory.com/57</link>
      <description>&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;p data-ke-size=&quot;size16&quot;&gt;1. 우선 번역해야 할 사이트를 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 번역이 필요할 레퍼런스가 필요하겠죠. 지금은 예시로 아래 사이트를 이용하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nobuhikosawai.com/en/behind-the-eraser/&quot;&gt;Behind the eraser&lt;/a&gt; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대충 사이트 내용은 line을 erase 할 수 있는 알고리즘입니다.&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;2. Google NoteBookLM&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://notebooklm.google.com/&quot;&gt;NotebookLM&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google NoteBookLM은 구글에서 만든 인공지능 노트 애플리케이션입니다.&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;새노트를 만들면 입력해야 할 소스를 업로드 하라고 하는데 우리는 여기에 번역해야할 사이트 or 유튜브를 입력하면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2085&quot; data-origin-height=&quot;1324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgTcTE/dJMcac2vUvw/eoN8nMxD7uXFtJFF1pKbh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgTcTE/dJMcac2vUvw/eoN8nMxD7uXFtJFF1pKbh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgTcTE/dJMcac2vUvw/eoN8nMxD7uXFtJFF1pKbh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgTcTE%2FdJMcac2vUvw%2FeoN8nMxD7uXFtJFF1pKbh0%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;2085&quot; height=&quot;1324&quot; data-origin-width=&quot;2085&quot; data-origin-height=&quot;1324&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;좌측의 소스를 클릭하면 ai가 출력한 원문을 볼 수 있게 됩니다.&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;2104&quot; data-origin-height=&quot;1360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgMIId/dJMcahirTxP/TRvVV2YKVMWYOoDFjADwn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgMIId/dJMcahirTxP/TRvVV2YKVMWYOoDFjADwn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgMIId/dJMcahirTxP/TRvVV2YKVMWYOoDFjADwn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgMIId%2FdJMcahirTxP%2FTRvVV2YKVMWYOoDFjADwn0%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;2104&quot; height=&quot;1360&quot; data-origin-width=&quot;2104&quot; data-origin-height=&quot;1360&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;2192&quot; data-origin-height=&quot;1318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJ8QTS/dJMcacIdfO4/yoD7ozA3bknptvPnUyhPU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJ8QTS/dJMcacIdfO4/yoD7ozA3bknptvPnUyhPU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJ8QTS/dJMcacIdfO4/yoD7ozA3bknptvPnUyhPU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJ8QTS%2FdJMcacIdfO4%2FyoD7ozA3bknptvPnUyhPU0%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;2192&quot; height=&quot;1318&quot; data-origin-width=&quot;2192&quot; data-origin-height=&quot;1318&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;원문을 복사한 후 사용하는 ai에게 물으면 원하는 결과를 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 같은 경우 복사한 원문을 google gemini를 통해 다른 프롬프트 없이 번역을 요청했습니다.&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;1578&quot; data-origin-height=&quot;1368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/box4oz/dJMcadmOEXJ/M9Qk5eKdZNaNKvlUg3A8f1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/box4oz/dJMcadmOEXJ/M9Qk5eKdZNaNKvlUg3A8f1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/box4oz/dJMcadmOEXJ/M9Qk5eKdZNaNKvlUg3A8f1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbox4oz%2FdJMcadmOEXJ%2FM9Qk5eKdZNaNKvlUg3A8f1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1578&quot; height=&quot;1368&quot; data-origin-width=&quot;1578&quot; data-origin-height=&quot;1368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 원하는 결과를 얻을수 있었습니다, 레퍼런스가 유튜브라도 아래와 같이 동일한 결과를 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 같은 경우 레퍼런스의 양이 적어 기본 버전을 이용했지만, 만약 번역하려는 본문의 양이 많다면 pro버전 사용을 추천합니다.&lt;/p&gt;</description>
      <category>개발지식</category>
      <author>weaklion</author>
      <guid isPermaLink="true">https://weaklion1.tistory.com/57</guid>
      <comments>https://weaklion1.tistory.com/57#entry57comment</comments>
      <pubDate>Tue, 11 Nov 2025 17:51:22 +0900</pubDate>
    </item>
    <item>
      <title>chatGPT 브라우저가 출시됐네요</title>
      <link>https://weaklion1.tistory.com/56</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;https://chatgpt.com/ko-KR/atlas/&quot; href=&quot;https://chatgpt.com/ko-KR/atlas/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://chatgpt.com/ko-KR/atlas/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ai랑 접목해서 브라우저의 정의를 바꾼 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 프론트엔드 개발자로 새로운 지평을 연 듯한 느낌이 드네요.&lt;/p&gt;</description>
      <category>개발소식</category>
      <author>weaklion</author>
      <guid isPermaLink="true">https://weaklion1.tistory.com/56</guid>
      <comments>https://weaklion1.tistory.com/56#entry56comment</comments>
      <pubDate>Wed, 22 Oct 2025 16:57:49 +0900</pubDate>
    </item>
    <item>
      <title>useImperativeHandle는 무엇인가?</title>
      <link>https://weaklion1.tistory.com/55</link>
      <description>&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;프론트엔드 개발자로써 사실 리액트보다 vue를 쓰는 기간이 더 많아서, 아직 react에 적응하는 기간인데&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;바로 vue는 양방향의 데이터바인딩을 지원하지만, react는 오직 단방향만 지원한다는 점입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 이 부분을 단적으로 보여주는 react의 useImperativeHandle에 대해서 적어보도록 하겠습니다.&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;useImperativeHandle&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useImperativeHandle은 ref를 통해 부모 컴포넌트에 노출할 인스턴스 값을 사용자 커스텀할 수 있는 기능입니다. 이를 통해 부모 컴포넌트는 자식 컴포넌트에 원하는 인스턴스를 호출할 수있습니다. 주로 fowardRef와 같이 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760603821550&quot; class=&quot;bash&quot; style=&quot;color: #333333; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;const CustomComponent = forwardRef((props, ref) =&amp;gt; {
	//...

  useImperativeHandle(ref, () =&amp;gt; ({
    handleCustomLog: (type, value) =&amp;gt; {
      console.log('custom log')
    }
  }));

  // ... 
});&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1760603821552&quot; class=&quot;bash&quot; style=&quot;color: #333333; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;const ParentComponent = () =&amp;gt; {
  const customRef = useRef(null);

  const handleLog = () =&amp;gt; {
    if (customRef.current) {
      customRef.current.handleCustomLog();

    }
  };

  return (
    &amp;lt;CustomComponent
      ref={customRef}
      /..
    /&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;react 19 버전을 기준으로 forwardRef는 삭제 됐으니, ref로 사용하셔도 무방합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쨌든 코드를 작성해보니 vue의 defineExpose()와 유사합니다. vue에서는 defineExpose()를 통한 양방향의 데이터바인딩을 권장하고 있지만 , react에서는 권장하지 않는 방식입니다.&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;명령형(Imperative) 코드를 조장함&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;React는 &quot;무엇을(what)&quot; 렌더링할지 선언하는 방식을 지향하는데, useImperativeHandle은 &quot;어떻게(how)&quot; 동작할지 명령하는 방식입니다.&lt;/li&gt;
&lt;li&gt;부모가 자식의 메서드를 직접 호출하는 것은 명령형 패턴입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컴포넌트 캡슐화를 깨뜨림&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자식 컴포넌트의 내부 구현을 부모에게 노출하게 되어, 컴포넌트의 독립성이 약화됩니다.&lt;/li&gt;
&lt;li&gt;자식의 내부 구조를 변경하면 부모 코드도 함께 수정해야 할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 흐름을 추적하기 어려움&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;props를 통한 단방향 흐름이 아니라 ref를 통한 직접 제어이기 때문에, 코드를 읽을 때 데이터 흐름을 파악하기 힘듭니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트와 유지보수가 어려움&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ref를 통한 직접 조작은 테스트 코드 작성이 복잡해지고, 디버깅도 어렵습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 사용해야 한다면 사용하는 게 맞습니다. 저는 dom 제어를 위해서 사용할 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>react</category>
      <author>weaklion</author>
      <guid isPermaLink="true">https://weaklion1.tistory.com/55</guid>
      <comments>https://weaklion1.tistory.com/55#entry55comment</comments>
      <pubDate>Thu, 16 Oct 2025 17:59:52 +0900</pubDate>
    </item>
    <item>
      <title>[vue3] defineModel 알아보기</title>
      <link>https://weaklion1.tistory.com/54</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;vue의 가장 큰 특징 중 하나인 양방향 데이터 바인딩에서 주로 사용되는 기능 중 하나가 v-model 입니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748420814244&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup&amp;gt;
const model = ref('');
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;input v-model=&quot;model&quot; /&amp;gt;
&amp;lt;/template&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;(아주)지난 포스팅에서 v-model의 대략적인 사용법에 대해 글을 적은 적이 있습니다. 개인적으로 vue의 가장 강력한 장점 중 하나라 생각하는데 최근에 나온 vue 3.4에서 이걸 더욱 편리하게 해주는 defineModel이 등장했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748420987443&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup&amp;gt;
const model = defineModel()
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;input v-model=&quot;model&quot; /&amp;gt;
&amp;lt;/template&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;뭔 차이야...? 생각 할 수 있는데 좀 더 봅시다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748421626398&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;InputField.vue

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
const modelValue = defineModel&amp;lt;string&amp;gt;()
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;input
    type=&quot;text&quot;
    v-model=&quot;modelValue&quot;
    class=&quot;border px-2 py-1 rounded&quot;
    placeholder=&quot;입력하세요&quot;
  /&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748421653644&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;App.vue

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref } from 'vue'
import InputField from './components/InputField.vue'

const text = ref('초기값')
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;p-4&quot;&amp;gt;
    &amp;lt;InputField v-model=&quot;text&quot; /&amp;gt;
    &amp;lt;p class=&quot;mt-2 text-gray-600&quot;&amp;gt;입력된 값: {{ text }}&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&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;코드를 작성하다 보면 공통 컴포넌트를 만들고 부모가 상태값을 제어하는 경우가 많습니다. vue에선 v-model을 통해 양방향 데이터 바인딩으로 값을 구현하죠.&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;부모에서 컴포넌트에 v-model에 제어할 상태값을 넣은 후 자식에서 그 값을 defineModel로 가져와 다시 v-model로 제어합니다. 이걸 defineModel 없이 써 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748421812967&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { defineProps, defineEmits } from 'vue'

const props = defineProps&amp;lt;{
  modelValue: string
}&amp;gt;()

const emit = defineEmits&amp;lt;{
  (e: 'update:modelValue', value: string): void
}&amp;gt;()
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;input
    type=&quot;text&quot;
    :value=&quot;props.modelValue&quot;
    @input=&quot;event =&amp;gt; emit('update:modelValue', event.target.value)&quot;
    class=&quot;border px-2 py-1 rounded&quot;
    placeholder=&quot;입력하세요&quot;
  /&amp;gt;
&amp;lt;/template&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;defineModel이 없을땐 수동으로 modelValue를 명시하고 emit으로 처리해야 합니다. 타입도 직접 명시해야 하며 코드량도 당연히 많아지겠죠.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;input 하나만 해도 이정돈데, 더 많은 상태값을 다뤄야 한다면 그만큼 코드의 리팩토링이 힘들어질 수 있습니다.&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;defineModel을 썼을 때&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748422667166&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UserForm.vue

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
type UserInfo = {
  name: string
  email: string
}

const userInfo = defineModel&amp;lt;UserInfo&amp;gt;('userInfo')
const agreed = defineModel&amp;lt;boolean&amp;gt;('agreed', { required : true })
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;space-y-3&quot;&amp;gt;
    &amp;lt;input
      v-model=&quot;userInfo.name&quot;
      placeholder=&quot;이름&quot;
      class=&quot;border px-2 py-1 rounded w-full&quot;
    /&amp;gt;
    &amp;lt;input
      v-model=&quot;userInfo.email&quot;
      placeholder=&quot;이메일&quot;
      class=&quot;border px-2 py-1 rounded w-full&quot;
    /&amp;gt;
    &amp;lt;label class=&quot;flex items-center gap-2&quot;&amp;gt;
      &amp;lt;input type=&quot;checkbox&quot; v-model=&quot;agreed&quot; /&amp;gt;
      약관에 동의합니다
    &amp;lt;/label&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1748422760046&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;App.vue

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref } from 'vue'
import UserForm from './components/UserForm.vue'

const userInfo = ref({ name: '홍길동', email: 'hong@example.com' })
const agreed = ref(false)
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;UserForm v-model:userInfo=&quot;userInfo&quot; v-model:agreed=&quot;agreed&quot; /&amp;gt;
  &amp;lt;p&amp;gt;입력 결과: {{ userInfo }} / 동의 여부: {{ agreed }}&amp;lt;/p&amp;gt;
&amp;lt;/template&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;defineModel 이전&lt;/p&gt;
&lt;pre id=&quot;code_1748422838326&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { defineProps, defineEmits } from 'vue'

type UserInfo = {
  name: string
  email: string
}

const props = defineProps&amp;lt;{
  userInfo: UserInfo
  agreed: boolean
}&amp;gt;()

const emit = defineEmits&amp;lt;{
  (e: 'update:userInfo', value: UserInfo): void
  (e: 'update:agreed', value: boolean): void
}&amp;gt;()

// 반응형 객체로 만들기 위해 복사
const localUserInfo = { ...props.userInfo }

watchEffect(() =&amp;gt; {
  emit('update:userInfo', localUserInfo)
})
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;space-y-3&quot;&amp;gt;
    &amp;lt;input
      v-model=&quot;localUserInfo.name&quot;
      placeholder=&quot;이름&quot;
      class=&quot;border px-2 py-1 rounded w-full&quot;
    /&amp;gt;
    &amp;lt;input
      v-model=&quot;localUserInfo.email&quot;
      placeholder=&quot;이메일&quot;
      class=&quot;border px-2 py-1 rounded w-full&quot;
    /&amp;gt;
    &amp;lt;label class=&quot;flex items-center gap-2&quot;&amp;gt;
      &amp;lt;input
        type=&quot;checkbox&quot;
        :checked=&quot;props.agreed&quot;
        @change=&quot;e =&amp;gt; emit('update:agreed', e.target.checked)&quot;
      /&amp;gt;
      약관에 동의합니다
    &amp;lt;/label&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1748422874200&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref } from 'vue'
import UserForm from './components/UserForm.vue'

const userInfo = ref({ name: '홍길동', email: 'hong@example.com' })
const agreed = ref(false)
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;UserForm v-model:userInfo=&quot;userInfo&quot; v-model:agreed=&quot;agreed&quot; /&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안 써도 되긴 하는데... 안 쓸 이유가 없겠죠?&lt;/p&gt;</description>
      <category>vue</category>
      <category>definemodel</category>
      <category>VUE</category>
      <author>weaklion</author>
      <guid isPermaLink="true">https://weaklion1.tistory.com/54</guid>
      <comments>https://weaklion1.tistory.com/54#entry54comment</comments>
      <pubDate>Wed, 28 May 2025 18:01:43 +0900</pubDate>
    </item>
    <item>
      <title>기능 분할 설계(Feature-Sliced Design, FSD)란</title>
      <link>https://weaklion1.tistory.com/53</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Feature-Sliced Design(FSD)는 프론트엔드 애플리케이션의 구조를 잡는 아키텍처 방법론입니다. FSD는 이름 그대로 기능 단위로 분할하는 것에 초점이 맞춰져 있으며 현재 많은 주목을 받는 아키텍처 중에 하나 이기도 합니다.&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;FSD는 대부분의 프로젝트에 적합하다고 공식에선 이야기합니다. 프론트엔드를 개발하고 있는 모든 환경에 적합하며 라이브러리가 아닌 애플리케이션을 만들고 있다면 FSD는 좋은 선택이 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;명시적인 비즈니스 로직&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 스코프 덕분에 찾고자 하는 로직을 쉽게 발견할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;유연성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아키텍쳐 구성 요소를 새로운 요구사항에 맞춰 유연하게 교체하고 추가할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기술 부채 및 리팩토링&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 모듈을 부작용 없이 독립적으로 수정, 재작성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;명시적인 코드 재사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DRY원칙과 로컬 커스터마이징 사이에 균형을 유지합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;FSD의 구조&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1030&quot; data-origin-height=&quot;573&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQKu5u/btsNvzmWfGM/Bpy123ZHUAOlvZ0xD2o3tK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQKu5u/btsNvzmWfGM/Bpy123ZHUAOlvZ0xD2o3tK/img.jpg&quot; data-alt=&quot;https://feature-sliced.github.io/documentation&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQKu5u/btsNvzmWfGM/Bpy123ZHUAOlvZ0xD2o3tK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQKu5u%2FbtsNvzmWfGM%2FBpy123ZHUAOlvZ0xD2o3tK%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;1030&quot; height=&quot;573&quot; data-origin-width=&quot;1030&quot; data-origin-height=&quot;573&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://feature-sliced.github.io/documentation&lt;/figcaption&gt;
&lt;/figure&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;위 이미지를 토대로 3개의 구조가 어떤 건지에 대해 간략하게 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;레이어(Layers)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이어는 FSD의 가장 상위 수준 구조입니다.&amp;nbsp; 총 7개로 제한되어 있으며 모든 레이어를 사용할 필요는 없지만 네이밍 규칙은 지키는 걸 권장하고 있습니다. 각 네이밍에 해당하는 역할은 아래와 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #1c1e21; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;App&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 앱을 실행하는 모든 것 - 라우팅, 진입점, 전역 스타일, 프로바이더.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Processes&lt;/b&gt;(더 이상 사용되지 않음) - 페이지 간 복잡한 시나리오.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Pages&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 전체 페이지 또는 중첩 라우팅에서 페이지의 주요 부분.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Widgets&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 독립적으로 작동하는 대규모 기능 또는 UI 컴포넌트, 보통 하나의 완전한 기능.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Features&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 제품 전반에 걸쳐 재사용되는 기능 구현체로, 사용자에게 실질적인 비즈니스 가치를 제공하는 동작 (ex: 리뷰 작성, 제품 평가 등).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Entities&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 프로젝트가 다루는 비즈니스 엔티티.(ex : 사용자, 리뷰, 댓글 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Shared&lt;/b&gt; - 재사용 가능한 기능, 특히 프로젝트/비즈니스의 특성과 분리되어 있을 때. (ex : ui kit, axios config, config 등)&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;App과 Shared는 다른 레이어들과 달리 슬라이스를 가지지 않고, 세그먼트로만 구성됩니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zYivt/btsNv5SS5St/tgoDS2N3GR1hUdqlSY5rF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zYivt/btsNv5SS5St/tgoDS2N3GR1hUdqlSY5rF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zYivt/btsNv5SS5St/tgoDS2N3GR1hUdqlSY5rF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzYivt%2FbtsNv5SS5St%2FtgoDS2N3GR1hUdqlSY5rF0%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;700&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 계층구조에서 하위 레이어는 상위 레이어의 컴포넌트를 사용할 수 없습니다. 계층 구조에서 레이어의 위치가 낮을 수록 더 많은 곳에서 사용될 확률이 높으며 이러한 선형적인 흐름을 유지해야 관리하기가 더 편합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctHP75/btsNs7dvMUP/bdgKuwagxMHeM9wnGLTik0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctHP75/btsNs7dvMUP/bdgKuwagxMHeM9wnGLTik0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctHP75/btsNs7dvMUP/bdgKuwagxMHeM9wnGLTik0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctHP75%2FbtsNs7dvMUP%2FbdgKuwagxMHeM9wnGLTik0%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;374&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;374&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;슬라이스(Slices)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬라이스는 비즈니스 도메인 별로 코드를&amp;nbsp; 분할합니다.&amp;nbsp; 논리적으로 관련된 모듈들을 그룹회하고 이를 유지함으로써 독립성을 높여줍니다. 슬라이스는 같은 레이어 안에서 다른 슬라이스를 참조할 수 없으며 이 규칙은 높은 응집도와 낮은 결합도 유지에 도움이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬라이스 이름은 비즈니스 영역에 따라 결정되므로 표준화되어있지 않습니다.용도에 따라 개발자들끼리 네이밍을 마음대로 지어도 상관없습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1745380850853&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;src/
	features/
			auth/ //인증 슬라이스
			order-list // 상품 리스트 슬라이스
			buy-product // 상품 구매 슬라이스&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;세그먼트(Segments)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세그먼트는 슬라이스 내의 코드를 나누는데 도움이 됩니다. 세그먼트의 구성 역시 자유롭게 사용 가능하지만, 일반적으로 권장하는 몇가지 세그먼트들이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #1c1e21; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ui&lt;span&gt;&amp;nbsp;&lt;/span&gt;- UI와 관련된 모든 것: UI 컴포넌트, 날짜 포맷터, 스타일 등.&lt;/li&gt;
&lt;li&gt;api&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 백엔드 상호작용: request 함수, 데이터 타입, mapper 등.&lt;/li&gt;
&lt;li&gt;model&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 데이터 모델: 스키마, 인터페이스, 스토어, 비즈니스 로직.&lt;/li&gt;
&lt;li&gt;lib&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 슬라이스 안에 있는 다른 모듈이 필요로 하는 라이브러리 코드.&lt;/li&gt;
&lt;li&gt;config&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 설정 파일과 기능&amp;nbsp;&lt;/li&gt;
&lt;li&gt;const - 필요한 상수&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;공개 API&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 슬라이스와 세그먼트를 외부에 노출할 수 있는 인터페이스 입니다. index.js 또는 index.ts로 정의하며, 이 파일을 통해 슬라이스, 세그먼트에서 필요한 기능만 외부로 추출하여 독립성을 높일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공개 API에 대한 규칙은 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 슬라이스와 세그먼트는 공개 api 인덱스 파일에 정의된 슬라이스의 기능과 컴포넌트만 사용합니다.&lt;/li&gt;
&lt;li&gt;정의되지 않는 것들은 격리된 것으로 간주해 슬라이스, 혹은 세그먼트 내부에서만 접근할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1745381201438&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export { Button } from './Button';
export { Modal} from './Modal';
export { Input } from './Input';

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발지식</category>
      <author>weaklion</author>
      <guid isPermaLink="true">https://weaklion1.tistory.com/53</guid>
      <comments>https://weaklion1.tistory.com/53#entry53comment</comments>
      <pubDate>Wed, 23 Apr 2025 13:09:34 +0900</pubDate>
    </item>
    <item>
      <title>next.js, gemini AI를 이용한 한국인 캐릭터 생성기 - 5</title>
      <link>https://weaklion1.tistory.com/52</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 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;&amp;nbsp;캐릭터는 gemini에게 프롬프트를 통해서 물어본 뒤, 그 답을 가져와서 뿌리게 되는데 그러기 위해선 gemini의 api 키가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api key는 google ai studio에서 가져올 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;https://ai.google.dev&quot; href=&quot;https://ai.google.dev&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://ai.google.dev&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1743054530585&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;Gemini Developer API | Gemma open models &amp;nbsp;|&amp;nbsp; Google AI for Developers&quot; data-og-description=&quot;Build with Gemini 2.0 Flash, 2.0 Pro, and Gemma using the Gemini API and Google AI Studio.&quot; data-og-host=&quot;ai.google.dev&quot; data-og-source-url=&quot;https://ai.google.dev&quot; data-og-url=&quot;https://ai.google.dev/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/3wEjC/hyYugxj5Ps/IyQ9C8MFhR1YxnVojY0I81/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cIZ5VW/hyYuhXiwP0/LGJcmDEXTydUwKQ3EHCwUK/img.png?width=1400&amp;amp;height=1202&amp;amp;face=0_0_1400_1202,https://scrap.kakaocdn.net/dn/M9UWO/hyYxMH7jk6/egIKy5d2KtgnQ7iRkhGyM0/img.png?width=1300&amp;amp;height=1066&amp;amp;face=0_0_1300_1066&quot;&gt;&lt;a href=&quot;https://ai.google.dev&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ai.google.dev&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/3wEjC/hyYugxj5Ps/IyQ9C8MFhR1YxnVojY0I81/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cIZ5VW/hyYuhXiwP0/LGJcmDEXTydUwKQ3EHCwUK/img.png?width=1400&amp;amp;height=1202&amp;amp;face=0_0_1400_1202,https://scrap.kakaocdn.net/dn/M9UWO/hyYxMH7jk6/egIKy5d2KtgnQ7iRkhGyM0/img.png?width=1300&amp;amp;height=1066&amp;amp;face=0_0_1300_1066');&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;Gemini Developer API | Gemma open models &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;Build with Gemini 2.0 Flash, 2.0 Pro, and Gemma using the Gemini API and Google AI Studio.&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;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;479&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dv5YoS/btsMYtubBQG/WXY9zKK4hNyzKatJyK0rU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dv5YoS/btsMYtubBQG/WXY9zKK4hNyzKatJyK0rU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dv5YoS/btsMYtubBQG/WXY9zKK4hNyzKatJyK0rU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdv5YoS%2FbtsMYtubBQG%2FWXY9zKK4hNyzKatJyK0rU0%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;1101&quot; height=&quot;479&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;479&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 버튼을 누르고 하라는대로만 하면 api key를 쉽게 얻을 수 있습니다.&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;2794&quot; data-origin-height=&quot;1614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MkAHM/btsMZkQOTaI/0efW3ynIak0uCW8CaPCie1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MkAHM/btsMZkQOTaI/0efW3ynIak0uCW8CaPCie1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MkAHM/btsMZkQOTaI/0efW3ynIak0uCW8CaPCie1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMkAHM%2FbtsMZkQOTaI%2F0efW3ynIak0uCW8CaPCie1%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;2794&quot; height=&quot;1614&quot; data-origin-width=&quot;2794&quot; data-origin-height=&quot;1614&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 api key를 집어넣고 연동합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 .env에 api key를 집어넣었고 action에서 가져오는 식으로 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743054964056&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;p data-ke-size=&quot;size16&quot;&gt;google 에서 지원하는 generative-ai 라이브러리를 설치 후 ai를 연동합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743054892673&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;use server&quot;;

import { GoogleGenerativeAI } from &quot;@google/generative-ai&quot;;

const generativeAI = new GoogleGenerativeAI(process.env.API_KEY || &quot;&quot;);

const model = generativeAI.getGenerativeModel({
  model: &quot;gemini-1.5-flash&quot;,
  generationConfig: { responseMimeType: &quot;application/json&quot; },
});
{
}
export async function getData(count: number, gender: string) {
  try {
    const result = await model.generateContent(
      `웹소설에 등장하는 가상의 한국인 캐릭터들을 생성해주세요. 성별이 **반드시** ${gender}인 ${count}명의 캐릭터를 생성하여 characters 배열에 넣고 JSON 형식으로 출력해주세요. 각 캐릭터는 다음 속성을 가져야 합니다.

- name: 무작위로 선택한 한국인 이름 (문자열)
- age: 나이 (문자열, 예: &quot;25세&quot;) (다양한 나이대를 섞어서 무작위로 선택)
- gender: 성별 (문자열, **반드시** &quot;${gender}&quot;로 지정)
- appearance: 외형적 특징을 **상세하게** 설명합니다. 눈 색깔, 머리 스타일, 키, 체형, 옷 스타일 등을 포함하여 구체적으로 묘사 (문자열). (각 캐릭터마다 다른 특징을 갖도록 무작위로 선택)
- background: 캐릭터의 간단한 배경 설명을 구체적으로 작성합니다. 성장 환경, 가족 관계, 특별한 경험 등을 포함하여 각 캐릭터마다 고유한 스토리를 부여 (문자열).
- occupation: 직업 (문자열). (다양한 직업을 무작위로 선택)

출력 형식:
{
  &quot;characters&quot;: [
    {
      &quot;name&quot;: string,
      &quot;age&quot;: string,
      &quot;gender&quot;: string,
      &quot;appearance&quot;: string,
      &quot;background&quot;: string,
      &quot;occupation&quot;: string
    },
    ...
  ]
}

각 필드에 대해 구체적이고 다양한 설명을 제공해주세요. 캐릭터들은 서로 다른 특징과 배경을 가져야 하며, 성별은 반드시 프롬프트에 지정된 ${gender}로 생성되어야 합니다. 각 캐릭터는 다른 나이, 직업을 갖도록 해주세요.`
    );
    const response = await result.response;
    const text = await response.text();
    if (text) {
      console.log(text, &quot;결과 텍스트 &quot;);
      return text;
    }
  } catch (e) {
    console.error(e, &quot;error&quot;);
  }
}

export async function getList(name: string) {
  try {
    const result = await model.generateContent(
      `앞서 생성된 캐릭터 중 ${name}의 상세한 배경을 제공해주세요. 다음 요소들을 포함하여 자세히 설명해주세요:

1. 가족 관계
2. 성장 환경
3. 교육 배경
4. 주요 삶의 경험이나 전환점
5. 현재 상황에 이르게 된 과정
6. 성격 형성에 영향을 준 사건들
7. 주요 인간관계
8. 꿈과 목표
9. 강점과 약점
10. 특별한 기술이나 재능

형식은 다음과 같이 JSON으로 출력해주세요:

{
  &quot;characterBackground&quot;: {
    &quot;name&quot;: &quot;${name}&quot;,
    &quot;detailedBackground&quot;: {
      &quot;familyRelations&quot;: string,
      &quot;growthEnvironment&quot;: string,
      &quot;educationalBackground&quot;: string,
      &quot;lifeExperiences&quot;: string,
      &quot;pathToCurrentSituation&quot;: string,
      &quot;personalityShapingEvents&quot;: string,
      &quot;keyRelationships&quot;: string,
      &quot;dreamsAndGoals&quot;: string,
      &quot;strengthsAndWeaknesses&quot;: string,
      &quot;specialSkillsOrTalents&quot;: string
    }
  }
}

각 항목에 대해 2-3문장 정도의 구체적인 설명을 제공해주세요. 캐릭터의 기존 정보와 일관성을 유지하면서, 풍부하고 현실감 있는 배경을 만들어주세요.`
    );
    const response = await result.response;
    const text = await response.text();
    if (text) {
      console.log(text, &quot;결과&quot;);
      return text;
    }
  } catch (e) {
    console.error(e, &quot;error&quot;);
  }
}

//app/detail/actions.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;이후 서버 액션에서 설치한 라이브러리에 가져온 api key를 넣고 모델을 만듭니다. 저는 gemeni-1.5를 이용했는데 모델은 다른 것을 이용해도 상관없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 모델에 따라 구글에서 제공하는 가격정책이 다 다르니 이부분은 아래 참조해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;https://ai.google.dev/gemini-api/docs/pricing?hl=ko&quot; href=&quot;https://ai.google.dev/gemini-api/docs/pricing?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://ai.google.dev/gemini-api/docs/pricing?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1743055110417&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;Gemini Developer API 가격 책정 &amp;nbsp;|&amp;nbsp; Gemini API &amp;nbsp;|&amp;nbsp; Google AI for Developers&quot; data-og-description=&quot;Gemini Developer API 가격 책정&quot; data-og-host=&quot;ai.google.dev&quot; data-og-source-url=&quot;https://ai.google.dev/gemini-api/docs/pricing?hl=ko&quot; data-og-url=&quot;https://ai.google.dev/gemini-api/docs/pricing?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/wvLAU/hyYumK5i0I/sO3WeId2wq4v3jQ4KCzFGK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://ai.google.dev/gemini-api/docs/pricing?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ai.google.dev/gemini-api/docs/pricing?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/wvLAU/hyYumK5i0I/sO3WeId2wq4v3jQ4KCzFGK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Gemini Developer API 가격 책정 &amp;nbsp;|&amp;nbsp; 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;Gemini Developer 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;
&lt;p data-ke-size=&quot;size16&quot;&gt;api key를 통해서 통신할 때 프롬프트는 정확해야 합니다. 원하는 출력 형식과 인풋을 명확하게 알려줘야 원하는 값을 주기 때문에 되도록 프롬프트를 정확하게 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 사람에 따라 많은 팁이 있겠지만 개인적으론 물어볼 모델에게 대략적인 베이스를 물어본 뒤 받은 프롬프트를 다시 모델에게 물어 정확도를 얻는게 가장 결과가 만족스러웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 서버 액션에 있는 api를 메인에 있는 버튼에 연결하면 끝입니다. 코드엔 react에서 제공하는 suspense를 붙여 ux를 좋게 만들려고 했습니다.&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;근데 ai에 api로 요청하면 response를 200으로 만든 뒤, 데이터가 나올때 까지 기다리더라구요. 그래서 그냥 loading 상태값 만들어서 loading이 false면 데이터 안보여주도록 만들어 줬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 상세페이지와 리스트엔 요청한 모델에 대한 값을 그대로 보여주면 끝.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743055647248&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export interface Character {
  name: string;
  age: number;
  gender: string;
  appearance: string;
  background: string;
  occupation: string;
}

export interface CharacterBackground {
  name: string;
  detailedBackground: {
    familyRelations: string;
    growthEnvironment: string;
    educationalBackground: string;
    lifeExperiences: string;
    pathToCurrentSituation: string;
    personalityShapingEvents: string;
    keyRelationships: string;
    dreamsAndGoals: string;
    strengthsAndWeaknesses: string;
    specialSkillsOrTalents: string;
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743055659088&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 { CharacterBackground } from &quot;@/app/model&quot;;

export default function Client({
  background,
}: {
  background: CharacterBackground;
}) {
  const detail = background;

  return (
    &amp;lt;main className=&quot;flex flex-col items-center justify-center size-full&quot;&amp;gt;
      &amp;lt;h3 className=&quot;mt-8 scroll-m-20 text-2xl font-semibold tracking-tight&quot;&amp;gt;
        이름 : {detail.name}
      &amp;lt;/h3&amp;gt;
      &amp;lt;h3 className=&quot;mt-8 scroll-m-20 text-2xl font-semibold tracking-tight&quot;&amp;gt;
        가족 관계 : {detail.detailedBackground.familyRelations}
      &amp;lt;/h3&amp;gt;
      &amp;lt;h3 className=&quot;mt-8 scroll-m-20 text-2xl font-semibold tracking-tight&quot;&amp;gt;
        성장 환경 : {detail.detailedBackground.growthEnvironment}
      &amp;lt;/h3&amp;gt;

      &amp;lt;h3 className=&quot;mt-8 scroll-m-20 text-2xl font-semibold tracking-tight&quot;&amp;gt;
        교육 배경 : {detail.detailedBackground.educationalBackground}
      &amp;lt;/h3&amp;gt;
      &amp;lt;h3 className=&quot;mt-8 scroll-m-20 text-2xl font-semibold tracking-tight&quot;&amp;gt;
        주요 삶의 경험이나 전환점 : {detail.detailedBackground.lifeExperiences}
      &amp;lt;/h3&amp;gt;
      &amp;lt;h3 className=&quot;mt-8 scroll-m-20 text-2xl font-semibold tracking-tight&quot;&amp;gt;
        현재 상황에 이르게 된 과정 :
        {detail.detailedBackground.pathToCurrentSituation}
      &amp;lt;/h3&amp;gt;
      &amp;lt;h3 className=&quot;mt-8 scroll-m-20 text-2xl font-semibold tracking-tight&quot;&amp;gt;
        성격 형성에 영향을 준 사건들 :{&quot; &quot;}
        {detail.detailedBackground.personalityShapingEvents}
      &amp;lt;/h3&amp;gt;
      &amp;lt;h3 className=&quot;mt-8 scroll-m-20 text-2xl font-semibold tracking-tight&quot;&amp;gt;
        주요 인간관계: {detail.detailedBackground.keyRelationships}
      &amp;lt;/h3&amp;gt;

      &amp;lt;h3 className=&quot;mt-8 scroll-m-20 text-2xl font-semibold tracking-tight&quot;&amp;gt;
        꿈과 목표: {detail.detailedBackground.dreamsAndGoals}
      &amp;lt;/h3&amp;gt;
      &amp;lt;h3 className=&quot;mt-8 scroll-m-20 text-2xl font-semibold tracking-tight&quot;&amp;gt;
        강점과 약점: {detail.detailedBackground.strengthsAndWeaknesses}
      &amp;lt;/h3&amp;gt;

      &amp;lt;h3 className=&quot;mt-8 scroll-m-20 text-2xl font-semibold tracking-tight&quot;&amp;gt;
        특별한 기술이나 재능 :{detail.detailedBackground.specialSkillsOrTalents}
      &amp;lt;/h3&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딱 보면 알죠?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네 이게 끝입니다. 사실 이번 프로젝트는 ai를 어떻게 사용할지에 대해 초점을 둔지라 ui에 대해선 깊게 생각하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 ai에 대해 공부할 수 있게 됐네요. next는 안 쓸 거 같습니다.&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;사실 이 프로젝트를 만들고 이미 5~6개월 정도가 흘렀습니다. 만드는데는 1주일도 안걸린 게 설명하려다 보니 더 오랜 시간이 걸렸네요.&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;여담인데, 이 이후로 next를 실제로 안 썼습니다. 대신 react router v7이 나왔다고 해서 거기 문서나 조금 읽어본 게 다네요. 만약 다음에 react에 ssr이 필요하다면 react router 7을 써보지 않을까 싶습니다. 실제로 next에 불편함을 느낀 사람이 많이 쓰고 있다고도 하고요.&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;어쨌든 이제 코드에 대한 설명은 끝났습니다. 최종 코드는 아래 repo를 참조해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;git@github.com:weaklion/k-character-generator.git&quot; href=&quot;http://git@github.com:weaklion/k-character-generator.git&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;http://git@github.com:weaklion/k-character-generator.git&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;</description>
      <category>next</category>
      <author>weaklion</author>
      <guid isPermaLink="true">https://weaklion1.tistory.com/52</guid>
      <comments>https://weaklion1.tistory.com/52#entry52comment</comments>
      <pubDate>Thu, 27 Mar 2025 15:11:45 +0900</pubDate>
    </item>
    <item>
      <title>next.js, gemini AI를 이용한 한국인 캐릭터 생성기 - 4</title>
      <link>https://weaklion1.tistory.com/51</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 app 폴더 내부에 대해 알아보도록 합시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여담인데 최근 rust 언어를 공부하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 점점 하다보니 로우 랭귀지를 배울 필요성을 느껴서 공부를 시작했는데 배워보니까 꽤나 어렵고 흥미로운 언어더군요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 난다면 rust에 대해서도 포스트를 남겨보도록 하겠습니다.&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;main으로 쓸 main 페이지 폴더, 그리고 결과가 나올 list. 더 구체적인 결과를 나타낼 detail 페이지. 총 3가지입니다.&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는 client와 server를 나눌 것을 권장하고 있는데, 저는 client에서 사용될 코드를 client.tsx로 정의했고, route의 시작지점이 될&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지이자 서버 컴포넌트는 page.tsx로 정의했습니다.&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;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UH834/btsMohBm8rr/gFy2F9pQLF1txD7H5skXik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UH834/btsMohBm8rr/gFy2F9pQLF1txD7H5skXik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UH834/btsMohBm8rr/gFy2F9pQLF1txD7H5skXik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUH834%2FbtsMohBm8rr%2FgFy2F9pQLF1txD7H5skXik%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;900&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;900&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;처음 보여질 페이지는 main입니다. 소괄호 안에 이름을 적고 폴더를 만든다면 그 route로 이동하지 않고 코드를 작성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 만약 main 폴더를 만들었다면&amp;nbsp; /main 의 라우트가 생기겠지만, 소괄호로 감싼다면 생기지 않습니다.&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;가장 상위에 만들었기 때문에 localhost:3000. 즉 첫 페이지엔 (main)폴더 안에 page.tsx가 실행이 되겠죠.&lt;/p&gt;
&lt;pre id=&quot;code_1739955884017&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { Suspense } from &quot;react&quot;;
import Client from &quot;./client&quot;;
import Loader from &quot;@/components/loader&quot;;

export default function Main() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Suspense fallback={&amp;lt;Loader /&amp;gt;}&amp;gt;
        &amp;lt;Client /&amp;gt;
      &amp;lt;/Suspense&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

//(main)/page.tsx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;suspense&amp;gt;는 컴포넌트의 자식 요소가 렌더링 되기 전 로딩을 선언함으로써 사용자의 경험을 개선할 수 있게 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 해석하자면 client 페이지가 렌더링 되기 전 loader 컴포넌트를 띄우고 렌더링을 마치면 client가 띄어지는 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739955940229&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 {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from &quot;@/components/ui/select&quot;;
import { Button } from &quot;@/components/ui/button&quot;;
import { useState } from &quot;react&quot;;
import { usePathname, useRouter, useSearchParams } from &quot;next/navigation&quot;;
import { Input } from &quot;@/components/ui/input&quot;;
import Loader from &quot;@/components/loader&quot;;

export default function Home() {
  const [gender, setGender] = useState&amp;lt;&quot;man&quot; | &quot;women&quot;&amp;gt;(&quot;man&quot;);
  const [count, setCount] = useState(1);
  const [loading, setLoading] = useState(false);
  const router = useRouter();
  const searchParams = useSearchParams();
  const pathname = usePathname();

  const setQueryParams = (params: {
    gender: &quot;man&quot; | &quot;women&quot;;
    count: string;
  }) =&amp;gt; {
    const urlSearchParams = new URLSearchParams(searchParams);
    Object.entries(params).forEach(([name, value]) =&amp;gt; {
      urlSearchParams.set(name, value);
    });

    return urlSearchParams.toString();
  };

  const handleGenderChange = (value: &quot;man&quot; | &quot;women&quot;) =&amp;gt; {
    setGender(value);
  };

  const onCreateCharacter = () =&amp;gt; {
    const params = {
      gender: gender,
      count: count.toString(),
    };
    const qs = setQueryParams(params);
    setLoading(true);
    router.push(`/list?${qs}`);
  };

  return (
    &amp;lt;main className=&quot;flex min-h-screen flex-col items-center justify-center p-12&quot;&amp;gt;
      &amp;lt;h1 className=&quot;scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl&quot;&amp;gt;
        한국인 캐릭터 생성기
      &amp;lt;/h1&amp;gt;

      &amp;lt;h3 className=&quot;scroll-m-20 my-4 text-2xl font-semibold tracking-tight&quot;&amp;gt;
        성별
      &amp;lt;/h3&amp;gt;
      &amp;lt;Select
        onValueChange={(value: &quot;man&quot; | &quot;women&quot;) =&amp;gt; handleGenderChange(value)}
      &amp;gt;
        &amp;lt;SelectTrigger className=&quot;w-[180px]&quot;&amp;gt;
          &amp;lt;SelectValue placeholder=&quot;성별을 선택해주세요.&quot; /&amp;gt;
        &amp;lt;/SelectTrigger&amp;gt;
        &amp;lt;SelectContent&amp;gt;
          &amp;lt;SelectGroup&amp;gt;
            &amp;lt;SelectItem value=&quot;man&quot;&amp;gt;남자&amp;lt;/SelectItem&amp;gt;
            &amp;lt;SelectItem value=&quot;women&quot;&amp;gt;여자&amp;lt;/SelectItem&amp;gt;
          &amp;lt;/SelectGroup&amp;gt;
        &amp;lt;/SelectContent&amp;gt;
      &amp;lt;/Select&amp;gt;

      &amp;lt;h3 className=&quot;scroll-m-20 my-4 text-2xl font-semibold tracking-tight&quot;&amp;gt;
        개수
      &amp;lt;/h3&amp;gt;

      &amp;lt;Input
        type=&quot;number&quot;
        className=&quot;max-w-48&quot;
        value={count}
        onChange={(e) =&amp;gt; setCount(e.target.valueAsNumber)}
        placeholder=&quot;개수를 입력해주세요.&quot;
      /&amp;gt;

      &amp;lt;Button className=&quot;my-4&quot; onClick={onCreateCharacter}&amp;gt;
        생성
      &amp;lt;/Button&amp;gt;
      {loading ? (
        &amp;lt;div className=&quot;flex flex-col py-4 gap-2 justify-center&quot;&amp;gt;
          &amp;lt;Loader /&amp;gt;
          잠시만 기다려 주세요..
        &amp;lt;/div&amp;gt;
      ) : (
        &quot;&quot;
      )}
    &amp;lt;/main&amp;gt;
  );
}

//(main)/client.tsx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client.tsx 코드입니다. 지금은 코드를 다 보여줬지만, 이 이후론 제가 코딩하면서 인상깊었던 코드에 대해서만 적을까 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;return 되는 템플릿엔 특별한 부분이 없으니 넘어가고, 다른 것 중 특이한 건 라우팅을 하는 방식이었습니다.&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;저는 이전번에 vue를 쓰고 vue-router를 쓰면서 routing에 썩 불편함을 느끼진 못했습니다. 하지만 이번에 next의 라우팅을 쓰면서 처음으로 불편함을 느꼈습니다.&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;useRouter를 이용해서 하는 이전 방식과 다르게 이번엔 url에 원하는 queryString을 직접 집어 넣어야만 합니다. URLSearchParams 인터페이스를 이용해서 하나하나씩 set 해줘야만 하죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 저도 찾다가 답이 안나와서 한 거긴 합니다. 더 좋은 방식이 있으면 답변해주시면 감사하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 아레 레퍼런스를 참조했습니다.&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 title=&quot;mdn web docs&quot; href=&quot;https://developer.mozilla.org/ko/docs/Web/API/URLSearchParams&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.mozilla.org/ko/docs/Web/API/URLSearchParams&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739956267881&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;URLSearchParams - Web API | MDN&quot; data-og-description=&quot;URLSearchParams 인터페이스는 URL의 쿼리 문자열을 대상으로 작업할 수 있는 유틸리티 메서드를 정의합니다.&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/URLSearchParams&quot; data-og-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/URLSearchParams&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b7o9Q9/hyYjM80eyb/dOpX3lfZLaKI6vtJwk6q41/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/URLSearchParams&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/URLSearchParams&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b7o9Q9/hyYjM80eyb/dOpX3lfZLaKI6vtJwk6q41/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&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;URLSearchParams - Web API | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;URLSearchParams 인터페이스는 URL의 쿼리 문자열을 대상으로 작업할 수 있는 유틸리티 메서드를 정의합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;medium&quot; href=&quot;https://medium.com/@lucarestagno/query-string-params-in-next-js-14-with-app-router-df44c8efa1f9&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://medium.com/@lucarestagno/query-string-params-in-next-js-14-with-app-router-df44c8efa1f9&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739956440516&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;Query string params in Next.js (14) with App Router&quot; data-og-description=&quot;The query string is a part of the URL (Uniform Resource Locator) that assigns values to specific parameters.&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/@lucarestagno/query-string-params-in-next-js-14-with-app-router-df44c8efa1f9&quot; data-og-url=&quot;https://medium.com/@lucarestagno/query-string-params-in-next-js-14-with-app-router-df44c8efa1f9&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bOPmMH/hyYjB7sSAG/T6zT3WJHHzYHAWP7C3isbk/img.jpg?width=1200&amp;amp;height=900&amp;amp;face=0_0_1200_900,https://scrap.kakaocdn.net/dn/H2VkG/hyYf02cGrs/SZWneMAXhM1iK9NzJaDbb0/img.jpg?width=1358&amp;amp;height=776&amp;amp;face=451_101_841_432,https://scrap.kakaocdn.net/dn/bStNv3/hyYjxcXNJZ/u3hIA2zsGPkmfRcivkjTk1/img.jpg?width=1358&amp;amp;height=713&amp;amp;face=0_0_1358_713&quot;&gt;&lt;a href=&quot;https://medium.com/@lucarestagno/query-string-params-in-next-js-14-with-app-router-df44c8efa1f9&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/@lucarestagno/query-string-params-in-next-js-14-with-app-router-df44c8efa1f9&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bOPmMH/hyYjB7sSAG/T6zT3WJHHzYHAWP7C3isbk/img.jpg?width=1200&amp;amp;height=900&amp;amp;face=0_0_1200_900,https://scrap.kakaocdn.net/dn/H2VkG/hyYf02cGrs/SZWneMAXhM1iK9NzJaDbb0/img.jpg?width=1358&amp;amp;height=776&amp;amp;face=451_101_841_432,https://scrap.kakaocdn.net/dn/bStNv3/hyYjxcXNJZ/u3hIA2zsGPkmfRcivkjTk1/img.jpg?width=1358&amp;amp;height=713&amp;amp;face=0_0_1358_713');&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;Query string params in Next.js (14) with App Router&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The query string is a part of the URL (Uniform Resource Locator) that assigns values to specific parameters.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.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;urlSearchParams로 queryString을 집어넣고 그 url을 router로 push 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;router.push({ name : &quot;...&quot; , params : {...} })로 딸깍하다가 이걸 다른 방식으로 하더니 영 답답했습니다.&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가 불편하더군요. 버전이 올라갈수록 방식 자체가 바뀌는지라 정보를 찾기가 힘들었습니다. 이 부분은 라우팅 뿐만 아니라 다른 것도 마찬가지인지라 next.js를 쓰는 다른 유저들도 만만찮게 불편을 토로하더군요.&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;어쨌든 router.push로 변환한 url을 넣으면 라우팅을 하는데, 이 때 input에 넣은 값들을 ai의 프롬프트에 집어 넣습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 과정에서 원래는 suspense로 처리하면 될 걸, 굳이 loading을 넣어서 처리했는데요.&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; api에 값을 requeest했을 때 곧바로 200으로 상태값이 떨어지는데도 response를 늦게 불러와서 제가 loading을 제어하기 위해 넣었습니다. 이건 프롬프트에 대한 결과를 다룰 때 더 자세히 봅시다.&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와 통신하기 위한 api를 만들어야 합니다. 저는 next.js의 서버 액션을 통해 api를 만들었는데요. 그 전에 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;그 과정은 다음 시간에... 알아 보도록 하겠습니다.&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>next</category>
      <category>Next.js</category>
      <author>weaklion</author>
      <guid isPermaLink="true">https://weaklion1.tistory.com/51</guid>
      <comments>https://weaklion1.tistory.com/51#entry51comment</comments>
      <pubDate>Wed, 19 Feb 2025 18:27:30 +0900</pubDate>
    </item>
    <item>
      <title>next.js, gemini AI를 이용한 한국인 캐릭터 생성기 - 3</title>
      <link>https://weaklion1.tistory.com/50</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저번까지는  Intro였고요. 오늘부터는 본격적으로 코딩을 해봅시다. 다만 코드의 구조가 간단하고, 얼마 없는지라 꽤나 싱겁게 끝날 수도 있어요.&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;이전에 말했다시피 저는 app router를 사용했습니다. app router를 쓰게 되면 기본적으로&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;984&quot; data-origin-height=&quot;1592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOoJuZ/btsLYdMNSmj/5y8zG4qI2gNKUVTqlbBIf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOoJuZ/btsLYdMNSmj/5y8zG4qI2gNKUVTqlbBIf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOoJuZ/btsLYdMNSmj/5y8zG4qI2gNKUVTqlbBIf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOoJuZ%2FbtsLYdMNSmj%2F5y8zG4qI2gNKUVTqlbBIf0%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;984&quot; height=&quot;1592&quot; data-origin-width=&quot;984&quot; data-origin-height=&quot;1592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 폴더가 만들어질겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 프로젝트가 만들어지면 기본으로 next.js 템플릿 스타일과 페이지가 있을텐데, 지웁시다. 그리고 메인화면을 원하는대로 수정합시다.&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도 애기 수준이라 코드 작성법은 오히려 다른 분의 블로그가 더 잘 되있을거에요.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;984&quot; data-origin-height=&quot;1908&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dSGTSW/btsLXq6QYt0/N1Txqedp09vnnGvOSGvSMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dSGTSW/btsLXq6QYt0/N1Txqedp09vnnGvOSGvSMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dSGTSW/btsLXq6QYt0/N1Txqedp09vnnGvOSGvSMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdSGTSW%2FbtsLXq6QYt0%2FN1Txqedp09vnnGvOSGvSMK%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;984&quot; height=&quot;1908&quot; data-origin-width=&quot;984&quot; data-origin-height=&quot;1908&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;app 폴더는 기본적으로 app router를 했을 때 생성되있는 폴더입니다. typescript로 구성했기 떄문에 필요한 타입 정의를 위해 model이라는 폴더를 만들었고, 그 안에서 타입을 정의 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;components라는 폴더엔 공용으로 사용할 ui들이 정의 되있는데요. 여기서 폴더 구조를 보면 슬슬 눈치채셨겠지만, components 폴더는 제가 스스로 만든 게 아니라 shadcn 라이브러리를 사용했기 때문에 생성된 폴더입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;https://ui.shadcn.com/docs&quot; href=&quot;https://ui.shadcn.com/docs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://ui.shadcn.com/docs&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;현재 리액트에서 가장 인기가 많은 css 라이브러리로, shadcn을 이용해 컴포넌트를 구축하기 위해선, components 폴더와 ui폴더가 있어야만 합니다. shadcn 내부에서 tailwindcss를 사용하기 때문에 그것도 포함이 되어야 하고요.&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에 만들어집니다. 그외에 제가 사용할 컴포넌트들은 그냥 ui 폴더 바깥에다가 만들었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737622535875&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 { ChevronLeft } from &quot;lucide-react&quot;;
import { Button } from &quot;./ui/button&quot;;
import { useRouter } from &quot;next/navigation&quot;;

export default function Header() {
  const router = useRouter();

  const onClickBackButton = () =&amp;gt; {
    router.back();
  };

  return (
    &amp;lt;header className=&quot;sticky top-0 z-30 flex h-14 items-center bg-background&quot;&amp;gt;
      &amp;lt;Button
        size=&quot;icon&quot;
        variant=&quot;ghost&quot;
        className=&quot;ml-4&quot;
        onClick={onClickBackButton}
      &amp;gt;
        &amp;lt;ChevronLeft /&amp;gt;
      &amp;lt;/Button&amp;gt;
    &amp;lt;/header&amp;gt;
  );
}
// components/header.tsx&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;간단하게 만들어 본 header입니다. 이전에도 말했다시피 app router는 server와 client 컴포넌트를 따로 구분하기 때문에, header 컴포넌트는 클라이언트에서 사용하기 위해 'use client'를 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1737622896356&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function Loader() {
  return (
    &amp;lt;div role=&quot;status&quot; className=&quot;flex justify-center&quot;&amp;gt;
      &amp;lt;svg
        aria-hidden=&quot;true&quot;
        className=&quot;w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600&quot;
        viewBox=&quot;0 0 100 101&quot;
        fill=&quot;none&quot;
        xmlns=&quot;http://www.w3.org/2000/svg&quot;
      &amp;gt;
        &amp;lt;path
          d=&quot;M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z&quot;
          fill=&quot;currentColor&quot;
        /&amp;gt;
        &amp;lt;path
          d=&quot;M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z&quot;
          fill=&quot;currentFill&quot;
        /&amp;gt;
      &amp;lt;/svg&amp;gt;
      &amp;lt;span className=&quot;sr-only&quot;&amp;gt;Loading...&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
// components/loader.tsx&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;로더는 로더..svg로 그냥 로더만 돌아가도록 구성했습니다. 딱히 뭐 없어요.&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;lib 폴더도 shadcn을 설치하면 추가되는 폴더입니다. 컴포넌트 내부에서 tailwind-merge를 쓰기 위함으로, 알 필요는 없지만 굳이 알자면 이미 컴포넌트 내부에서 구현한 인라인 클래스 스타일과 밖에서 선언한 클래스 스타일이 중복되는 게 있을 때 override하여 밖에서 선언한 클래스로 선언하게 만들어주는 라이브러리 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(ex : 컴포넌트 내부에서 container 스타일을 px-1 으로 선언하고, 외부에서 p-3로 선언했을 때 p-3로 만들어줌)&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;tailwindcss만을 위한 라이브러리로, shadcn 내부 컴포넌트에서 주로 사용됩니다.&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;model 폴더엔 컴포넌트나 api 통신시 사용되는 타입들을 선언했습니다. 지금은 작은 프로젝트라 index.ts 하나만으로 가능하겠지만, 규모가 커진다면 더 깔끔하게 바꿔야겠죠.&lt;/p&gt;
&lt;pre id=&quot;code_1737623241931&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export interface Character {
  name: string;
  age: number;
  gender: string;
  appearance: string;
  background: string;
  occupation: string;
}

export interface CharacterBackground {
  name: string;
  detailedBackground: {
    familyRelations: string;
    growthEnvironment: string;
    educationalBackground: string;
    lifeExperiences: string;
    pathToCurrentSituation: string;
    personalityShapingEvents: string;
    keyRelationships: string;
    dreamsAndGoals: string;
    strengthsAndWeaknesses: string;
    specialSkillsOrTalents: string;
  };
}

model/index.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;Charcter 인터페이스는 추후 api를 호출 할 때 필요한 타입으로, 추후&amp;nbsp; gemini 와 데이터를 주고받을 때 사용될 타입입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CharacterBackground는 캐릭터의 자세한 배경을 알고싶을 때 사용하는 타입으로, 추후 둘을 어떻게 사용할지 코드를 보시면 더 잘 이해가 될 것 같네요.&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;이제 마지막으로 app 폴더만 남았네요. 여기서부터가 진짜인데, 진짜는 다음 시간에 알아보겠습니다.&lt;/p&gt;</description>
      <category>next</category>
      <author>weaklion</author>
      <guid isPermaLink="true">https://weaklion1.tistory.com/50</guid>
      <comments>https://weaklion1.tistory.com/50#entry50comment</comments>
      <pubDate>Thu, 23 Jan 2025 18:10:43 +0900</pubDate>
    </item>
    <item>
      <title>next.js, gemini AI를 이용한 한국인 캐릭터 생성기 - 2</title>
      <link>https://weaklion1.tistory.com/49</link>
      <description>&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;그 전에, 우리는 지난 번 선택했던 app router가 어떤 건지에 대해서 알아야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;next.js엔 현재 두가지의 방식이 있습니다. page router와 app router 인데요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;page Router는 next가 13이 되기 전까지 사용했던 router로 우리가 흔히 알고 있는 알고 있는 라우터입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736412150465&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/pages
	/index.ts  -&amp;gt; www.tistory.com
    /list
    	/index.ts -&amp;gt; www.tistory.com/list
    /login
    	/index.ts -&amp;gt; www.tistory.com/login&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;page router는 보이기에 직관적으로 보여서 편하지만, 앱의 규모가 커지면 유지보수가 힘들어지는 단점을 가지고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 next는 이를 보완할 방법을 찾았고, 그 방법이 바로 app router 입니다.&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;app router도 page router와 방식은 비슷하지만 몇몇 차이점이 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app router는 react 18에서 부터 나온 개념인 server Components 기반으로 만들어진 router로 client와 server를 명확하게 구분하여 개발할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말로 하면 이해가 어려운데 예제를 좀 봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1736412740551&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/app
    /page.tsx -&amp;gt; www.tistory.com
    /list
    	/page.tsx -&amp;gt; www.tistory.com/list
    /login
    	/page.tsx -&amp;gt; www.tistory.com/login&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;쉽게 보면 pages -&amp;gt; app, index.ts -&amp;gt; page.tsx가 됐다고 보면 되겠죠. 하지만 app router는 이게 다가 아닙니다.&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;방금 말했듯이 app router는 server component를 기반으로 합니다. server component는 앱을 클라이언트에서 렌더링 하는 게 아니라 의도에 따라 일부 컴포넌트에만 렌더링을 할 수 있도록 정할 수 있는 컴포넌트입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pXWCd/btsLHLXO9Qt/nGLQ32DE63pjEmfrP4pVfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pXWCd/btsLHLXO9Qt/nGLQ32DE63pjEmfrP4pVfK/img.png&quot; data-alt=&quot;nextjs offical docs&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pXWCd/btsLHLXO9Qt/nGLQ32DE63pjEmfrP4pVfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpXWCd%2FbtsLHLXO9Qt%2FnGLQ32DE63pjEmfrP4pVfK%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;299&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;nextjs offical docs&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림에서 Navbar, Sidebar,Main은 interactive하지 않은 정적인 컴포넌트이기 때문에 server component에서 렌더링 할 수 있습니다. 반면 Search, Button은 interactive한 ui이기 때문에 client Component로 렌더링 합니다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 구체적으로 보자면 Server Component는 백엔드 리소스에 접근하거나 데이터 페칭을 통하여 자식에 있는 client Component와 통신하고, client Component에서는 액티비티한 애니메이션 및 상태값을 통한 이벤트 제어등을 담당하는거죠.&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;863&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdq1wj/btsLH8LZhtv/jpLFVky4FGrIudXaVkmyC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdq1wj/btsLH8LZhtv/jpLFVky4FGrIudXaVkmyC1/img.png&quot; data-alt=&quot;nextjs offical docs&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdq1wj/btsLH8LZhtv/jpLFVky4FGrIudXaVkmyC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcdq1wj%2FbtsLH8LZhtv%2FjpLFVky4FGrIudXaVkmyC1%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;863&quot; height=&quot;612&quot; data-origin-width=&quot;863&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;nextjs offical docs&lt;/figcaption&gt;
&lt;/figure&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;기본적으로 app router는 server component를 default입니다. client component를 사용하고 싶다면 use client를 선언해야합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736413867692&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Suspense } from &quot;react&quot;;
import Client from &quot;./client&quot;;
import Loader from &quot;@/components/loader&quot;;

export default function Main() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Suspense fallback={&amp;lt;Loader /&amp;gt;}&amp;gt;
        &amp;lt;Client /&amp;gt;
      &amp;lt;/Suspense&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
// main/page.tsx&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1736414017602&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 { useState } from &quot;react&quot;;

export default function Client() {
  const router = useRouter();
  const [loading, setLoading] = useState(false);

  return (
   &amp;lt;div&amp;gt; &amp;lt;/div&amp;gt;
  );
}

// main/page.tsx&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;client component는 'use client'를 사용 안하면 에러가 뜹니다. 저는 사실 이&amp;nbsp; 'user client'를 언제 사용해야 할지 잘 몰라서 갈팡질팡 했었는데, 제가 보고 있는 인강에서 언제 사용하는지 명확하게 알려주더군요.&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;여러분도 그러세요. server component는 요즘 react 진영의 중요한 기술이니만큼, 앞으로 더 디테일하게 설명할 예정입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vercel에서는 app router를 더 시각적으로 명료하게 볼 수 있도록, 페이지를 만들었습니다. 자세한 사항은 아래를 참조해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;https://app-router.vercel.app/&quot; href=&quot;https://app-router.vercel.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://app-router.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1736414413049&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;Next.js App Router Playground&quot; data-og-description=&quot;A playground to explore new Next.js App Router features such as nested layouts, instant loading states, streaming, and component level data fetching.&quot; data-og-host=&quot;app-router.vercel.app&quot; data-og-source-url=&quot;https://app-router.vercel.app/&quot; data-og-url=&quot;https://app-router.vercel.app/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/f9fLH/hyX0pfpS62/H6ctXRIhyS26JZKQpzfwuK/img.png?width=843&amp;amp;height=441&amp;amp;face=0_0_843_441,https://scrap.kakaocdn.net/dn/cheFHY/hyXWpOUJog/ne6AP0p9KR6M2KsMlNNrN1/img.png?width=843&amp;amp;height=441&amp;amp;face=0_0_843_441&quot;&gt;&lt;a href=&quot;https://app-router.vercel.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://app-router.vercel.app/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/f9fLH/hyX0pfpS62/H6ctXRIhyS26JZKQpzfwuK/img.png?width=843&amp;amp;height=441&amp;amp;face=0_0_843_441,https://scrap.kakaocdn.net/dn/cheFHY/hyXWpOUJog/ne6AP0p9KR6M2KsMlNNrN1/img.png?width=843&amp;amp;height=441&amp;amp;face=0_0_843_441');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Next.js App Router Playground&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A playground to explore new Next.js App Router features such as nested layouts, instant loading states, streaming, and component level data fetching.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;app-router.vercel.app&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이야기를 하다보니 꽤 많이 썼네요. 코드를 쓸려고 했는데, 저만 썼습니다. 다음 시간엔 정말로 같이 코드를 타이핑하는 시간을 갖도록 하겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 게시글은 좀 더 빨리 올릴게요.&lt;/p&gt;</description>
      <category>next</category>
      <author>weaklion</author>
      <guid isPermaLink="true">https://weaklion1.tistory.com/49</guid>
      <comments>https://weaklion1.tistory.com/49#entry49comment</comments>
      <pubDate>Thu, 9 Jan 2025 18:21:05 +0900</pubDate>
    </item>
  </channel>
</rss>