<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>영리의 테크블로그</title>
    <link>https://youngri.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 4 Jul 2026 12:23:58 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>영리0</managingEditor>
    <image>
      <title>영리의 테크블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/6883924/attach/5827b526aaac4235a154c4a5490366fd</url>
      <link>https://youngri.tistory.com</link>
    </image>
    <item>
      <title>해커톤 준비 / Human In The Loop(HITL) 란?</title>
      <link>https://youngri.tistory.com/75</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;이번 해커톤에서 백엔드/AI 파트를 맡아 쉬운말 번역기 라는 서비스의 흐름을 설계해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 langgraph 로 '문서 분기 - ocr - 쉬운말 변환 - tts' 를 어떻게 한 파이프라인으로 묶었는지 정리한 글이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;백엔드 파이프라인&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1377&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8mWLA/btsOWL0pleh/qkteH8Zl3RVqnXWjq59EM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8mWLA/btsOWL0pleh/qkteH8Zl3RVqnXWjq59EM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8mWLA/btsOWL0pleh/qkteH8Zl3RVqnXWjq59EM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8mWLA%2FbtsOWL0pleh%2FqkteH8Zl3RVqnXWjq59EM1%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;1377&quot; height=&quot;559&quot; data-origin-width=&quot;1377&quot; data-origin-height=&quot;559&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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;auth/ratelimit : fastapi 로 구현 / 파일 업로드, jwt 인증, websocket 브릿지&lt;/li&gt;
&lt;li&gt;worker : 분기, 재시도, 스트림 처리&lt;/li&gt;
&lt;li&gt;s3 : 이미지,음성 저장&lt;/li&gt;
&lt;li&gt;db : 작업 메타데이터, 사용자 로그 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그래프 구조 짜기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2018&quot; data-origin-height=&quot;813&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s8z42/btsOWEAxKwg/z8ZCkgnN1jmwOMUtL1KXf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s8z42/btsOWEAxKwg/z8ZCkgnN1jmwOMUtL1KXf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s8z42/btsOWEAxKwg/z8ZCkgnN1jmwOMUtL1KXf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs8z42%2FbtsOWEAxKwg%2Fz8ZCkgnN1jmwOMUtL1KXf0%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;2018&quot; height=&quot;813&quot; data-origin-width=&quot;2018&quot; data-origin-height=&quot;813&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세부 노드 설명&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;load doc : S3 등 이미지 서버에서 발급&lt;/li&gt;
&lt;li&gt;ocr snippit : OCR&lt;/li&gt;
&lt;li&gt;classfiy doc : OCR 내용 공적/사적 판별&lt;/li&gt;
&lt;li&gt;private : 로그인 리턴&lt;/li&gt;
&lt;li&gt;public : 다음 노드로 이동&lt;/li&gt;
&lt;li&gt;easy translate : LLM 스트림 호출 -&amp;gt; 토큰 스트림 yeild -&amp;gt; websocket 전송&lt;/li&gt;
&lt;li&gt;error : 서버 오류일시 재시도 클라이언트 오류일시 휴먼인더루프 실행&lt;/li&gt;
&lt;li&gt;tts : 비동기로? 미정임&lt;/li&gt;
&lt;li&gt;return : 최종상태 push&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Human In The Loop (휴먼인더루프)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mBNur/btsOUWoEkAX/fT9WxS2fmpXrteaOdNs7E0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mBNur/btsOUWoEkAX/fT9WxS2fmpXrteaOdNs7E0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mBNur/btsOUWoEkAX/fT9WxS2fmpXrteaOdNs7E0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmBNur%2FbtsOUWoEkAX%2FfT9WxS2fmpXrteaOdNs7E0%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;310&quot; height=&quot;267&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Human In The Loop (HITL) 란 자동화된 LLM 워크플로우 중간에 사람을 끼워 넣어 오류리스크를 최소화하는 장치이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangGraph 에서는 이를 위해 그래프 진행을 일시 중단하고 사람 입력을 받아 재시동할 수 있는 기능을 제공한다.&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;934&quot; data-origin-height=&quot;646&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9Xx7s/btsOX97jKIK/CUw2vgwEqxurbxN9radT41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9Xx7s/btsOX97jKIK/CUw2vgwEqxurbxN9radT41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9Xx7s/btsOX97jKIK/CUw2vgwEqxurbxN9radT41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9Xx7s%2FbtsOX97jKIK%2FCUw2vgwEqxurbxN9radT41%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;934&quot; height=&quot;646&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;646&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;LangGraph 가 제공하는 HITL 메커니즘&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/&quot;&gt;https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1751199273091&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;Overview&quot; data-og-description=&quot;hil human-in-the-loop overview Human-in-the-loop To review, edit, and approve tool calls in an agent or workflow, use LangGraph's human-in-the-loop features to enable human intervention at any point in a workflow. This is especially useful in large languag&quot; data-og-host=&quot;langchain-ai.github.io&quot; data-og-source-url=&quot;https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/&quot; data-og-url=&quot;https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cThXzH/hyZboONemp/xXQ56PRWGJ1UERiCKxjVd1/img.png?width=1209&amp;amp;height=308&amp;amp;face=0_0_1209_308&quot;&gt;&lt;a href=&quot;https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cThXzH/hyZboONemp/xXQ56PRWGJ1UERiCKxjVd1/img.png?width=1209&amp;amp;height=308&amp;amp;face=0_0_1209_308');&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;Overview&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;hil human-in-the-loop overview Human-in-the-loop To review, edit, and approve tool calls in an agent or workflow, use LangGraph's human-in-the-loop features to enable human intervention at any point in a workflow. This is especially useful in large languag&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;langchain-ai.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 171px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;기능&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;&lt;b&gt;그래프 중단&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;노드에서 interrupt() 호출 &amp;rarr; RUNNING &amp;rarr; INTERRUPTED 상태&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;return interrupt(data=my_payload)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;&lt;b&gt;재시작&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;외부 서비스(UI&amp;middot;FastAPI)에서 사용자 입력 수집 &amp;rarr; graph.resume(id, Command(...))&lt;/td&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;python graph.resume(job_id, Command(update=patch, goto=&quot;next_node&quot;))&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;&lt;b&gt;부모 그래프 점프&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;하위 서브그래프에서 상위 노드로 이동&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;Command(graph=Command.PARENT, goto=&quot;human_review&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;상태 패치&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;입력값&amp;middot;교정내용을 update={...} 로 주입&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;수정된 OCR&amp;middot;번역 텍스트 등&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;무한루프 방지 패턴 예시&lt;/p&gt;
&lt;pre id=&quot;code_1751199359457&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@node
async def classify_doc(state):
    outcome, conf = await llm_classify(state.snippet)
    # ❶ 확신도 낮음 &amp;rarr; interrupt
    if conf &amp;lt; 0.5:
        return interrupt({ &quot;snippet&quot;: state.snippet,
                           &quot;issue&quot;: &quot;low_confidence&quot; })
    # ❷ 판별 결과가 'unknown' &amp;rarr; human_review로 점프
    if outcome == &quot;unknown&quot;:
        return Command(goto=&quot;human_review&quot;, graph=Command.PARENT)
    # ❸ 정상 분기
    state.doc_type = outcome
    return state&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>dev/구름톤 유니브</category>
      <author>영리0</author>
      <guid isPermaLink="true">https://youngri.tistory.com/75</guid>
      <comments>https://youngri.tistory.com/75#entry75comment</comments>
      <pubDate>Sun, 29 Jun 2025 21:17:29 +0900</pubDate>
    </item>
    <item>
      <title>앱개발 백엔드 프레임워크 선택하기</title>
      <link>https://youngri.tistory.com/73</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;간단한 mvp 프로젝트의 서버를 선택하기 위해 백엔드 프레임워크들을 정리해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. FastAPI&lt;/h2&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;h3 data-ke-size=&quot;size23&quot;&gt;주요 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 I/O: async/await 문법 지원!&lt;/li&gt;
&lt;li&gt;자동 API 문서: 스웨거 문서 지원해 주어 개발 아주 편함!!&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생산성: 타입 기반 오류 검출, 자동 문서화로 CRUD API 빠르게 작성 가능&lt;/li&gt;
&lt;li&gt;성능: Uvicorn + Starlette 조합으로 동기 파이썬 프레임워크 대비 높은 처리량&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생태계 규모: Django&amp;middot;Flask만큼 방대한 커뮤니티&amp;middot;플러그인은 아직 부족 / 정식 개발로 넘어갈때 문제가 될 가능성 높음..&lt;/li&gt;
&lt;li&gt;배포 복잡도: Uvicorn, Gunicorn 등 서버 설정이 필요 / AI 모델 적용시 잘 터짐.. + 아주아주 높은 서버 비용&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. NestJS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트 기반 Node.js 프레임워크&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈화 설계: Controller, Service, Module 패턴 강제 (코드 가독성 Good)&lt;/li&gt;
&lt;li&gt;의존성 주입(DI): Angular 스타일 DI 컨테이너 내장 (Spring 이랑 비슷?)&lt;/li&gt;
&lt;li&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;큰 규모 프로젝트: 모듈 구분이 명확해 코드 관리&amp;middot;유지보수 용이&lt;/li&gt;
&lt;li&gt;TypeScript: 정적 타입 안정성 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오버 엔지니어링: 구조가 무겁고 설정 할것이 많음..&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 typescript 를 파고싶은 마음이 든다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Spring Boot&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 많이 써본 프레임워크&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자동 설정: 복잡한 xml 설정 없어도 됨!&lt;/li&gt;
&lt;li&gt;기능 모듈화: Spring Security, Spring Data, Spring Cloud 등 모듈화 (작년인가 Spring AI 도 나옴!!)&lt;/li&gt;
&lt;li&gt;대규모 서비스 지원: 스레드 풀&amp;middot;트랜잭션&amp;middot;배치 처리 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성숙도&amp;middot;안정성: 10년 이상 검증된 프레임워크 + 회사에서 제일 많이쓴다?&lt;/li&gt;
&lt;li&gt;멀티스레딩: CPU-바운드 작업에 유리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기 설정 무거움: JDK&amp;middot;Maven/Gradle&amp;middot;서버 설정 등 진입 장벽&lt;/li&gt;
&lt;li&gt;런타임 메모리: JVM 기반으로 메모리 사용량이 상대적으로 높음&lt;/li&gt;
&lt;li&gt;개발 속도: 간단한 CRUD에도 자바 언어 특성상 코드량이 늘어날 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 결정은 못했지만 개인적으로는 Nest &amp;gt; Fastapi &amp;gt; SpringBoot 순으로 마음에 감&lt;/p&gt;</description>
      <category>dev</category>
      <author>영리0</author>
      <guid isPermaLink="true">https://youngri.tistory.com/73</guid>
      <comments>https://youngri.tistory.com/73#entry73comment</comments>
      <pubDate>Sun, 25 May 2025 20:50:40 +0900</pubDate>
    </item>
    <item>
      <title>마리오카트 RNN 학습</title>
      <link>https://youngri.tistory.com/72</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학교 과제인&amp;nbsp;&lt;i&gt;Super Mario Kart&lt;/i&gt; 에이전트를 학습하기 위해 구현한 순환 신경망(RNN) 기반 모델을 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/YoonJae00/gym-SuperMarioKart-Snes&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/YoonJae00/gym-SuperMarioKart-Snes&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1747625000395&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - YoonJae00/gym-SuperMarioKart-Snes: Super Mario Kart Snes integration with OpenAI Retro Gym&quot; data-og-description=&quot;Super Mario Kart Snes integration with OpenAI Retro Gym - GitHub - YoonJae00/gym-SuperMarioKart-Snes: Super Mario Kart Snes integration with OpenAI Retro Gym&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/YoonJae00/gym-SuperMarioKart-Snes&quot; data-og-url=&quot;https://github.com/YoonJae00/gym-SuperMarioKart-Snes&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bPgrN8/hyYU07RPss/0Poy4vB5D0LLsKrrL3U5Ek/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/dnVYkX/hyYWSVaZ6Z/kFvubC2ptkf2O6wv4WXKp0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/YoonJae00/gym-SuperMarioKart-Snes&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/YoonJae00/gym-SuperMarioKart-Snes&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bPgrN8/hyYU07RPss/0Poy4vB5D0LLsKrrL3U5Ek/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/dnVYkX/hyYWSVaZ6Z/kFvubC2ptkf2O6wv4WXKp0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - YoonJae00/gym-SuperMarioKart-Snes: Super Mario Kart Snes integration with OpenAI Retro Gym&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Super Mario Kart Snes integration with OpenAI Retro Gym - GitHub - YoonJae00/gym-SuperMarioKart-Snes: Super Mario Kart Snes integration with OpenAI Retro Gym&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터 수집 및 전처리&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Gym-Retro 환경에 &lt;i&gt;SuperMarioKart-Snes&lt;/i&gt; 폴더를 등록했다.&lt;/li&gt;
&lt;li&gt;Pyglet 키 이벤트를 바인딩해 Z 키와 방향키 입력을 raw 액션 벡터로 기록했다.&lt;/li&gt;
&lt;li&gt;한 에피소드가 끝나면 별도 &lt;code&gt;.npz&lt;/code&gt; 파일로 저장했다.&lt;/li&gt;
&lt;li&gt;데이터 로딩 속도 병목을 해소하기 위해 모든 에피소드 파일을 메모리에 미리 로드하는 &lt;code&gt;MarioFastDataset&lt;/code&gt;을 구현했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class MarioFastDataset(Dataset):
    def __init__(self, npz_dir, seq_len=10):
        files = sorted(glob.glob(f&quot;{npz_dir}/*.npz&quot;))
        self.seq_len = seq_len
        self.frames_list = []
        self.actions_list = []
        for f in files:
            arr = np.load(f)
            frames  = arr['frames'].astype(np.float32) / 255.0  # 정규화
            actions = arr['actions']
            self.frames_list.append(frames)
            self.actions_list.append(actions)
        self.lengths = [len(a) - seq_len for a in self.actions_list]
        self.cum_lengths = np.cumsum(self.lengths)
    def __len__(self):
        return int(self.cum_lengths[-1])
    def __getitem__(self, idx):
        ep = np.searchsorted(self.cum_lengths, idx, side='right')
        start = idx - (self.cum_lengths[ep-1] if ep&amp;gt;0 else 0)
        frames  = self.frames_list[ep]
        actions = self.actions_list[ep]
        x = frames[start:start+self.seq_len]
        y = actions[start+self.seq_len]
        x = np.expand_dims(x, 1)  # (seq_len,1,84,84)
        return torch.from_numpy(x), torch.tensor(y, dtype=torch.long)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모델 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CNN 특징 추출기와 LSTM 순환층을 결합한 &lt;code&gt;MarioRNN&lt;/code&gt; 모델을 사용한다.&lt;br /&gt;각 프레임에서 특징 벡터를 추출하고 시퀀스 전체를 LSTM으로 처리해 마지막 시점 출력을 분류한다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;import torch.nn as nn

class MarioRNN(nn.Module):
    def __init__(self, hidden_size=128, n_layers=1, n_actions=3):
        super().__init__()
        self.cnn = nn.Sequential(
            nn.Conv2d(1,16,3,stride=2,padding=1),
            nn.ReLU(),
            nn.Conv2d(16,32,3,stride=2,padding=1),
            nn.ReLU(),
            nn.Flatten()  # (32*21*21)
        )
        feat_size = 32 * 21 * 21
        self.lstm = nn.LSTM(
            input_size=feat_size,
            hidden_size=hidden_size,
            num_layers=n_layers,
            batch_first=True
        )
        self.fc = nn.Linear(hidden_size, n_actions)
    def forward(self, x):
        B,S,C,H,W = x.shape
        x = x.view(B*S, C, H, W)
        feat = self.cnn(x)            # (B*S, feat_size)
        feat = feat.view(B, S, -1)    # (B,S,feat_size)
        out, _ = self.lstm(feat)      # (B,S,hidden)
        return self.fc(out[:, -1, :]) # (B,n_actions)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 설명&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;nn.Conv2d&lt;/code&gt; 두 층으로 입력 프레임 특징 벡터 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nn.LSTM&lt;/code&gt;으로 시퀀스를 순차 처리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;out[:, -1, :]&lt;/code&gt;로 마지막 시점 은닉 상태 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nn.Linear&lt;/code&gt;로 행동(직진&amp;middot;좌회전+가속&amp;middot;우회전+가속) 확률 예측&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;학습 스크립트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Colab T4 환경에서 빠른 학습을 위해 메모리 캐시 데이터셋과 &lt;code&gt;num_workers&lt;/code&gt;를 활용한다.&lt;br /&gt;각 epoch마다 loss와 accuracy를 출력해 학습 경과를 모니터링한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from tqdm.notebook import tqdm
import time

def train(data_dir, epochs=20, lr=1e-3, batch_size=32, seq_len=15):
    loader = get_fast_loader(data_dir, batch_size, seq_len, num_workers=4)
    print(&quot;  총 배치 수:&quot;, len(loader))
    print(&quot;  Using device:&quot;, DEVICE)
    xb,yb = next(iter(loader))
    print(&quot;샘플 배치 shapes &amp;rarr; x:&quot;, xb.shape, &quot;y:&quot;, yb.shape)

    model = MarioRNN().to(DEVICE)
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)

    for epoch in range(1, epochs+1):
        print(f&quot;\n▶ Epoch {epoch}/{epochs}&quot;)
        total_loss, correct, total = 0,0,0
        for x,y in tqdm(loader, leave=False):
            x,y = x.to(DEVICE), y.to(DEVICE)
            logits = model(x)
            loss = criterion(logits,y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            total_loss += loss.item()
            preds = logits.argmax(dim=1)
            correct += (preds==y).sum().item()
            total += y.size(0)
        avg_loss = total_loss/len(loader)
        acc = correct/total*100
        print(f&quot;✅ loss={avg_loss:.4f} acc={acc:.2f}%&quot;)
    torch.save(model.state_dict(), MODEL_PATH)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동화된 단위 테스트로 데이터셋과 모델의 입출력 형태를 검증한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;pip install pytest
pytest -q&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;test_dataset.py&lt;/code&gt;는 &lt;code&gt;MarioFastDataset&lt;/code&gt;와 &lt;code&gt;get_fast_loader&lt;/code&gt;의 배치 형태를 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test_model.py&lt;/code&gt;는 &lt;code&gt;MarioRNN&lt;/code&gt; forward 출력 차원 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>dev/AI</category>
      <category>rnn gnsfus</category>
      <author>영리0</author>
      <guid isPermaLink="true">https://youngri.tistory.com/72</guid>
      <comments>https://youngri.tistory.com/72#entry72comment</comments>
      <pubDate>Sun, 18 May 2025 21:45:56 +0900</pubDate>
    </item>
    <item>
      <title>Langgraph 클래스 기반 코드 리팩토링</title>
      <link>https://youngri.tistory.com/71</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 langgraph를 사용할 때는 단순히 def 함수로 로직을 구현했던 경험이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이번에 투자 AI 모임 코드를 보면서 각 모듈이 클래스를 기반으로 설계되어 있다는 점이 눈에 띄었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 함수 기반 구현 vs 클래스 기반 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 기반 코드는 짧고 간단한 작업을 수행할 때 빠르게 작성할 수 있지만 다음과 같은 한계가 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;상태 관리의 어려움:&lt;/b&gt; 여러 함수에 걸쳐 데이터를 주고받으며 상태를 관리해야 하는 경우 복잡성이 증가함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드 중복:&lt;/b&gt; 반복되는 로직을 분리하여 관리하기 어렵고, 동일한 코드가 여러 곳에 등장할 수 있음.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유지보수 난이도:&lt;/b&gt; 프로젝트 규모가 커질수록 어떤 함수가 어느 역할을 수행하는지 파악하기 어려워짐.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 클래스 기반 구현은 관련 변수와 함수를 하나의 객체로 묶어 캡슐화함으로써 다음과 같은 장점을 제공한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;모듈화:&lt;/b&gt; 클래스 내부에 상태와 메서드를 묶어 관리하여 코드 구조를 명확하게 함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재사용성:&lt;/b&gt; 상속과 다형성을 통해 반복되는 로직을 최소화할 수 있음.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 용이성:&lt;/b&gt; 객체 단위의 테스트가 가능해지고 Mocking이나 스텁을 활용해 세밀한 단위 테스트 작성이 쉬워짐.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 후임자에게 인수인계를 할때 상태관리에 대해 설명하기 복잡한 경우가 있었는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;class 기반 구축이 되어있었다면 어땠을까를 다시한번 생각하게 되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드 예시&lt;/h2&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;class RewrittenFactorAgent:

    def __init__(self, llm):
        self.llm = llm

    def make_ast(self, state: FactorAgentState):
        print(f&quot;{state['hypothesis']}&quot;)
        prompt = ChatPromptTemplate.from_messages(
            [
                (&quot;system&quot;, Prompt[&quot;system&quot;]),
                (&quot;human&quot;, Prompt[&quot;human&quot;]),
            ]
        )
        chain = prompt | self.llm
 		# ...생략...

        state[&quot;market&quot;] = market_result
        return state

    def get_market_prices(self, tickers):
        # ...생략...
        return {}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>dev</category>
      <category>langgraph</category>
      <author>영리0</author>
      <guid isPermaLink="true">https://youngri.tistory.com/71</guid>
      <comments>https://youngri.tistory.com/71#entry71comment</comments>
      <pubDate>Sun, 11 May 2025 20:02:00 +0900</pubDate>
    </item>
    <item>
      <title>[구름톤 유니브] 1. ERD 설계 및 JPA 복습</title>
      <link>https://youngri.tistory.com/70</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;erd &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;설계하고&lt;span&gt; &lt;/span&gt;&lt;/span&gt;jpa, mybatis 복습하였다.&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;547&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5gVSA/btsNLUX1NjO/1kM6XgwPt2VJTK6Csws44K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5gVSA/btsNLUX1NjO/1kM6XgwPt2VJTK6Csws44K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5gVSA/btsNLUX1NjO/1kM6XgwPt2VJTK6Csws44K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5gVSA%2FbtsNLUX1NjO%2F1kM6XgwPt2VJTK6Csws44K%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;547&quot; height=&quot;314&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;팀 회의 설계도&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3225&quot; data-origin-height=&quot;1983&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAx5RI/btsNJ5mrPdA/OPzDvRPhaYZfBKw0kQ13n1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAx5RI/btsNJ5mrPdA/OPzDvRPhaYZfBKw0kQ13n1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAx5RI/btsNJ5mrPdA/OPzDvRPhaYZfBKw0kQ13n1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAx5RI%2FbtsNJ5mrPdA%2FOPzDvRPhaYZfBKw0kQ13n1%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;3225&quot; height=&quot;1983&quot; data-origin-width=&quot;3225&quot; data-origin-height=&quot;1983&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요구사항 분석&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;사용자(Users)&lt;/b&gt;: 로그인&amp;middot;회원 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공연(Shows)&lt;/b&gt;: 제목, 시간, 장소 등&lt;/li&gt;
&lt;li&gt;&lt;b&gt;좌석(Seats)&lt;/b&gt;: 공연별 구역&amp;middot;행&amp;middot;번호&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예약(Reservations)&lt;/b&gt;: 사용자 &amp;harr; 공연 매핑, 상태 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결제(Payments)&lt;/b&gt;: 예약 단위 결제 정보&lt;/li&gt;
&lt;li&gt;&lt;b&gt;매핑 테이블&lt;/b&gt;: 예약 &amp;harr; 좌석 (다대다 해소)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개체(Entity) 식별&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Users&lt;/li&gt;
&lt;li&gt;Shows&lt;/li&gt;
&lt;li&gt;Seats&lt;/li&gt;
&lt;li&gt;Reservations&lt;/li&gt;
&lt;li&gt;ReservationSeats&lt;/li&gt;
&lt;li&gt;Payments&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;관계(Relationship) 정의&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Users 1:N Reservations&lt;/li&gt;
&lt;li&gt;Shows 1:N Seats&lt;/li&gt;
&lt;li&gt;Shows 1:N Reservations&lt;/li&gt;
&lt;li&gt;Reservations 1:N ReservationSeats&lt;/li&gt;
&lt;li&gt;Seats 1:N ReservationSeats&lt;/li&gt;
&lt;li&gt;Reservations 1:1(또는 1:N) Payments&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;물리 모델링&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1484&quot; data-origin-height=&quot;755&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brI7fu/btsNKAGb9fY/oKwfYW8MwNGkBe3AF1GC80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brI7fu/btsNKAGb9fY/oKwfYW8MwNGkBe3AF1GC80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brI7fu/btsNKAGb9fY/oKwfYW8MwNGkBe3AF1GC80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrI7fu%2FbtsNKAGb9fY%2FoKwfYW8MwNGkBe3AF1GC80%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;1484&quot; height=&quot;755&quot; data-origin-width=&quot;1484&quot; data-origin-height=&quot;755&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;JPA 정리&lt;/h1&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;엔티티 매핑&lt;/b&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1746369449498&quot; class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity @Table(name = &quot;users&quot;) public class User { @Id @GeneratedValue private Long id; private String name; private String email; private String passwordHash; private LocalDateTime createdAt; // getter/setter }&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;&lt;br /&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;OneToMany / ManyToOne&lt;/li&gt;
&lt;li&gt;ManyToMany는 중간 테이블(ReservationSeat)로 풀어내는 것이 권장됨&lt;/li&gt;
&lt;li&gt;fetch 전략 (LAZY vs EAGER) 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;Repository 인터페이스&lt;br /&gt;&lt;/b&gt;&lt;/b&gt;
&lt;pre id=&quot;code_1746369470904&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface ReservationRepository extends JpaRepository&amp;lt;Reservation, Long&amp;gt; { List&amp;lt;Reservation&amp;gt; findByUserId(Long userId); }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JPQL &amp;amp; QueryDSL&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복잡한 조건이 필요할 땐 @Query로 JPQL 직접 작성&amp;nbsp;&lt;/li&gt;
&lt;li&gt;타입 안전성을 위해 쿼리DSL 도입 검토&lt;/li&gt;
&lt;li&gt;Q.JPA 와 마이바티스 섞어쓴다 했을때 JPQL 대신 마이바티스 를 쓰는 이유는?&lt;/li&gt;
&lt;li&gt;GPT A. &lt;b&gt;단순 CRUD&lt;/b&gt; 수준이라면 JPA(JPQL)로도 충분하지만,&lt;/li&gt;
&lt;li data-end=&quot;1516&quot; data-start=&quot;1414&quot;&gt;&lt;b&gt;동적&amp;middot;복잡 SQL&lt;/b&gt;, &lt;b&gt;DB 특화 기능&lt;/b&gt;, &lt;b&gt;성능 최적화&lt;/b&gt;, &lt;b&gt;레거시 SQL 재사용&lt;/b&gt; 등이 필요할 때는 MyBatis를 섞어 쓰는 것이 훨씬 편리하고 효율적입니다.&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;@Transactional 어노테이션으로 서비스 계층 트랜잭션 범위 지정&lt;/li&gt;
&lt;li&gt;예외 발생 시 롤백 전략 확인 (rollbackFor 등)&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;MyBatis 정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XML 기반 설정과 SQL 매핑을 선호할 때 사용&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;설정 파일 (mybatis-config.xml)&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/b&gt;
&lt;pre id=&quot;code_1746369488824&quot; class=&quot;xml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt; &amp;lt;configuration&amp;gt; &amp;lt;environments default=&quot;development&quot;&amp;gt; &amp;lt;environment id=&quot;development&quot;&amp;gt; &amp;lt;transactionManager type=&quot;JDBC&quot;/&amp;gt; &amp;lt;dataSource type=&quot;POOLED&quot;&amp;gt; &amp;lt;!-- DB 커넥션 설정 --&amp;gt; &amp;lt;/dataSource&amp;gt; &amp;lt;/environment&amp;gt; &amp;lt;/environments&amp;gt; &amp;lt;mappers&amp;gt; &amp;lt;mapper resource=&quot;mappers/UserMapper.xml&quot;/&amp;gt; &amp;lt;/mappers&amp;gt; &amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;매퍼 XML 예시 (UserMapper.xml)&lt;br /&gt;&lt;/b&gt;&lt;/b&gt;
&lt;pre id=&quot;code_1746369501272&quot; class=&quot;pgsql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&amp;lt;mapper namespace=&quot;com.example.mapper.UserMapper&quot;&amp;gt; &amp;lt;select id=&quot;selectUser&quot; resultType=&quot;User&quot;&amp;gt; SELECT * FROM users WHERE id = #{id} &amp;lt;/select&amp;gt; &amp;lt;insert id=&quot;insertUser&quot; useGeneratedKeys=&quot;true&quot; keyProperty=&quot;id&quot;&amp;gt; INSERT INTO users (name, email, password_hash, created_at) VALUES (#{name}, #{email}, #{passwordHash}, #{createdAt}) &amp;lt;/insert&amp;gt; &amp;lt;/mapper&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;Mapper 인터페이스&lt;br /&gt;&lt;/b&gt;&lt;/b&gt;
&lt;pre id=&quot;code_1746369521097&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface UserMapper { User selectUser(Long id); void insertUser(User user); }&lt;/code&gt;&lt;/pre&gt;
&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장점 vs 단점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;: SQL 제어력 극대화, 복잡도 높은 쿼리 유연&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;: XML 관리 부담, 코드&amp;harr;SQL 분리로 가독성 저하 가능성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA Mybatis 섞어야함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ibatis 와의 차이점은 뭘까?&lt;/p&gt;</description>
      <category>dev/구름톤 유니브</category>
      <category>구름톤</category>
      <category>구름톤 유니브</category>
      <category>스프링 공부</category>
      <author>영리0</author>
      <guid isPermaLink="true">https://youngri.tistory.com/70</guid>
      <comments>https://youngri.tistory.com/70#entry70comment</comments>
      <pubDate>Sun, 4 May 2025 23:14:16 +0900</pubDate>
    </item>
    <item>
      <title>[Agent 연습] Claude MCP 알아보기</title>
      <link>https://youngri.tistory.com/69</link>
      <description>&lt;h1&gt;Claude MCP&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;425&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nEE6q/btsNi3P3wbA/L51ly2yFBXElpZwHaX6K40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nEE6q/btsNi3P3wbA/L51ly2yFBXElpZwHaX6K40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nEE6q/btsNi3P3wbA/L51ly2yFBXElpZwHaX6K40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnEE6q%2FbtsNi3P3wbA%2FL51ly2yFBXElpZwHaX6K40%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;586&quot; height=&quot;425&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;425&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;최근 Anthropic이 개발한 오픈 소스 표준 &lt;b&gt;Claude MCP(Model Context Protocol)&lt;/b&gt;를 공부하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP는 AI 모델이 외부 데이터 소스 및 다양한 도구와 연결되도록 하는 프로토콜 (Like USB-C 포트) 임&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;원래는 function calling 기법으로 프로젝트를 기획했지만 이번에 MCP 방식으로 마이그래이션하기로 결정&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MCP 개요 및 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP는 AI 모델과 외부 시스템이 원활하게 통신할 수 있도록 설계되었음.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. MCP Host&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Claude Desktop, IDE, LangGraph 에이전트 등 MCP를 활용하는 주체.&lt;/li&gt;
&lt;li&gt;사용자 요청을 MCP 형식으로 변환.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. MCP Server&lt;/h3&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;h3 data-ke-size=&quot;size23&quot;&gt;3. MCP Client&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Host와 Server 간 통신을 중개하는 프로토콜 변환.&lt;/li&gt;
&lt;li&gt;SSE(실시간 스트리밍)와 Stdio(표준 입출력) 등 다양한 전송 방식을 지원함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LangGraph와의 통합 진행&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP는 LangChain-MCP Adapters를 통해 LangGraph 에이전트와 쉽게 통합됨.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MCP 서버 설정 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수학 연산 서버 예시&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# math_server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(&quot;Math&quot;)

@mcp.tool()
def add(a: int, b: int) -&amp;gt; int:
    return a + b

mcp.run(transport=&quot;stdio&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 통해 간단한 수학 연산 기능을 MCP 서버에 탑재 가능&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LangGraph 에이전트 구성 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 LangGraph 에이전트에 MCP 클라이언트를 연결&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent

async with MultiServerMCPClient({
    &quot;math&quot;: {&quot;command&quot;: &quot;python&quot;, &quot;args&quot;: [&quot;math_server.py&quot;], &quot;transport&quot;: &quot;stdio&quot;}
}) as client:
    agent = create_react_agent(model, client.get_tools())
    response = await agent.ainvoke({&quot;messages&quot;: &quot;3+5*2 계산해줘&quot;})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식을 사용하여 JSON 설정으로 실시간 도구 추가 및 제거를 쉽게 관리 가능&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 적용 사례&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP 도입 후 활용 사례는 다양하게 진행됨.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;자동화 워크플로우&lt;/b&gt;&lt;br /&gt;Git 리포지토리 분석 &amp;rarr; README 자동 생성 &amp;rarr; Slack 알림 전송 등의 작업을 자동화함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 분석&lt;/b&gt;&lt;br /&gt;Postgres DB 쿼리 수행 &amp;rarr; 데이터 시각화 생성 &amp;rarr; 보고서 작성 등 단계를 연계하여 진행함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실시간 협업&lt;/b&gt;&lt;br /&gt;다중 MCP 서버(예: ChatGPT와 Claude)를 LangGraph로 오케스트레이션하여 실시간 협업 환경을 구성함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;function calling 방식에서 MCP 방식으로 마이그래이션 결정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 vllm function calling 기법을 사용하여 프로젝트를 진행할 계획이었음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 도구 연동의 복잡성과 설정 관리의 어려움을 경험함. 이에 따라 보다 표준화되고 유연한 MCP 방식을 도입하기로 결정함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP 방식의 주요 장점은 아래와 같음.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;표준화된 인터페이스 제공&lt;/b&gt;: 다양한 서버와 클라이언트, 도구를 일관된 방식으로 연결함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설정 시간 절감&lt;/b&gt;: JSON 기반의 도구 관리 덕분에 설정 및 배포 시간이 대폭 단축됨.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;혼합 전송 방식 지원&lt;/b&gt;: SSE와 Stdio를 혼용하여 실시간 스트리밍과 기본 입출력 모두 지원함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성 및 신뢰성&lt;/b&gt;: 엔터프라이즈 시스템 연동에 대해 이미 검증됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP 방식으로의 마이그래이션을 통해 기존의 vllm 방식에서 벗어나, 더욱 표준화된 도구 통합 시스템을 구축 가능하다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 설정은 복잡할 수 있으나, 한 번 구축해두면 다양한 외부 도구와의 연동 및 AI 모델 기능 확장이 원활하다!!&lt;/p&gt;</description>
      <category>dev/AI</category>
      <category>AI</category>
      <category>langchain</category>
      <category>langgraph</category>
      <category>langgraph mcp</category>
      <category>MCP</category>
      <category>vllmmcp</category>
      <author>영리0</author>
      <guid isPermaLink="true">https://youngri.tistory.com/69</guid>
      <comments>https://youngri.tistory.com/69#entry69comment</comments>
      <pubDate>Sun, 13 Apr 2025 20:17:01 +0900</pubDate>
    </item>
    <item>
      <title>소설 캐릭터 정보 추출 &amp;amp; 대화 토이프로젝트 (Book Buddy)</title>
      <link>https://youngri.tistory.com/68</link>
      <description>&lt;h1&gt;소설 캐릭터 챗봇 개발기: 텍스트 처리부터 채팅 구현까지&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹소설을 읽다가 궁금한 점이 생기면 바로 캐릭터와 대화하면서 확인하고 싶었습니다. 소설을 읽기 전 등장인물들과 대화할 수 있는 기능을 만들고자 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-24 오후 11.32.44.png&quot; data-origin-width=&quot;1580&quot; data-origin-height=&quot;1216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cy6xnn/btsKUf6f6Wd/5rDlH9p9I5VVKj32H1T3Y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cy6xnn/btsKUf6f6Wd/5rDlH9p9I5VVKj32H1T3Y1/img.png&quot; data-alt=&quot;달빛조각사 / 주인공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cy6xnn/btsKUf6f6Wd/5rDlH9p9I5VVKj32H1T3Y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcy6xnn%2FbtsKUf6f6Wd%2F5rDlH9p9I5VVKj32H1T3Y1%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;593&quot; height=&quot;456&quot; data-filename=&quot;스크린샷 2024-11-24 오후 11.32.44.png&quot; data-origin-width=&quot;1580&quot; data-origin-height=&quot;1216&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;달빛조각사 / 주인공&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a href=&quot;https://github.com/YoonJae00/BookBuddy&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/YoonJae00/BookBuddy&lt;/a&gt;&lt;/h2&gt;
&lt;figure id=&quot;og_1732458807168&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - YoonJae00/BookBuddy: 소설원문 기반 채팅 app&quot; data-og-description=&quot;소설원문 기반 채팅 app. Contribute to YoonJae00/BookBuddy development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/YoonJae00/BookBuddy&quot; data-og-url=&quot;https://github.com/YoonJae00/BookBuddy&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/EtTBp/hyXDiBl0mV/lXg9BCOskQTH8uXdabgdw0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/d61oXA/hyXDjNNxpI/67MCU7gNALRATUjeUMca0k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/YoonJae00/BookBuddy&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/YoonJae00/BookBuddy&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/EtTBp/hyXDiBl0mV/lXg9BCOskQTH8uXdabgdw0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/d61oXA/hyXDjNNxpI/67MCU7gNALRATUjeUMca0k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - YoonJae00/BookBuddy: 소설원문 기반 채팅 app&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;소설원문 기반 채팅 app. Contribute to YoonJae00/BookBuddy development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 텍스트 분할 (Text Splitting)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소설 텍스트를 효율적으로 처리하기 위해 청크 단위로 분할했습니다:&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;            # 2. 텍스트 분할
            chunks = self.text_splitter.split_text(content)
            if not chunks:
                raise NovelProcessingError(&quot;Failed to split text into chunks&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 고려한 점들:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문맥 유지를 위한 적절한 청크 크기 설정&lt;/li&gt;
&lt;li&gt;청크 간 오버랩으로 문맥 연결성 확보&lt;/li&gt;
&lt;li&gt;메모리 효율성 고려&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;LLM 일부 데이터 확인하기&quot; href=&quot;https://smith.langchain.com/public/8bc2632b-70ea-4471-9e1c-0ae45b153331/r&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://smith.langchain.com/public/8bc2632b-70ea-4471-9e1c-0ae45b153331/r&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1732459062246&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;LangSmith&quot; data-og-description=&quot;&quot; data-og-host=&quot;smith.langchain.com&quot; data-og-source-url=&quot;https://smith.langchain.com/public/8bc2632b-70ea-4471-9e1c-0ae45b153331/r&quot; data-og-url=&quot;https://smith.langchain.com/public/8bc2632b-70ea-4471-9e1c-0ae45b153331/r&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://smith.langchain.com/public/8bc2632b-70ea-4471-9e1c-0ae45b153331/r&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://smith.langchain.com/public/8bc2632b-70ea-4471-9e1c-0ae45b153331/r&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;LangSmith&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;smith.langchain.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;h2 data-ke-size=&quot;size26&quot;&gt;2. 캐릭터 추출 (Character Extraction)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분할된 텍스트에서 등장인물 정보를 추출합니다:&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;            # 1. 초기 캐릭터 식별
            characters = await self._identify_characters(content, novel_id)
            if not characters:
                raise NovelProcessingError(&quot;No characters found in the novel&quot;)

            for char in characters:
                self.name_resolver.add_character(char['full_name'], char['aliases'])
                self.logger.log_character_found(char['full_name'], char)

                # 캐릭터 정보 저장 (동기식)
                db.save_character({
                    'id': str(uuid.uuid4()),
                    'novel_id': novel_id,
                    **char
                })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추출하는 캐릭터 정보:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이름과 별칭&lt;/li&gt;
&lt;li&gt;성격 특성&lt;/li&gt;
&lt;li&gt;배경 정보&lt;/li&gt;
&lt;li&gt;관계도&lt;/li&gt;
&lt;li&gt;역할&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-24 오후 11.26.58.png&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYrhFW/btsKT1GM6l0/EJCneQIn30kouiZ1ZW4Co1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYrhFW/btsKT1GM6l0/EJCneQIn30kouiZ1ZW4Co1/img.png&quot; data-alt=&quot;이름/성격/특성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYrhFW/btsKT1GM6l0/EJCneQIn30kouiZ1ZW4Co1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYrhFW%2FbtsKT1GM6l0%2FEJCneQIn30kouiZ1ZW4Co1%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;388&quot; height=&quot;304&quot; data-filename=&quot;스크린샷 2024-11-24 오후 11.26.58.png&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;740&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이름/성격/특성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-24 오후 11.27.17.png&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bj9qTj/btsKT8MFLJz/TdLk75WmO2AtvT7tkcqPv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bj9qTj/btsKT8MFLJz/TdLk75WmO2AtvT7tkcqPv0/img.png&quot; data-alt=&quot;상세 등등&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bj9qTj/btsKT8MFLJz/TdLk75WmO2AtvT7tkcqPv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbj9qTj%2FbtsKT8MFLJz%2FTdLk75WmO2AtvT7tkcqPv0%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;513&quot; height=&quot;239&quot; data-filename=&quot;스크린샷 2024-11-24 오후 11.27.17.png&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;332&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;상세 등등&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-24 오후 11.27.32.png&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIPZSf/btsKTooYYnJ/FdSIjTTWlYbG3Th4Ta9klk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIPZSf/btsKTooYYnJ/FdSIjTTWlYbG3Th4Ta9klk/img.png&quot; data-alt=&quot;관계&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIPZSf/btsKTooYYnJ/FdSIjTTWlYbG3Th4Ta9klk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIPZSf%2FbtsKTooYYnJ%2FFdSIjTTWlYbG3Th4Ta9klk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;594&quot; height=&quot;321&quot; data-filename=&quot;스크린샷 2024-11-24 오후 11.27.32.png&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;관계&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 이벤트 추출 (Event Extraction)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐릭터들의 행동과 사건을 추출합니다:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;            # 3. 각 청크 처리
            all_events = []
            for i, chunk in enumerate(chunks):
                try:
                    normalized_chunk = self._normalize_names(chunk)
                    events = await self._extract_events(normalized_chunk, i)
                    for event in events:
                        # 이벤트 저장 (동기식)
                        event['id'] = str(uuid.uuid4())
                        event['novel_id'] = novel_id
                        db.save_event(event)
                        self.logger.log_event_extracted(event['summary'], i)
                    all_events.extend(events)
                except Exception as e:
                    self.logger.log_error(f&quot;Error processing chunk {i}&quot;, {&quot;error&quot;: str(e)})
                    continue&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 데이터 구조:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사건 요약&lt;/li&gt;
&lt;li&gt;관련 캐릭터&lt;/li&gt;
&lt;li&gt;발생 위치(챕터)&lt;/li&gt;
&lt;li&gt;중요도&lt;/li&gt;
&lt;li&gt;감정 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://smith.langchain.com/public/8cc0e5b5-c393-4745-8f09-a237c7856b8d/r&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://smith.langchain.com/public/8cc0e5b5-c393-4745-8f09-a237c7856b8d/r&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1732458709266&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;LangSmith&quot; data-og-description=&quot;&quot; data-og-host=&quot;smith.langchain.com&quot; data-og-source-url=&quot;https://smith.langchain.com/public/8cc0e5b5-c393-4745-8f09-a237c7856b8d/r&quot; data-og-url=&quot;https://smith.langchain.com/public/8cc0e5b5-c393-4745-8f09-a237c7856b8d/r&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://smith.langchain.com/public/8cc0e5b5-c393-4745-8f09-a237c7856b8d/r&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://smith.langchain.com/public/8cc0e5b5-c393-4745-8f09-a237c7856b8d/r&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;LangSmith&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;smith.langchain.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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-24 오후 11.30.39.png&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;1044&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHVPqw/btsKUHOESsi/5pLOtNlNX2Z5VpilH8T06K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHVPqw/btsKUHOESsi/5pLOtNlNX2Z5VpilH8T06K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHVPqw/btsKUHOESsi/5pLOtNlNX2Z5VpilH8T06K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHVPqw%2FbtsKUHOESsi%2F5pLOtNlNX2Z5VpilH8T06K%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;621&quot; height=&quot;655&quot; data-filename=&quot;스크린샷 2024-11-24 오후 11.30.39.png&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;1044&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 채팅 시스템 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추출된 정보를 바탕으로 캐릭터와의 대화를 구현합니다:&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class CharacterChatbot:
    def __init__(self, character_data: Dict, events: List[Dict], settings, user_id: str):
        self.character = character_data
        self.events = events
        self.user_id = user_id
        self.db = DatabaseService()
        self.vector_store = VectorStore(settings)
        self.llm = ChatOpenAI(
            temperature=0.7,
            model=&quot;gpt-4o-mini&quot;,
            openai_api_key=settings.OPENAI_API_KEY
        )
        )
        self.chat_history = ChatMessageHistory()
        # 응답 생성
        # 이전 대화 기록 로드
        history = self.db.get_chat_history(
            character_id=character_data['id'],
            user_id=user_id
        )
            성격: {character[personality_traits]}
        for msg in history:
            if msg['role'] == 'user':
                self.chat_history.add_user_message(msg['content'])
            else:
                self.chat_history.add_ai_message(msg['content'])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅 시스템의 주요 특징:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐릭터 성격 반영&lt;/li&gt;
&lt;li&gt;관련 이벤트 참조&lt;/li&gt;
&lt;li&gt;대화 맥락 유지&lt;/li&gt;
&lt;li&gt;이전 대화 기록 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터 구조 설계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐릭터 정보&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;    def save_character(self, character_data: Dict) -&amp;gt; None:
        &quot;&quot;&quot;캐릭터 정보 저장&quot;&quot;&quot;
        try:
            character_id = character_data.get('id')
            if not character_id:
                raise Exception(&quot;Character ID is required&quot;)
                .where('novel_id', '==', novel_id)\
            self.db.collection('characters')\
                .document(character_id)\
                .set(character_data)
                .where('novel_id', '==', novel_id)\
        except Exception as e:
            raise Exception(f&quot;Failed to save character: {str(e)}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이벤트 저장&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;    def save_event(self, event_data: Dict) -&amp;gt; None:
        &quot;&quot;&quot;이벤트 정보 저장&quot;&quot;&quot;
        try:
            self.db.collection('events').document(event_data['id']).set(event_data)
        except Exception as e:
            raise Exception(f&quot;Failed to save event: {str(e)}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대화 기록&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;    def save_chat_history(self, character_id: str, user_id: str, message: Dict) -&amp;gt; None:
        &quot;&quot;&quot;대화 기록 저장&quot;&quot;&quot;
        try:
            self.db.collection('chat_history').add({
                'character_id': character_id,
                'user_id': user_id,
                'content': message['content'],
                'role': message['role'],
                'timestamp': firestore.SERVER_TIMESTAMP
            })
        except Exception as e:
            print(f&quot;Failed to save chat history: {str(e)}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발 과정에서 배운 점&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;텍스트 처리 최적화&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 텍스트를 저장할 필요 없이, 필요한 정보만 추출하여 저장&lt;/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;캐릭터의 경험과 기억을 이벤트 단위로 관리&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;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;향후 개선 방향&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;텍스트 분석 고도화&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;더 정확한 캐릭터 정보 추출&lt;/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;실시간 피드백 반영&lt;/li&gt;
&lt;li&gt;대화 컨텍스트 시각화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>dev/AI</category>
      <author>영리0</author>
      <guid isPermaLink="true">https://youngri.tistory.com/68</guid>
      <comments>https://youngri.tistory.com/68#entry68comment</comments>
      <pubDate>Sun, 24 Nov 2024 23:34:27 +0900</pubDate>
    </item>
    <item>
      <title>Agent 만들기 [8] - 분신 성격 업데이트: LLM과 상호작용 분석을 활용한 페르소나 발전</title>
      <link>https://youngri.tistory.com/67</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI의 GPT-4o 모델과 Firebase, Redis, Firestore, LangChain 활용.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 동작 원리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 상호작용 데이터 수집&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis와 Firestore를 사용하여 사용자의 대화와 행동 데이터를 저장&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;async def store_user_interaction(uid: str, interaction_data: dict):
    # Redis 및 Firestore에 상호작용 저장
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. LLM 기반 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangChain을 활용해 OpenAI GPT-4 모델로 사용자 상호작용 데이터를 분석.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;analysis_template = &quot;&quot;&quot;
당신은 사용자의 분신 페르소나를 발전시키는 전문가입니다.
...
&quot;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 페르소나 업데이트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석 결과를 Firestore에 저장된 기존 페르소나와 결합하여 업데이트.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;async def analyze_and_update_persona(uid: str):
    # 상호작용 데이터 분석 및 Firestore 업데이트
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상호작용 데이터 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석에 사용된 상호작용 데이터는 다음과 같이 Redis에 저장:&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[
    {&quot;timestamp&quot;: &quot;2024-11-21T10:00:00&quot;, &quot;type&quot;: &quot;message&quot;, &quot;message&quot;: &quot;안녕! 오늘 기분 어때?&quot;},
    {&quot;timestamp&quot;: &quot;2024-11-21T10:05:00&quot;, &quot;type&quot;: &quot;message&quot;, &quot;message&quot;: &quot;나는 새로운 프로젝트를 시작했어.&quot;}
]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;업데이트 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 페르소나를 업데이트:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-04 오후 7.43.56.png&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;870&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFoeZg/btsKPUaeDCg/qMJR5U35eOWqsmyxngke11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFoeZg/btsKPUaeDCg/qMJR5U35eOWqsmyxngke11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFoeZg/btsKPUaeDCg/qMJR5U35eOWqsmyxngke11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFoeZg%2FbtsKPUaeDCg%2FqMJR5U35eOWqsmyxngke11%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;952&quot; height=&quot;870&quot; data-filename=&quot;스크린샷 2024-11-04 오후 7.43.56.png&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;870&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>dev/AI</category>
      <author>영리0</author>
      <guid isPermaLink="true">https://youngri.tistory.com/67</guid>
      <comments>https://youngri.tistory.com/67#entry67comment</comments>
      <pubDate>Thu, 21 Nov 2024 12:31:29 +0900</pubDate>
    </item>
    <item>
      <title>nest.js + react (jotai) 개발 [2] - jotai 비동기 처리</title>
      <link>https://youngri.tistory.com/66</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 비동기 Atom&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본적인 비동기 Atom&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// 가장 심플한 비동기 atom
const asyncAtom = atom(async () =&amp;gt; {
  const response = await fetch('https://api.example.com/data');
  return response.json();
});&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;redux + thunk 처럼 길지 않고 매우 편리한거 같음!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로딩/에러 상태 처리&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;const todoWithStatusAtom = atom(async (get) =&amp;gt; {
  try {
    const data = await get(asyncAtom);
    return { status: 'success', data };
  } catch (error) {
    return { status: 'error', error };
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 실제 로그인 구현하기&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-19 오후 12.50.10.png&quot; data-origin-width=&quot;746&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2YLYC/btsKN2FcXdy/WwZ5OK1yot1VaAKYxyhiW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2YLYC/btsKN2FcXdy/WwZ5OK1yot1VaAKYxyhiW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2YLYC/btsKN2FcXdy/WwZ5OK1yot1VaAKYxyhiW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2YLYC%2FbtsKN2FcXdy%2FWwZ5OK1yot1VaAKYxyhiW0%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;746&quot; height=&quot;484&quot; data-filename=&quot;스크린샷 2024-11-19 오후 12.50.10.png&quot; data-origin-width=&quot;746&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Auth Atom 만들기&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;interface LoginResponse {
    accessToken?: string;
}

interface LoginRequest {
    userId: string;
    userPassword: string;
}

// 인증 상태 저장용 atom
export const authAtom = atom&amp;lt;LoginResponse | null&amp;gt;(null);

// 로그인 처리용 atom
export const loginAtom = atom(
    (get) =&amp;gt; get(authAtom),
    async (get, set, payload: LoginRequest) =&amp;gt; {
        await axios.post&amp;lt;LoginResponse&amp;gt;('/auth/login', payload)
            .then((res) =&amp;gt; {
                set(authAtom, res.data);
            })
    }
);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용하기&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function LoginComponent() {
    const [auth] = useAtom(authAtom);
    const [, login] = useAtom(loginAtom);

    const handleLogin = async () =&amp;gt; {
        await login({
            userId: &quot;user123&quot;,
            userPassword: &quot;pass123&quot;
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. useEffect vs Jotai 비동기 Atom&lt;/h2&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
    const fetchData = async () =&amp;gt; {
        try {
            setLoading(true);
            const response = await axios.get('/api/data');
            setData(response.data);
        } catch (error) {
            setError(error);
        } finally {
            setLoading(false);
        }
    };

    fetchData();
}, []);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;const dataAtom = atom(async () =&amp;gt; {
    const response = await axios.get('/api/data');
    return response.data;
});

// 컴포넌트에서
const [data] = useAtom(dataAtom);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훨씬 짧아짐!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 왜 Jotai가 더 좋을까?&amp;nbsp;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;보일러플레이트 감소&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;loading, error 상태 관리 자동화&lt;/li&gt;
&lt;li&gt;try-catch 로직 단순화&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;TypeScript 친화적&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;code class=&quot;language-typescript&quot;&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>jotai</category>
      <category>react</category>
      <category>상태관리</category>
      <category>타입스크립트상태관리</category>
      <author>영리0</author>
      <guid isPermaLink="true">https://youngri.tistory.com/66</guid>
      <comments>https://youngri.tistory.com/66#entry66comment</comments>
      <pubDate>Tue, 19 Nov 2024 15:50:37 +0900</pubDate>
    </item>
    <item>
      <title>nest.js + react (jotai) 개발 [1] - jotai 에 대해..</title>
      <link>https://youngri.tistory.com/65</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Jotai가 뭔데?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 Jotai는 React 상태관리 라이브러리임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름이 일본어로 '상태'라는 뜻&lt;br /&gt;심플하고 직관적인거 같음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 사용법&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { atom, useAtom } from 'jotai';

const textAtom = atom('안녕하세요'); // 초기값 설정

function Component() {
  const [text, setText] = useAtom(textAtom);
  // useState랑 비슷하게 생겼지?
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;useState랑 뭐가 다른데?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 그냥 useState랑 비슷하네? 했는데 완전 다름.&lt;br /&gt;제일 큰 차이점은 &lt;b&gt;전역 상태 관리&lt;/b&gt;가 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어서:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useState는 컴포넌트 안에서만 씀&lt;/li&gt;
&lt;li&gt;Jotai는 여러 컴포넌트에서 같은 상태 공유 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;// atoms/themeAtom.ts
const isDarkAtom = atom(false);

// components/Header.tsx
function Header() {
  const [isDark] = useAtom(isDarkAtom);
  // 여기서 다크모드 상태 사용
}

// components/Footer.tsx
function Footer() {
  const [isDark] = useAtom(isDarkAtom);
  // 여기서도 같은 상태 사용 가능!
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언제 써야 할까?&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 걸 Jotai로 하면 안 됨&amp;nbsp; &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;중요&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Jotai 쓰기 좋은 경우:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 컴포넌트에서 같이 쓰는 상태 (다크모드, 로그인 정보 등)&lt;/li&gt;
&lt;li&gt;복잡한 상태 로직이 필요할 때&lt;/li&gt;
&lt;li&gt;props로 계속 넘기기 귀찮은 경우 (props drilling 피하기)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그냥 useState 쓰는게 나은 경우:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트 안에서만 쓰는 임시 상태&lt;/li&gt;
&lt;li&gt;간단한 토글이나 폼 입력값&lt;/li&gt;
&lt;li&gt;모달 열고 닫기같은 UI 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jotai의 더 고급 기능들 (비동기 처리, 파생 상태 등) 공부해봐야겠음.&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;hr data-ke-style=&quot;style1&quot; /&gt;</description>
      <category>dev/nest+jotai</category>
      <category>jotai</category>
      <category>react</category>
      <category>상태관리</category>
      <author>영리0</author>
      <guid isPermaLink="true">https://youngri.tistory.com/65</guid>
      <comments>https://youngri.tistory.com/65#entry65comment</comments>
      <pubDate>Tue, 19 Nov 2024 11:52:01 +0900</pubDate>
    </item>
  </channel>
</rss>