<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Aiden Tech Blog</title>
    <link>https://aidengoldkr.tistory.com/</link>
    <description>Aiden Tech Blog</description>
    <language>ko</language>
    <pubDate>Tue, 16 Jun 2026 15:07:44 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Aidengoldkr</managingEditor>
    <image>
      <title>Aiden Tech Blog</title>
      <url>https://tistory1.daumcdn.net/tistory/8518444/attach/d5809133461047749cbe739f36551313</url>
      <link>https://aidengoldkr.tistory.com</link>
    </image>
    <item>
      <title>이제 성우는 망했다 - GPT-Realtime-2</title>
      <link>https://aidengoldkr.tistory.com/27</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스트는 2026년 5월 7일에 공개된 OpenAI의 새 실시간 음성 모델군, &lt;b&gt;GPT-Realtime-2&lt;/b&gt;에 관한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제목은 의도적으로 어그로성으로 뽑았다. 진짜로 성우 업계가 사라진다는 뜻은 아니다. &lt;span style=&quot;color: #dddddd;&quot;&gt;(강은애 성우님 절대 지켜)&lt;/span&gt; 다만 이번 릴리즈를 직접 들여다보면, &quot;음성 에이전트가 사람과 구분 안 되는 시점&quot;이 더 이상 미래 시제가 아니라는 감각이 분명히 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Realtime API 자체가 베타에서 정식 출시(GA)로 전환됐고, 그 위에서 세 개의 모델이 같이 풀렸다. &lt;code&gt;gpt-realtime-2&lt;/code&gt;, &lt;code&gt;gpt-realtime-translate&lt;/code&gt;, &lt;code&gt;gpt-realtime-whisper&lt;/code&gt;. 각자 역할이 다르다.&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;p data-ke-size=&quot;size16&quot;&gt;이슈: 음성 파이프라인을 만들 때 보통 &lt;b&gt;STT &amp;rarr; LLM &amp;rarr; TTS&lt;/b&gt; 세 단계를 직접 엮어야 했다. 지연시간, 에러 누적, 상태 동기화 같은 문제가 매 단계마다 끼어들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 릴리즈는 그 세 단계를 OpenAI가 자기네 모델로 각각 갈아치웠다.&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;gpt-realtime-2&lt;/b&gt; &amp;mdash; 입출력이 모두 음성/텍스트/이미지인 네이티브 speech-to-speech 추론 모델. 컨텍스트 128K, 최대 출력 32K. 지식 컷오프는 2024년 9월 30일.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;gpt-realtime-translate&lt;/b&gt; &amp;mdash; 라이브 동시통역. 70개 이상 언어를 입력으로, 13개 언어로 출력. 힌디&amp;middot;타밀&amp;middot;텔루구에서 경쟁사 대비 WER이 12.5% 낮다고 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;gpt-realtime-whisper&lt;/b&gt; &amp;mdash; 스트리밍 STT. 기존 Whisper v2 대비 환각이 약 90% 줄었고, &lt;code&gt;gpt-4o-transcribe&lt;/code&gt; 대비도 약 70% 줄었다는 게 OpenAI 측 수치다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 두 가지다. 첫째, &lt;code&gt;gpt-realtime-2&lt;/code&gt;의 컨텍스트가 전작 대비 4배인 128K가 됐다는 것. 둘째, &lt;b&gt;reasoning effort&lt;/b&gt;라는 다이얼이 세션 단위로 붙었다는 것.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reasoning Effort &amp;mdash; 지연시간과 지능의 트레이드오프&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;gpt-realtime-2&lt;/code&gt;에는 &lt;code&gt;reasoning.effort&lt;/code&gt; 옵션이 있다. 호출마다 추론 강도를 골라 끼울 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매체별로 보도된 enum 값이 갈리는데, 더 디테일하게 다룬 쪽 기준으로는 다섯 단계다:&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;minimal | low (default) | medium | high | xhigh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값을 올릴수록 Big Bench Audio 점수가 올라가는 대신 첫 음성이 나오기까지 시간이 늘어난다. 보고된 수치 기준으로 minimal에서 약 1.12초, high에서 약 2.33초 정도다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import WebSocket from &quot;ws&quot;;

const ws = new WebSocket(
  &quot;wss://api.openai.com/v1/realtime?model=gpt-realtime-2&quot;,
  {
    headers: {
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
      &quot;OpenAI-Beta&quot;: &quot;realtime=v1&quot;,
    },
  }
);

ws.on(&quot;open&quot;, () =&amp;gt; {
  ws.send(JSON.stringify({
    type: &quot;session.update&quot;,
    session: {
      voice: &quot;alloy&quot;,
      modalities: [&quot;audio&quot;, &quot;text&quot;],
      reasoning: { effort: &quot;medium&quot; },          // 단계별로 응답 속도 vs 지능
      turn_detection: { type: &quot;server_vad&quot; },
      tools: [/* function definitions */],
    },
  }));
});&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;위 코드는 Realtime API의 기존 컨벤션을 따른 예시다. 실제 필드명은 출시 직후라 변경될 여지가 있으니, OpenAI 공식 changelog에서 한 번 더 확인하는 게 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설계 관점에서 이게 왜 의미 있냐면, &lt;b&gt;콜센터 메인 라인은 minimal로 빠르게 받고, 복잡한 약관 안내 같은 분기에서만 high로 끌어올리는 식의 분기 라우팅&lt;/b&gt;이 가능해진다는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;병렬 도구 호출과 Preamble&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;gpt-realtime-2&lt;/code&gt;는 한 턴 안에서 여러 함수를 동시에 호출할 수 있다. 그리고 그 사이에 &quot;잠깐만 확인해볼게요&quot; 같은 &lt;b&gt;preamble(필러 멘트)&lt;/b&gt;를 음성으로 흘려보낸다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;ws.send(JSON.stringify({
  type: &quot;session.update&quot;,
  session: {
    tool_choice: &quot;auto&quot;,
    parallel_tool_calls: true,
    instructions: &quot;Speak a short preamble before any tool call so the caller hears you working.&quot;,
  },
}));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 왜 중요하냐면, 전화 응대의 가장 큰 어색함이었던 &quot;긴 침묵 &amp;rarr; 갑작스러운 답변&quot; 패턴을 끊을 수 있기 때문이다. 사람이 검색하고 있는 듯한 소리가 중간에 들어가니, 사용자 입장에서는 &quot;지금 처리 중이구나&quot;라고 자연스럽게 받아들이게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거기에 &lt;b&gt;Interruption recovery&lt;/b&gt;까지 붙었다. 사용자가 중간에 끼어들어도 모델이 직전 맥락을 잃지 않고 이어서 답한다. 종합하면 turn-taking 벤치마크인 Conversational Dynamics에서 minimal 티어 기준 96.1%가 나온다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스트리밍 STT와 라이브 번역&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;gpt-realtime-whisper&lt;/code&gt;는 일반 Whisper와는 다른 모델이다. 배치 전사가 아니라 라이브 캡셔닝 용도로 튜닝됐다.&lt;/p&gt;
&lt;pre class=&quot;scilab&quot;&gt;&lt;code&gt;curl https://api.openai.com/v1/realtime/transcription_sessions \
  -H &quot;Authorization: Bearer $OPENAI_API_KEY&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d '{
    &quot;model&quot;: &quot;gpt-realtime-whisper&quot;,
    &quot;input_audio_format&quot;: &quot;pcm16&quot;,
    &quot;input_audio_transcription&quot;: { &quot;language&quot;: &quot;en&quot; }
  }'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요금은 분당 $0.017. 회의록이나 긴 인터뷰 통째로 떠야 한다면 여전히 기존 Whisper나 &lt;code&gt;gpt-4o-transcribe&lt;/code&gt;가 적합하다. 라이브 자막용으로 만들어진 모델이라는 점을 짚고 가야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;gpt-realtime-translate&lt;/code&gt;는 결제 인터뷰나 글로벌 콜센터 같은 동시통역 시나리오를 노린다.&lt;/p&gt;
&lt;pre class=&quot;scilab&quot;&gt;&lt;code&gt;curl https://api.openai.com/v1/realtime/translations \
  -H &quot;Authorization: Bearer $OPENAI_API_KEY&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d '{
    &quot;model&quot;: &quot;gpt-realtime-translate&quot;,
    &quot;source_language&quot;: &quot;ko&quot;,
    &quot;target_language&quot;: &quot;en&quot;
  }'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 한 가지 주의할 점은 &lt;b&gt;입출력 비대칭&lt;/b&gt;이다. 받을 수 있는 언어는 70개 이상이지만, 내보낼 수 있는 언어는 13개뿐이다. 양방향 대화 시나리오라면 양쪽 다 출력 가능한 언어인지 미리 확인해야 한다.&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;p data-ke-size=&quot;size16&quot;&gt;가격 보도가 매체마다 조금씩 다르게 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI 개발자 문서 페이지 기준으로 &lt;code&gt;gpt-realtime-2&lt;/code&gt;는 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;가격&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;오디오 입력&lt;/td&gt;
&lt;td&gt;$32 / 1M tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;오디오 출력&lt;/td&gt;
&lt;td&gt;$64 / 1M tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;오디오 캐시 입력&lt;/td&gt;
&lt;td&gt;$0.40 / 1M tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;텍스트 입력&lt;/td&gt;
&lt;td&gt;$4 / 1M tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;텍스트 출력&lt;/td&gt;
&lt;td&gt;$24 / 1M tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Latent Space는 같은 가격을 시간당으로 환산해서 오디오 입력 약 $1.15/hr, 출력 약 $4.61/hr이라고 정리했고, 이는 전작 &lt;code&gt;gpt-realtime-1.5&lt;/code&gt;의 시간당 단가와 동일하다고 설명했다. 둘 다 모순되는 정보는 아니지만, 글로 인용할 때는 공식 pricing 페이지를 한 번 더 확인하는 걸 권장한다. Realtime API가 정식 GA로 풀린 직후라 가격 동결 보장이 없는 상태다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;gpt-realtime-2&lt;/code&gt;는 &lt;b&gt;streaming, fine-tuning, structured outputs, predicted outputs를 지원하지 않는다.&lt;/b&gt; Function calling만 지원된다. 여기서 말하는 &quot;streaming&quot;은 Chat Completions 식 토큰 스트리밍이고, Realtime 자체의 오디오 스트리밍은 당연히 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;128K 컨텍스트는 세션 단위다.&lt;/b&gt; 한 통화가 길어지면 누적 토큰을 서버에서 관리해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지식 컷오프가 2024년 9월 30일&lt;/b&gt;이다. 2025년 이후 사실은 무조건 도구 호출로 끌어와야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SIP 연동&lt;/b&gt;이 1급 트랜스포트로 추가됐지만 공식 문서가 아직 빈약하다. 실제 SIP 트렁크 구성은 changelog를 파고들어야 할 가능성이 높다.&lt;/li&gt;
&lt;/ul&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;b&gt;운용 가능한 인터페이스로 다듬어진 정도&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reasoning effort 다이얼로 비용을 조절하고, parallel tool calling으로 답변 사이 침묵을 메우고, interruption recovery로 사람처럼 끼어듦을 흡수하고, SIP로 곧장 전화망에 꽂힌다. 음성 에이전트를 만들 때 일일이 직접 짜야 했던 UX 디테일을 OpenAI가 모델 레벨에서 가져갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배운 것: 실시간 음성 분야는 이제 &quot;모델 성능 경쟁&quot;에서 &quot;통합 운용 경쟁&quot;으로 한 단계 옮겨갔다. STT, LLM, TTS, 통역, 전화 트렁크까지 한 벤더가 묶어주는 시대에, 음성 인터페이스를 붙이려는 서비스 입장에서는 직접 파이프라인을 짜는 비용이 빠르게 사라지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성우가 망했다는 건 농담이지만, &lt;b&gt;합성 음성을 &quot;특수 자산&quot;으로 분리해서 다루던 시대가 끝나가고 있다&lt;/b&gt;는 건 농담이 아니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고 자료&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/models/gpt-realtime-2&quot;&gt;gpt-realtime-2 Model &amp;mdash; OpenAI API docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://techcrunch.com/2026/05/07/openai-launches-new-voice-intelligence-features-in-its-api/&quot;&gt;OpenAI launches new voice intelligence features in its API &amp;mdash; TechCrunch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.marktechpost.com/2026/05/08/openai-releases-three-realtime-audio-models-gpt-realtime-2-gpt-realtime-translate-and-gpt-realtime-whisper-in-the-realtime-api/&quot;&gt;OpenAI Releases Three Realtime Audio Models &amp;mdash; MarkTechPost&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.latent.space/p/ainews-gpt-realtime-2-translate-and&quot;&gt;AINews: GPT-Realtime-2, -Translate, and -Whisper &amp;mdash; Latent Space&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.buildfastwithai.com/blogs/openai-gpt-realtime-2-voice-ai-models&quot;&gt;GPT-Realtime-2: OpenAI Voice AI Models 2026 &amp;mdash; BuildFastWithAI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openai.com/index/advancing-voice-intelligence-with-new-models-in-the-api/&quot;&gt;Advancing voice intelligence with new models in the API &amp;mdash; OpenAI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>Aidengoldkr</author>
      <guid isPermaLink="true">https://aidengoldkr.tistory.com/27</guid>
      <comments>https://aidengoldkr.tistory.com/27#entry27comment</comments>
      <pubDate>Tue, 12 May 2026 07:56:52 +0900</pubDate>
    </item>
    <item>
      <title>Vercel 내부 시스템 무단 접근 사고 - 26.04.20</title>
      <link>https://aidengoldkr.tistory.com/26</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 4월 19일, Vercel이 내부 시스템에 대한 무단 접근이 발생했다고 공식적으로 인정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel은 Next.js의 핵심 후원사이자 전 세계 수백만 개발자가 사용하는 클라우드 배포 플랫폼이다. 이 사건이 단순한 기업 침해로 끝나지 않은 이유는, Vercel이 인터넷 인프라의 핵심을 담당하고 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://vercel.com/kb/bulletin/vercel-april-2026-security-incident&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://vercel.com/kb/bulletin/vercel-april-2026-security-incident&lt;/a&gt;&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;p data-ke-size=&quot;size16&quot;&gt;이슈: 공격자가 Vercel 내부 시스템에 침투해 소스 코드, 직원 계정 데이터, 고객 환경 변수, NPM 토큰, GitHub 개인 액세스 토큰(PAT)을 탈취했다고 주장했다.&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은 &quot;제한적인 하위 집합(limited subset)&quot;의 고객이 영향을 받았다고 밝혔다. 핵심 호스팅 서비스는 정상 운영 중이라고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격자는 사이버 범죄 포럼 &lt;b&gt;BreachForums&lt;/b&gt;에 탈취한 데이터를 &lt;b&gt;200만 달러&lt;/b&gt;에 판매한다는 게시글을 올렸다. 자신을 악명 높은 해킹 그룹인 &lt;b&gt;ShinyHunters&lt;/b&gt;라고 주장했고, Vercel 직원 수백명 (약 580명)의 계정 정보와 내부 대시보드 스크린샷을 샘플로 공개했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흥미로운 점은, ShinyHunters 본체가 이 사건과의 연관성을 전면 부인했다는 것이다. 실제 공격자의 정체는 여전히 불명확하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;침해의 시작점: 서드파티 AI 도구의 OAuth 앱&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책을 논하기 전에, 어떻게 침투했는지를 이해하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel의 자체 보안 게시판에 따르면, 이번 사건의 진입점은 Vercel 직원들이 업무에 사용하던 &lt;b&gt;소규모 서드파티 AI 도구&lt;/b&gt;였다. 이 도구는 Google Workspace OAuth 앱을 통해 직원의 Google 계정에 대한 접근 권한을 위임받은 상태였다.&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을 직접 공격한 것이 아니었다. 방어력이 상대적으로 취약한 AI 벤더의 인프라를 먼저 장악했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벤더가 장악되자, 벤더가 보관하고 있던 수백 개 조직의 OAuth 액세스 토큰이 공격자의 손에 들어갔다. 공격자는 탈취한 Vercel 직원의 OAuth 토큰으로 Google Workspace 환경에서 합법적인 사용자로 위장했고, 거기서부터 이슈 트래커인 &lt;b&gt;Linear&lt;/b&gt;와 &lt;b&gt;GitHub&lt;/b&gt; 엔터프라이즈 환경으로 측면 이동(Lateral Movement)을 수행했다.&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;침해 지표(IOC)로 공개된 손상된 OAuth App Client ID:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;110671459871-30f1spbu0hptbs60cb4vsmv79i7bbvqj.apps.googleusercontent.com&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;보안 연구자들과 보안 커뮤니티의 추정에 따르면 이 Client ID로 Google의 인증 동의 화면을 렌더링해 AI 도구 이름을 역추적하려 했을 때, Google 서버는 &quot;Error 401: invalid_client&quot;를 반환했다. 벤더가 자신의 신원이 공개되는 것을 막기 위해 OAuth 앱 자체를 삭제해버린 것이다.&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이 해당 AI 벤더의 이름을 공개하지 않은 결정은 보안 커뮤니티의 강한 비판을 받았다. 동일한 도구를 사내에서 사용 중인 다른 조직들이 적시에 방어 조치를 취하지 못하게 되기 때문이다.&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;p data-ke-size=&quot;size16&quot;&gt;Vercel은 환경 변수를 &lt;b&gt;민감한(Sensitive)&lt;/b&gt; 변수와 &lt;b&gt;일반(Non-sensitive)&lt;/b&gt; 변수로 구분하여 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;민감한 환경 변수&lt;/b&gt;는 쓰기 전용(Write-only)으로 취급된다. 한 번 등록하면 Vercel 대시보드나 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;문제는 &lt;b&gt;일반 환경 변수&lt;/b&gt;였다. 빠른 개발과 디버깅을 위해 접근성이 높은 형태로 저장되어 있는데, 관리자 권한이 탈취된 상황에서는 그 안에 있는 API 키, 데이터베이스 자격 증명, JWT 서명 비밀키가 전부 노출 위험에 처한다.&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;Vercel 대시보드에서 기존 키를 삭제하고 새 키를 입력하는 것만으로는 충분하지 않다. 공격자가 이미 이전 키를 데이터베이스 덤프로 확보한 상태라면, Vercel 내에서 값을 바꿔도 공격자의 구 키는 여전히 유효하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근본적인 대응은 &lt;b&gt;업스트림 서비스 제공자 측에서 기존 키를 명시적으로 폐기(Revoke)하는 것&lt;/b&gt;이다. AWS 콘솔, Stripe 대시보드, MongoDB Atlas 등에서 직접 접속해서 기존 액세스 토큰을 삭제하고 새로 발급받아야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NPM과 공급망 공격의 가능성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사건이 단순한 데이터 유출로 끝나지 않을 수 있는 이유가 바로 NPM 토큰이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel은 Next.js를 비롯해 JavaScript 생태계에서 수천만 건의 주간 다운로드를 기록하는 핵심 패키지들을 관리한다. 만약 공격자가 유효한 NPM 퍼블리시 토큰을 확보했다면, 악성 코드가 삽입된 업데이트를 공식 레지스트리에 배포하는 것이 가능해진다.&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;code&gt;npm install&lt;/code&gt; 한 번에 감염될 수 있는 시나리오다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel은 현재까지 빌드 조작이나 NPM 패키지 변조에 대한 명확한 증거는 없다고 밝혔다. 그러나 이론적 가능성만으로도 충분히 비상이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2026년 공급망 공격의 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사건을 독립적으로 보면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 1분기, &lt;b&gt;TeamPCP&lt;/b&gt;라는 위협 그룹이 LiteLLM이라는 LLM 프록시 라이브러리를 공격했다. LiteLLM은 하루 평균 340만 건 이상 다운로드되는 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;TeamPCP는 컨테이너 취약점 스캐너 Trivy와 정적 분석 도구 Checkmarx KICS의 배포 파이프라인을 먼저 침해하고, 거기서 탈취한 크리덴셜로 PyPI에 악성 LiteLLM 버전(1.82.7, 1.82.8)을 올렸다. 버전 1.82.8에는 &lt;code&gt;.pth&lt;/code&gt; 파일이 숨어 있었는데, Python 인터프리터가 실행되는 순간 해당 코드가 자동으로 실행되면서 AWS/GCP/Azure 토큰, SSH 키, &lt;code&gt;.env&lt;/code&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;이 캠페인의 직접적인 피해자 중 하나인 Mercor는 &lt;b&gt;4TB&lt;/b&gt; 규모의 데이터 유출을 공식 시인해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel 사건과 LiteLLM 사건의 공통점: &lt;b&gt;보안 검증 없이 서드파티 AI 도구에 과도한 권한을 부여하는 문화&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격자는 방화벽을 뚫지 않았다. 신뢰받는 서드파티를 통해 &quot;로그인&quot;했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Web3 생태계로의 전이 위험&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel이 단순한 정적 사이트 호스팅 업체가 아니라는 점이 이 사건을 더 복잡하게 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탈중앙화 거래소(DEX) 프론트엔드, 지갑 커넥터 인터페이스, 크로스체인 브리지 대시보드의 상당수가 Vercel 위에서 호스팅된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Web3 개발팀은 프라이빗 RPC 엔드포인트 URL, Alchemy/Infura API 키 등을 Vercel 환경 변수로 관리하는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비민감 환경 변수가 대량 노출되었다면, 특정 DeFi 프로젝트의 배포 파이프라인 권한까지 획득할 수 있다. 스마트 컨트랙트를 직접 건드리지 않고도, 프론트엔드 JavaScript를 미세하게 조작해 사용자의 지갑 트랜잭션을 공격자의 주소로 돌리는 것이 기술적으로 가능한 시나리오다.&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;같은 시기에 KelpDAO가 LayerZero 기반 rsETH 브리지 공격으로 &lt;b&gt;2억 9,200만 달러&lt;/b&gt; 규모의 자산을 탈취당했다. Vercel 침해와의 직접적 인과관계는 확인되지 않았지만, 두 사건이 겹치면서 DeFi 시장에 공포가 퍼졌고 Aave 토큰이 단기적으로 20% 폭락하는 연쇄 반응이 일어났다.&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;p data-ke-size=&quot;size16&quot;&gt;Vercel을 사용하고 있다면:&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;모든 비민감 환경 변수를 업스트림에서 폐기(Revoke)하고 재발급&lt;/b&gt;받을 것 &amp;mdash; Vercel 대시보드 교체가 아니라, AWS/Stripe/GitHub 등 각 서비스 콘솔에서 직접.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;새로 발급받은 모든 자격 증명은 Sensitive 플래그를 켜서 저장&lt;/b&gt;할 것.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GitHub 조직과 NPM 패키지에 연동된 Vercel 토큰을 전부 강제 만료&lt;/b&gt;시키고 재생성할 것.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Google Workspace 관리 콘솔에서 연동 앱 목록을 감사&lt;/b&gt;하고, 공개된 IOC(&lt;code&gt;110671459871-...&lt;/code&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;장기적으로는, 반영구적인 API 키를 환경 변수에 넣는 방식 자체를 재검토해야 한다. HashiCorp Vault나 AWS Secrets Manager를 통해 런타임 시점에 수명이 짧은 임시 토큰을 발급받는 &lt;b&gt;동적 시크릿 프로비저닝&lt;/b&gt; 구조로 전환하는 것이 근본적인 해결 방향이다.&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;p data-ke-size=&quot;size16&quot;&gt;이번 사건에서 배운 것은 명확하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대의 공격자는 방화벽을 뚫는 데 시간을 쓰지 않는다. 직원이 무비판적으로 OAuth 권한을 승인한 소규모 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;편의성과 보안은 언제나 상충하지만, AI 도구의 도입 속도가 보안 검토 속도를 압도하는 지금 이 시기에는 그 긴장이 특히 위험하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서드파티 AI 도구에 기업의 핵심 인프라 접근 권한을 부여하는 것은, 기존 엔터프라이즈 SaaS 도입 이상의 엄격한 보안 심사를 거쳐야 한다. 그것이 이번 사태가 남긴 가장 중요한 교훈이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고 자료&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://vercel.com/kb/bulletin/vercel-april-2026-security-incident&quot;&gt;Vercel April 2026 security incident | Vercel Knowledge Base&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bleepingcomputer.com/news/security/vercel-confirms-breach-as-hackers-claim-to-be-selling-stolen-data/&quot;&gt;Vercel confirms breach as hackers claim to be selling stolen data | BleepingComputer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://beincrypto.com/vercel-security-breach-internal-systems/&quot;&gt;Vercel Security Breach Raises Concerns for Crypto Projects | BeInCrypto&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://securitylabs.datadoghq.com/articles/litellm-compromised-pypi-teampcp-supply-chain-campaign/&quot;&gt;LiteLLM and Telnyx compromised on PyPI: Tracing the TeamPCP supply chain campaign | Datadog Security Labs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=47824463&quot;&gt;Vercel security incident | Hacker News&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web</category>
      <author>Aidengoldkr</author>
      <guid isPermaLink="true">https://aidengoldkr.tistory.com/26</guid>
      <comments>https://aidengoldkr.tistory.com/26#entry26comment</comments>
      <pubDate>Mon, 20 Apr 2026 07:37:55 +0900</pubDate>
    </item>
    <item>
      <title>Good Bye! BOJ! 백준 메모리얼 웹 배포 후기</title>
      <link>https://aidengoldkr.tistory.com/25</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;BOJ 서비스 종료 공지를 보고 뭔가를 남겨야겠다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리얼 방명록 사이트를 만들었다. &lt;a href=&quot;https://goodbye-boj.com&quot;&gt;goodbye-boj.com&lt;/a&gt;이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;만든 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;16년을 버텨온 플랫폼이 공지 하나로 마감됐다. 공지를 읽고 바로 개발을 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BOJ와 함께 성장한 사람들이 한마디씩 남길 공간이 있으면 좋겠다고 생각했다. 그리고 4월 28일 종료일까지 카운트다운이 흘러가는 사이트가 어울릴 것 같았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 기능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능은 단순하게 잡았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;카운트다운 타이머&lt;/b&gt; &amp;mdash; 2026-04-28 종료일까지 실시간 카운트다운&lt;/li&gt;
&lt;li&gt;&lt;b&gt;방명록&lt;/b&gt; &amp;mdash; Google 로그인 후 140자 이내 메시지 작성, 하루 1회 제한&lt;/li&gt;
&lt;li&gt;&lt;b&gt;좋아요 반응&lt;/b&gt; &amp;mdash; 다른 사람 글에 좋아요(❤️) 반응 전송&lt;/li&gt;
&lt;li&gt;&lt;b&gt;플로팅 배경&lt;/b&gt; &amp;mdash; 방명록 메시지들이 화면을 가로질러 흐르는 인터랙티브 배경&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로필&lt;/b&gt; &amp;mdash; BOJ 닉네임, 티어(Bronze ~ Master), 풀어본 문제 수, 주력 언어 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기술 스택&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;분류&lt;/th&gt;
&lt;th&gt;기술&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;프레임워크&lt;/td&gt;
&lt;td&gt;Next.js 14 (App Router) + TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인증&lt;/td&gt;
&lt;td&gt;NextAuth v5 (Google OAuth)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터베이스&lt;/td&gt;
&lt;td&gt;Supabase (PostgreSQL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스타일&lt;/td&gt;
&lt;td&gt;Tailwind CSS v4 + CSS Modules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;애니메이션&lt;/td&gt;
&lt;td&gt;Framer Motion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;클라이언트 데이터&lt;/td&gt;
&lt;td&gt;TanStack Query v5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 기술을 억지로 끼워 넣기보다, 이미 익숙한 조합을 빠르게 썼다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현하면서 신경 쓴 것들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Server / Client 역할 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js App Router의 기본 원칙을 최대한 따랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server Component에서는 데이터를 읽는다. Client Component에서는 상호작용을 처리한다. 모든 쓰기 작업(&lt;code&gt;createGuestbookEntry&lt;/code&gt;, &lt;code&gt;addFlower&lt;/code&gt;, 프로필 저장)은 &lt;code&gt;src/app/actions/&lt;/code&gt;의 &lt;b&gt;Server Action&lt;/b&gt;으로 분리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 API Route를 따로 만들 필요가 없었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Supabase 클라이언트를 읽기/쓰기로 나눈 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;src/lib/supabase/server.ts&lt;/code&gt;에 클라이언트를 두 개 만들었다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 읽기 &amp;mdash; Server Component에서 사용, anon key
export const createClient = () =&amp;gt; createServerClient(url, anonKey, ...)

// 쓰기 &amp;mdash; Server Action에서 사용, service role key (RLS 우회)
export const createServiceClient = () =&amp;gt; createServerClient(url, serviceRoleKey, ...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방명록 작성과 좋아요 반응은 인증된 사용자만 가능하다. RLS(Row Level Security)를 설정하기보다 Server Action에서 service role key로 직접 접근하는 방식을 선택했다. 뮤테이션이 서버에서만 일어나므로 키 노출 위험도 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 플로팅 배경 &amp;mdash; 20개 레인 설계&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;code&gt;FloatingMessages&lt;/code&gt; 컴포넌트가 메인 페이지 배경을 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방명록 메시지를 20개 레인으로 나눠 &lt;code&gt;setTimeout&lt;/code&gt; 틱 기반으로 하나씩 화면에 띄운다. CSS 애니메이션으로 오른쪽에서 왼쪽으로 흐르게 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 마퀴(marquee) 방식도 시도했다. &lt;code&gt;MessageMarquee&lt;/code&gt;라는 컴포넌트가 아직 코드에 남아 있는데, 최종적으로는 &lt;code&gt;FloatingMessages&lt;/code&gt;로 교체했다. 메시지가 동시에 우르르 흐르는 것보다 드문드문 등장하는 게 분위기에 맞았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 좋아요 &amp;mdash; localStorage 낙관적 업데이트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꽃 반응은 &lt;code&gt;GuestbookCard&lt;/code&gt;에서 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클릭 즉시 UI를 업데이트(낙관적 업데이트)하고, 서버에는 &lt;code&gt;addFlower&lt;/code&gt; / &lt;code&gt;removeFlower&lt;/code&gt; Server Action을 보낸다. 실패하면 롤백한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋아요 상태는 &lt;code&gt;localStorage&lt;/code&gt;의 &lt;code&gt;boj_liked_entries&lt;/code&gt; 키에 저장한다. 배열에 entry ID를 넣고 뺀다. 간단한 방식인데 재방문해도 상태가 유지된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. XSS 방어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방명록 내용을 저장하기 전에 &lt;code&gt;sanitizeHtml&lt;/code&gt;로 HTML 이스케이프를 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 입력이 그대로 데이터베이스에 들어가는 건 위험하다. 출력 시에만 이스케이프하는 방법도 있지만, 저장 시점에 한번 처리하는 게 더 명확하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. KST 기준 하루 1회 제한&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;createGuestbookEntry&lt;/code&gt;에서 당일 작성 횟수를 체크한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈: UTC 자정이 기준이면 한국 시간대에서 오전 9시에 카운트가 리셋된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책: KST 자정(&lt;code&gt;UTC+9&lt;/code&gt;)을 기준으로 오늘 작성 수를 집계해서 &lt;code&gt;user_limits&lt;/code&gt; 테이블의 &lt;code&gt;daily_limit&lt;/code&gt;과 비교한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 티어 배지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티어는 짧은 코드(&lt;code&gt;&quot;g4&quot;&lt;/code&gt;, &lt;code&gt;&quot;u&quot;&lt;/code&gt;, &lt;code&gt;&quot;m&quot;&lt;/code&gt;)로 저장한다. &lt;code&gt;TierBadge&lt;/code&gt; 컴포넌트가 &lt;code&gt;/public/tier/&amp;lt;code&amp;gt;.svg&lt;/code&gt;를 렌더링한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 &quot;Gold&quot; 같은 긴 형태로 저장된 값이 있을 수 있어서 &lt;code&gt;normalizeTier()&lt;/code&gt;로 정규화한다. &lt;code&gt;&quot;Gold&quot;&lt;/code&gt; &amp;rarr; &lt;code&gt;&quot;g3&quot;&lt;/code&gt; 같은 식이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 방명록 페이지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/guestbook&lt;/code&gt;은 &lt;b&gt;TanStack Query v5&lt;/b&gt;로 클라이언트 사이드 페이지네이션을 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;20개씩 로드하며 최신순 / 좋아요순 정렬을 지원한다. 정렬 기준을 바꿔도 페이지 전체가 새로고침되지 않는다. &lt;code&gt;GuestbookListClient&lt;/code&gt;에서 쿼리 파라미터만 바꾸면 TanStack Query가 알아서 데이터를 가져온다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배포&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel로 배포했다. Next.js 프로젝트라 설정이 거의 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경 변수 6개를 Vercel 대시보드에 등록했다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;AUTH_SECRET
AUTH_GOOGLE_ID
AUTH_GOOGLE_SECRET
NEXT_PUBLIC_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY
SUPABASE_SERVICE_ROLE_KEY&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google OAuth 콘솔에서 redirect URI를 프로덕션 도메인으로 추가하는 걸 빠뜨려서 로그인이 한 번 터졌다. 개발 URI(&lt;code&gt;localhost:3000&lt;/code&gt;)만 등록해뒀었다. 금방 발견해서 수정했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획부터 배포까지 짧게 만든 사이트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술적으로 새로운 시도가 많지는 않았다. 대신 빠르게 동작하는 걸 목표로 했고, 그 결과 실제로 사람들이 방명록을 남기고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BOJ 종료일인 4월 28일 이후에도 운영할지는 미지수다.&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>Web</category>
      <author>Aidengoldkr</author>
      <guid isPermaLink="true">https://aidengoldkr.tistory.com/25</guid>
      <comments>https://aidengoldkr.tistory.com/25#entry25comment</comments>
      <pubDate>Thu, 16 Apr 2026 11:55:42 +0900</pubDate>
    </item>
    <item>
      <title>백준 (BOJ) 서비스 종료</title>
      <link>https://aidengoldkr.tistory.com/24</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 4월 28일, BOJ가 문을 닫는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;16년의 끝&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백준 온라인 저지(BOJ)는 2010년 3월에 시작됐다. 최백준이 혼자 운영해온 알고리즘 문제 풀이 플랫폼으로, 국내 PS(Problem Solving) 커뮤니티의 사실상 중심지였다.&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;1786&quot; data-origin-height=&quot;944&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmXXOJ/dJMcah49QvN/Q8i5hi3qBm5GE9XZMjk3a1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmXXOJ/dJMcah49QvN/Q8i5hi3qBm5GE9XZMjk3a1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmXXOJ/dJMcah49QvN/Q8i5hi3qBm5GE9XZMjk3a1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmXXOJ%2FdJMcah49QvN%2FQ8i5hi3qBm5GE9XZMjk3a1%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;1786&quot; height=&quot;944&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;944&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;여러 상황의 변화로 인해 부득이하게 서비스를 종료하게 되었습니다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구체적인 이유는 밝히지 않았다. 하지만 16년을 혼자 끌어온 무게가 공지 한 줄에 담겨 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사라지는 것들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 종료 시점에는 &lt;b&gt;문제, 제출 기록, 대회 정보&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;4월 28일까지는 정상 이용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 남은 시간이 있다면, 풀었던 문제들을 백업해두는 편이 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;solved.ac도 멈춘다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BOJ 종료 공지가 올라온 후, solved.ac도 입장을 냈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;solved.ac는 BOJ 위에서 난이도 측정, 티어 시스템, 랜덤 마라톤 등을 제공해온 서비스다. BOJ 없이는 존재할 수 없는 구조였다.&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;BOJ와의 연동은 4월 28일을 기해 종료된다.&lt;/li&gt;
&lt;li&gt;문제 난이도 데이터 등은 28일 이후에도 어떤 방식으로든 유지할 예정이다.&lt;/li&gt;
&lt;li&gt;지금 이 시점부터 문제 풀이&amp;middot;기여로 얻는 별조각이 &lt;b&gt;10배&lt;/b&gt;로 늘어난다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별조각 10배라는 숫자가 묘하게 읽힌다. 유저들이 마지막으로 남길 수 있는 기여를 최대한 끌어내겠다는 의도처럼 보이기도 하고, 16년치 데이터를 붙잡아두려는 마지막 시도처럼 보이기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구체적인 이후 계획은 &quot;후속 공지로 안내&quot;한다고만 했다. 아직 아무것도 결정되지 않았다는 뜻이기도 하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;555&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J3j8Z/dJMcagd6TuJ/eIiXTnlGytFM1dinOABlZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J3j8Z/dJMcagd6TuJ/eIiXTnlGytFM1dinOABlZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J3j8Z/dJMcagd6TuJ/eIiXTnlGytFM1dinOABlZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ3j8Z%2FdJMcagd6TuJ%2FeIiXTnlGytFM1dinOABlZ1%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;1478&quot; height=&quot;555&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;555&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;남아있을 가능성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전히 사라지는 건 아닐 수도 있다. 최백준은 공지에 이렇게 남겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;완전히 사라지기보다는, 문제만이라도 다시 볼 수 있는 형태로 돌아올 수 있도록 고민하고 있습니다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기 전용 아카이브 형태로의 복귀를 염두에 두고 있다는 말이다. 향후 상황이 달라지면 서비스를 재개할 가능성도 언급했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능성으로 남겨둔 것이지 약속은 아니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초등학생 때 가입해서 취업까지 이어진 사람들이 있는 플랫폼이다. 나 역시 알고리즘을 처음 배우던 시절부터 BOJ와 함께했고, 최근 합격한 소프트웨어 마에스트로 과정을 대비하는데도 정말 유용하게 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최백준이 공지 말미에 남긴 문장이 마음에 걸린다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;언젠가는 그분들이 자녀와 함께 이 사이트를 사용하는 모습을 보는 것이 작은 꿈이었습니다.&quot;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 꿈은 이루어지지 못했지만, 16년 동안 나를 포함한 수많은 사람의 코딩 역사를 함께 기록한 건 사실이다. 나도 PS를 즐겨한편은 아니었지만, 가끔 심심풀이용으로 몇번 풀기도하고, 작년에는 스트릭 최장 찍어보겠다고 42스택까지 찍어본적이 있다. 정겨운 추억있던 서비스가 이렇게 하루아침에 사라지는 것이 아쉽기도 하지만, 이러한 것도 웹 서비스 개발자의 소양이라고 생각한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coS7rH/dJMcaiXeWjv/KMGeggzz3awLubfMfKZQLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coS7rH/dJMcaiXeWjv/KMGeggzz3awLubfMfKZQLK/img.png&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;1157&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.3781%; margin-right: 10px;&quot; data-widthpercent=&quot;32.76&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coS7rH/dJMcaiXeWjv/KMGeggzz3awLubfMfKZQLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoS7rH%2FdJMcaiXeWjv%2FKMGeggzz3awLubfMfKZQLK%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;1214&quot; height=&quot;1157&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mDDAb/dJMcabw4zqc/Jvg3jBWMy05cDq7a6zAx80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mDDAb/dJMcabw4zqc/Jvg3jBWMy05cDq7a6zAx80/img.png&quot; data-origin-width=&quot;1247&quot; data-origin-height=&quot;579&quot; data-is-animation=&quot;false&quot; style=&quot;width: 66.4591%;&quot; data-widthpercent=&quot;67.24&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mDDAb/dJMcabw4zqc/Jvg3jBWMy05cDq7a6zAx80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmDDAb%2FdJMcabw4zqc%2FJvg3jBWMy05cDq7a6zAx80%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;1247&quot; height=&quot;579&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDYcOB/dJMcaaEYDJn/RlpaVIdNHiZHJOdVdRZhk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDYcOB/dJMcaaEYDJn/RlpaVIdNHiZHJOdVdRZhk0/img.png&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;1042&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.7081%; margin-right: 10px;&quot; data-widthpercent=&quot;50.29&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDYcOB/dJMcaaEYDJn/RlpaVIdNHiZHJOdVdRZhk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDYcOB%2FdJMcaaEYDJn%2FRlpaVIdNHiZHJOdVdRZhk0%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;1414&quot; height=&quot;1042&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDjqiD/dJMcaf0xfuj/4V1qUvoGmzllMSjPBkslhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDjqiD/dJMcaf0xfuj/4V1qUvoGmzllMSjPBkslhK/img.png&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;1017&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.1291%;&quot; data-widthpercent=&quot;49.71&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDjqiD/dJMcaf0xfuj/4V1qUvoGmzllMSjPBkslhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDjqiD%2FdJMcaf0xfuj%2F4V1qUvoGmzllMSjPBkslhK%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;1364&quot; height=&quot;1017&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;소마 코테 한달전부터 풀이 수가 급증한 것을 알 수 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Good Bye! BOJ!&lt;/p&gt;</description>
      <category>알고리즘/BOJ</category>
      <category>Good_bye_BOJ</category>
      <author>Aidengoldkr</author>
      <guid isPermaLink="true">https://aidengoldkr.tistory.com/24</guid>
      <comments>https://aidengoldkr.tistory.com/24#entry24comment</comments>
      <pubDate>Wed, 15 Apr 2026 18:12:32 +0900</pubDate>
    </item>
    <item>
      <title>[풀스택 100시간 과정] #4 - 세션, 쿠키, API, 페이징</title>
      <link>https://aidengoldkr.tistory.com/23</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 수업에서는 로그인 구현의 핵심인 &lt;b&gt;쿠키와 세션&lt;/b&gt;, 서버와 클라이언트가 데이터를 주고받는 규칙인 &lt;b&gt;REST API&lt;/b&gt;, 그리고 대용량 데이터를 다루는 &lt;b&gt;페이징&lt;/b&gt;까지 한 번에 다뤘다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;쿠키 (Cookie)&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;주로 이런 상황에 쓰인다: &quot;오늘 하루 보지 않기&quot;, &quot;아이디 저장&quot;, 쇼핑몰 장바구니. HTTP는 기본적으로 &lt;b&gt;Stateless&lt;/b&gt;(상태 없음), &lt;b&gt;Connectionless&lt;/b&gt;(연결 없음) 특성을 가지는데, 쿠키가 그 공백을 메운다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 쿠키 저장 (만료일 포함)
const setUserInfoCookie = (username, expirationDays) =&amp;gt; {
    const date = new Date();
    date.setTime(date.getTime() + expirationDays * 24 * 60 * 60 * 1000);
    const expires = `expires=${date.toUTCString()}`;
    document.cookie = `username=${username};${expires};path=/`;
};

// 쿠키 읽기
const getUserInfoFromCookie = () =&amp;gt; {
    const cookieName = 'username=';
    const decodedCookie = decodeURIComponent(document.cookie);
    const cookieArray = decodedCookie.split(';');
    for (let cookie of cookieArray) {
        while (cookie.charAt(0) === ' ') cookie = cookie.substring(1);
        if (cookie.indexOf(cookieName) === 0) {
            return cookie.substring(cookieName.length);
        }
    }
    return '';
};

// 로그아웃 (쿠키 만료 처리)
const logoutUser = () =&amp;gt; {
    document.cookie = 'username=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;';
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그아웃은 쿠키를 삭제하는 게 아니라 &lt;b&gt;만료일을 과거로 설정&lt;/b&gt;해서 브라우저가 스스로 버리게 한다. 확인은 브라우저 개발자 도구 &amp;rarr; 응용 프로그램 &amp;rarr; 쿠키 탭에서 할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세션 (Session)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세션&lt;/b&gt;은 쿠키가 클라이언트에 저장하는 것과 달리, 민감한 정보를 &lt;b&gt;서버 측에서 관리&lt;/b&gt;한다. 로그인 상태 유지가 대표적인 사용 사례다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;개념&lt;/th&gt;
&lt;th&gt;저장 위치&lt;/th&gt;
&lt;th&gt;적합한 정보&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;쿠키&lt;/td&gt;
&lt;td&gt;브라우저 (클라이언트)&lt;/td&gt;
&lt;td&gt;공개돼도 괜찮은 정보&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;세션&lt;/td&gt;
&lt;td&gt;서버&lt;/td&gt;
&lt;td&gt;외부에 공개되면 안 되는 정보&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 이렇다. 서버가 로그인 시 고유한 &lt;b&gt;세션 ID&lt;/b&gt;를 생성해 서버에 저장하고, 그 ID만 쿠키로 브라우저에 내려준다. 이후 요청마다 브라우저가 세션 ID를 쿠키에 담아 보내고, 서버는 그걸로 사용자를 식별한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션을 서버 어디에 저장하느냐에 따라 3가지로 나뉜다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;저장 방식&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;In Memory&lt;/td&gt;
&lt;td&gt;코드 변수에 저장. 서버 재시작 시 전부 날아감&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File Storage&lt;/td&gt;
&lt;td&gt;텍스트 파일로 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;DB에 저장. 가장 안정적&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API &amp;amp; REST API&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;API&lt;/b&gt;는 두 소프트웨어가 통신할 수 있게 해주는 창구다. 웹에서는 주로 클라이언트가 서버에 데이터를 요청하거나 전달하는 엔드포인트 URL을 뜻한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;REST API&lt;/b&gt;는 그 창구를 만드는 규칙이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;규칙&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Resource 중심 URL&lt;/td&gt;
&lt;td&gt;URL은 자원(명사)을 표현&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/users&lt;/code&gt;, &lt;code&gt;/posts/1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 메서드로 행위 표현&lt;/td&gt;
&lt;td&gt;동사는 URL에 넣지 않음&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/getUser&lt;/code&gt; ❌ &amp;rarr; &lt;code&gt;/users&lt;/code&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 상태코드 사용&lt;/td&gt;
&lt;td&gt;결과를 코드로 명확히 전달&lt;/td&gt;
&lt;td&gt;&lt;code&gt;200&lt;/code&gt;, &lt;code&gt;201&lt;/code&gt;, &lt;code&gt;404&lt;/code&gt;, &lt;code&gt;500&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 메서드와 CRUD 매핑.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;메서드&lt;/th&gt;
&lt;th&gt;CRUD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;Read (조회)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;Create (생성)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PUT / PATCH&lt;/td&gt;
&lt;td&gt;Update (수정)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;td&gt;Delete (삭제)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;페이징 (Paging)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 데이터를 한 번에 내려주면 서버도 클라이언트도 다 죽는다. &lt;b&gt;페이징&lt;/b&gt;은 필요한 만큼만 잘라서 보내는 기법이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;사용 사례&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Offset 기반&lt;/td&gt;
&lt;td&gt;&lt;code&gt;page&lt;/code&gt;와 &lt;code&gt;limit&lt;/code&gt;으로 슬라이싱&lt;/td&gt;
&lt;td&gt;게시판, 검색 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor 기반&lt;/td&gt;
&lt;td&gt;마지막으로 본 항목 기준으로 다음 요청&lt;/td&gt;
&lt;td&gt;인스타그램&amp;middot;유튜브 무한 스크롤&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Offset 방식은 구현이 단순하지만 데이터가 많아질수록 뒤 페이지 조회가 느려진다. Cursor 방식은 구현이 복잡하지만 성능이 일정하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마무리: 세션과 쿠키는 &quot;어디에 저장하느냐&quot;의 차이고, REST API는 URL 설계 규칙이다. 페이징은 성능 문제가 생길 때 비로소 체감하는 개념인데, 미리 알고 설계하면 나중에 훨씬 편하다.&lt;/p&gt;</description>
      <category>Web</category>
      <author>Aidengoldkr</author>
      <guid isPermaLink="true">https://aidengoldkr.tistory.com/23</guid>
      <comments>https://aidengoldkr.tistory.com/23#entry23comment</comments>
      <pubDate>Mon, 13 Apr 2026 23:32:26 +0900</pubDate>
    </item>
    <item>
      <title>[풀스택 100시간 과정] #3 - 서버와 HTTP</title>
      <link>https://aidengoldkr.tistory.com/22</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 수업에서는 웹 개발의 가장 기본적인 개념인 &lt;b&gt;클라이언트-서버 구조&lt;/b&gt;와, 둘 사이에서 데이터를 주고받는 방식인 &lt;b&gt;HTTP&lt;/b&gt;를 다뤘다.&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;클라이언트와 서버&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;개념&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;클라이언트&lt;/td&gt;
&lt;td&gt;서버에 요청을 보내는 프로그램 또는 장치 (ex. 웹 브라우저)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버&lt;/td&gt;
&lt;td&gt;요청을 받아 처리하고 응답을 돌려주는 컴퓨터 시스템&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 &lt;b&gt;장군&lt;/b&gt;, 서버는 &lt;b&gt;성&lt;/b&gt;, 그 안의 왕이 &lt;b&gt;웹 서버&lt;/b&gt;다. 장군이 성에 물 100L 보급을 요청하면, 왕이 허가하고 처리하는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 비유를 전체 개념에 매핑하면 이렇다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;비유&lt;/th&gt;
&lt;th&gt;실제 개념&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;장군&lt;/td&gt;
&lt;td&gt;클라이언트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;성&lt;/td&gt;
&lt;td&gt;서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;왕&lt;/td&gt;
&lt;td&gt;웹 서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;좌표&lt;/td&gt;
&lt;td&gt;IP 주소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;도로명주소&lt;/td&gt;
&lt;td&gt;도메인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;성문&lt;/td&gt;
&lt;td&gt;포트 (0~65535)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;성벽&lt;/td&gt;
&lt;td&gt;방화벽&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;마패&lt;/td&gt;
&lt;td&gt;인증 (Authentication)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;접근 권한&lt;/td&gt;
&lt;td&gt;인가 (Authorization)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;요청서&lt;/td&gt;
&lt;td&gt;HTTP Message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;창고&lt;/td&gt;
&lt;td&gt;데이터베이스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;약속한 요청 규칙&lt;/td&gt;
&lt;td&gt;프로토콜&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인증(Authentication)&lt;/b&gt; 과 &lt;b&gt;인가(Authorization)&lt;/b&gt; 는 헷갈리기 쉬운 개념이다. 인증은 &quot;이 사람이 누구인지&quot; 확인하는 것이고, 인가는 &quot;이 사람이 어디에 접근할 수 있는지&quot; 권한을 부여하는 것이다. 마패(신분증)로 신분을 확인하는 게 인증, 확인 후 어느 구역까지 들어올 수 있는지 결정하는 게 인가다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP Message&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트와 서버가 실제로 주고받는 데이터 단위가 &lt;b&gt;HTTP Message&lt;/b&gt;다. 구조는 4가지로 나뉜다.&lt;/p&gt;
&lt;table style=&quot;height: 106px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;th style=&quot;height: 22px;&quot;&gt;구성 요소&lt;/th&gt;
&lt;th style=&quot;height: 22px;&quot;&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;시작줄 (Start Line)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;요청/응답의 상태를 나타내는 첫 번째 줄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;헤더 (Headers)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;메시지 정보를 담은 키-값 집합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;빈 줄 (Empty Line)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;헤더 종료, 본문 시작을 알리는 구분선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;본문 (Body)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;실제 데이터 (HTML, JSON 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헤더 중 &lt;code&gt;Content-Length&lt;/code&gt;가 특히 눈에 들어왔다. 서버가 요청을 처리하기 전에 본문 크기를 미리 파악해서, 너무 크거나 이상한 요청은 거절할 수 있도록 하는 역할이다. 이게 결국 Vercel의 4.5MB 제한 같은 서버 단 페이로드 제한의 근거이기도 하다.&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;HTTP Status Code&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 요청 처리 결과를 클라이언트에 알리는 3자리 숫자 코드다.&lt;/p&gt;
&lt;table style=&quot;height: 127px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;th style=&quot;height: 22px;&quot;&gt;번호대&lt;/th&gt;
&lt;th style=&quot;height: 22px;&quot;&gt;분류&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;1xx&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;정보&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;2xx&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;성공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;3xx&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;리다이렉션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;4xx&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;클라이언트 오류&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;5xx&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;서버 오류&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 보게 될 코드들.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;요청 성공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;201&lt;/td&gt;
&lt;td&gt;성공 + 새 리소스 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;204&lt;/td&gt;
&lt;td&gt;성공, 반환할 내용 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;잘못된 요청 (파라미터 누락 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;td&gt;인증 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;인가 없음 (권한 부족)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;리소스 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;429&lt;/td&gt;
&lt;td&gt;요청 과다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;서버 내부 오류&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;503&lt;/td&gt;
&lt;td&gt;서버 준비 안 됨&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;401과 403도 인증/인가처럼 헷갈리는 쌍이다. 401은 &quot;누구세요?&quot; &amp;mdash; 로그인하지 않은 상태다. 403은 &quot;알고는 있는데, 넌 여기 못 들어와&quot; &amp;mdash; 권한이 없는 상태다.&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;Express.js&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 &lt;b&gt;Express.js&lt;/b&gt;를 소개했다. Node.js 기반의 웹 서버 프레임워크로, 앞서 설명한 왕(웹 서버) 역할을 직접 구현할 수 있게 해주는 도구다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install express&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import express from 'express'

const app = express()
const PORT = 3000

app.get('/', (req, res) =&amp;gt; {
  res.send('서버 동작 중') //기본 라우트
})

app.listen(PORT, () =&amp;gt; {
  console.log(`포트 ${PORT}에서 실행 중`)
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js만으로도 서버를 만들 수 있지만, 처음부터 끝까지 직접 짜면 손이 너무 많이 간다. Express는 라우팅, 미들웨어 등 반복 작업을 추상화해서 최소한의 코드로 서버를 돌릴 수 있게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이어 구조로 보면: &lt;b&gt;운영체제 &amp;rarr; Node.js &amp;rarr; Express.js&lt;/b&gt; 순이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마무리: 클라이언트-서버, HTTP, 상태 코드는 웹 개발의 언어다. 개발하면서 이미 쓰고 있던 개념들이지만, 용어와 구조로 정리해두니 코드를 읽을 때 더 명확하게 보인다.&lt;/p&gt;</description>
      <category>Web</category>
      <author>Aidengoldkr</author>
      <guid isPermaLink="true">https://aidengoldkr.tistory.com/22</guid>
      <comments>https://aidengoldkr.tistory.com/22#entry22comment</comments>
      <pubDate>Mon, 13 Apr 2026 19:58:24 +0900</pubDate>
    </item>
    <item>
      <title>AI&amp;middot;SW 마에스트로 서울센터 17기 최종 합격 후기 - 고등학생 최연소</title>
      <link>https://aidengoldkr.tistory.com/21</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요, 오늘은 좀 늦었지만, AI&amp;middot;SW 마에스트로 (이하, 소마) 합격 후기를 적어보려합니다.&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;1.&amp;nbsp; AI&amp;middot;SW 마에스트로는 무엇인가?&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;2010년부터 시작한 최우수 SW 인재를 발굴하여, 체계적이고 파격적인 지원을 통해 SW 산업 발전에 기여하기 위해 기획된 정부 지원 사업. 실제로 국내 IT 대외활동 중 최고 수준의 지원을 받는다.&lt;br /&gt;&lt;br /&gt;소프트웨어 마에스트로는 과학기술정보통신부와 정보통신기획평가원에서 주관하고, 한국정보산업연합회에서 운영하는 과정으로, 국내 소프트웨어 전문가들을 만들자는 취지하에 운영되고 있다. 소프트웨어 산업 분야별 전문가를 멘토로 지정하여 도제식 교육 방식을 통해 프로젝트를 수행하면서 지도받는 시스템이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외적으로, 여기서 만날 수 있는 멘토님들과 지원금등, 물적, 인적 지원을 받을 수 있는 국가사업입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 지원동기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년 학교 SW 주제탐구 발표 이후 담당 선생님이 소마를 소개해줬다. 또래끼리 하는 팀프로젝트에서 느끼던 한계가 있었다 &amp;mdash; 실무 피드백이 없고, 프로젝트가 끝나면 그냥 끝이었다. 장기적으로 실무자들과 같이 뭔가를 만들어가는 경험이 필요하다고 생각했고, 소마가 그 구조에 딱 맞았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거기다 2학년 전공배정에서 게임과로 배정되는 황당한 상황을 겪으면서, 오히려 이걸로 뭔가 증명해야겠다는 마음이 강하게 생겼다. 솔직히 그게 의지 불태우는 데 꽤 도움됐다.&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;3. 준비과정&lt;/h4&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코딩테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략 한 달 준비했다. 기존 티어가 실버 2 정도였고, 구현 / DFS&amp;middot;BFS / DP 위주로 갈았다.&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;1차&lt;/b&gt;: 2&amp;middot;3&amp;middot;4번 3솔&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2차&lt;/b&gt;: 2&amp;middot;3번 2솔&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL은 프로그래머스 고득점 Kit으로 대비했는데 1&amp;middot;2차 모두 손도 못 댔다. 코테가 합격의 핵심 변수는 아닌 것 같다 &amp;mdash; 다른 요소로 상쇄가 가능하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;포트폴리오&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 프로젝트 나열이 아니라 &lt;b&gt;8년 커리어의 맥락 위에 소마를 위치&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;&lt;b&gt;ToDit (구 Actonix)&lt;/b&gt; &amp;mdash; 1월부터 직접 만들던 서비스&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Safe Ground&lt;/b&gt; &amp;mdash; 작년 추석 연휴에 Flutter 독학해서 만든 생활안전앱&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트폴리오 흐름은 이렇게 잡았다:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 8년 커리어 로드맵 제시 (1분) &amp;rarr; ToDit 프로젝트 설명 (1분 30초) &amp;rarr; 소마에서 하고 싶은 것과 포부 (30초)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3분 PR이 끝난 뒤 앉아서 면접으로 이어지는 구조라, 도입부에서 흐름을 확실히 잡는 게 중요하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1834&quot; data-origin-height=&quot;789&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lGSFI/dJMcacpccew/KzsXz1bRMFYDeEMVIPmJMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lGSFI/dJMcacpccew/KzsXz1bRMFYDeEMVIPmJMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lGSFI/dJMcacpccew/KzsXz1bRMFYDeEMVIPmJMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlGSFI%2FdJMcacpccew%2FKzsXz1bRMFYDeEMVIPmJMK%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;562&quot; height=&quot;242&quot; data-origin-width=&quot;1834&quot; data-origin-height=&quot;789&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Frame 1.png&quot; data-origin-width=&quot;1966&quot; data-origin-height=&quot;864&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WJzQz/dJMcadImecz/H0dUgIXkxw2UQQK9nxfa31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WJzQz/dJMcadImecz/H0dUgIXkxw2UQQK9nxfa31/img.png&quot; data-alt=&quot;포트폴리오 페이지에 실제로 넣었던 로드맵&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WJzQz/dJMcadImecz/H0dUgIXkxw2UQQK9nxfa31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWJzQz%2FdJMcadImecz%2FH0dUgIXkxw2UQQK9nxfa31%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;1966&quot; height=&quot;864&quot; data-filename=&quot;Frame 1.png&quot; data-origin-width=&quot;1966&quot; data-origin-height=&quot;864&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;포트폴리오 페이지에 실제로 넣었던 로드맵&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 면접 당일&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목요일 오후 3시 면접이었다. 마침 학교 설명회 날이라 별도 조퇴 처리 없이 1시에 퇴교하고 마포 포스트타워로 이동했다. 복장은 흰 셔츠에 학교 학잠 &amp;mdash; 굳이 정장까지는 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 분과는 결석자 2명이 있어서 45분으로 진행됐다. 3분 PR &amp;rarr; 착석 후 면접관 주도 질의 순서.&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;b&gt;Q. 고등학생인데 학교랑 어떻게 병행할 거냐?&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;Q. 팀 리더라면 어떤 팀원 구성을 원하냐?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 포지션이 PM + FE라 BE 2명을 구성하고 싶다. 한 명은 서버&amp;middot;FE 브릿지 역할, 나머지 한 명은 DevOps&amp;middot;보안 담당으로 포지셔닝할 생각이다.&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;Q. 제출 프로젝트가 앱이 더 접근성 좋을 것 같은데 왜 웹으로 했나?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVP 단계라 개발 속도 우선이었다. 미성년자라 앱스토어 직접 출시도 어렵다. 정식 버전에서는 앱으로 전환 예정.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에는 인성 질문과 프로젝트 관련 추가 질문들이었다. 분과마다 질문 방향이나 깊이가 다른 것 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 그 이후&lt;/h4&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;Screenshot_20260412_161734_Messages.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1371&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC4Ktx/dJMcaaLGzrx/KCiSi883WHd9DC48pw1ih0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC4Ktx/dJMcaaLGzrx/KCiSi883WHd9DC48pw1ih0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC4Ktx/dJMcaaLGzrx/KCiSi883WHd9DC48pw1ih0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC4Ktx%2FdJMcaaLGzrx%2FKCiSi883WHd9DC48pw1ih0%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;557&quot; height=&quot;707&quot; data-filename=&quot;Screenshot_20260412_161734_Messages.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1371&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행이도, 제 17기 AI&amp;middot;SW 마에스트로 과정에 선발되었다. 예비가 10번대 까지 돌았던것으로 아는데, 처음부터 최종합격이라니 감사할 나름이다. 또한 나 제외하고 고등학생이 한 명은 더 있을 줄 알았는데, 17기 300명 중 고등학생은 내가 유일하다.&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;이 글은 OT 이후 4월 12일 서울센터 12층에서 작성하고 있다. 궁금한 점을 댓글로 달아주면, 내가 아는 선에서 최대한 답변해보도록 하겠다.&lt;/p&gt;</description>
      <category>AI&amp;middot;SW 마에스트로</category>
      <author>Aidengoldkr</author>
      <guid isPermaLink="true">https://aidengoldkr.tistory.com/21</guid>
      <comments>https://aidengoldkr.tistory.com/21#entry21comment</comments>
      <pubDate>Sun, 12 Apr 2026 16:22:38 +0900</pubDate>
    </item>
    <item>
      <title>[풀스택 100시간 과정] #2 - JS 입출력, 조건문, 반복문</title>
      <link>https://aidengoldkr.tistory.com/20</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript는 브라우저에서 웹을 동적으로 구현하기 위해 만들어진 언어이다. 이를 웹 브라우저가 아닌 환경에서 실행하게 해주는 런타임 환경이 Node.js이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 말했다싶이, JS는 원래 웹 브라우저에서 돌아가는 언어이기 때문에, 입력을 받으려면 npm에서 readline-sync을 다운받아야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1775087413218&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install readline-sync&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;pre id=&quot;code_1775087438599&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import readlineSyncModule from 'readline-sync'

const name = readlineSyncModule.question('이름 입력 : ')
console.log('입력받은 이름 : ' + name)

const age = parseInt(readlineSyncModule.question('이름 입력 : '), 10);
console.log('입력받은 나이 : ' + age)&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;readline-sync에서 readlineSyncModule을 import하고, question 메소드를 통해 input을 받아 name과 age 변수에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 console.log를 통해 출력이 가능하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건문은 if-else로 C나 Python나 JS나 똑같다.&lt;/p&gt;
&lt;pre id=&quot;code_1775088094407&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let a = 5

if (a &amp;gt; 3){
	console.log(&quot;yes&quot;)
}
else {
	console.log(&quot;no&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복문은 C와 거의 동일하며, 초기화, 조건문, 증감자로 구성되어 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1775088195836&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import readlineSyncModule from 'readline-sync';

const size = parseInt(readlineSyncModule.question('size : '));

for (let i = -size; i &amp;lt;= size; i++) {
    let result = &quot;&quot;;
    const spaces = Math.abs(i);
    for (let s = 0; s &amp;lt; spaces; s++) {
        result += &quot; &quot;;
    }

    const stars = (size - Math.abs(i)) * 2 + 1;
    for (let j = 0; j &amp;lt; stars; j++) {
        result += &quot;*&quot;;
    }

    console.log(result);
}&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;위 코드는 간단한 마름모 별 찍기 예제 코드로, 당시 귀찮아서, abs를 활용한 절대값 로직으로 처리했다&lt;/p&gt;</description>
      <category>Web/풀스택 100시간 과정</category>
      <author>Aidengoldkr</author>
      <guid isPermaLink="true">https://aidengoldkr.tistory.com/20</guid>
      <comments>https://aidengoldkr.tistory.com/20#entry20comment</comments>
      <pubDate>Thu, 2 Apr 2026 09:03:49 +0900</pubDate>
    </item>
    <item>
      <title>[풀스택 100시간 과정] #1 - 들어가며 &amp;amp; JS 변수선언</title>
      <link>https://aidengoldkr.tistory.com/19</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘부터 차세대 SW혁신인재육성 아카데미에서 진행하는 AI 풀스택 웹마스터 과정 (100시간)에 참여하게되었다. 「AI 활용 풀스택 웹마스터 과정」은 학생들이 실무 중심의 기술 역량을 기르고 진로 멘토링을 통해 SW 전문 역량을 강화할 수 있도록 운영되는 교육 프로그램으로, 전에 독학으로 Next.js, React등을 배우고 ToDit 까지 운영해본 경험이 있지만, 생기부도 채울 겸 전문적으로, 풀스택 기술을 익히고자 신청했다. 7월에는 강남구청에서 해당 사업을 진행하는, 대덕디자인고, 단대소고, 수도공고, 로봇고 4개학교가 모여, 프로젝트를 발표하고 해커톤을 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫날은 오리엔테이션과 아이스브레이킹이 주로 이루어졌다. 프로그램의 전반적인 커리큘럼 소개와, 강사님 소개가 있었다. 쉬는 시간을 이용하여, 내가 어떤 사람이고, 지금 운영하고 있는 ToDit에 대한 피드백도 강사님께 받을 수 있었다.&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일차의 메인 이벤트인지라, 수업은 1시간 정도 진행했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;JavaScript 에서의 변수 및 상수 선언&lt;/h4&gt;
&lt;pre id=&quot;code_1774909990438&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var aiden = 0;
var aiden = 'kwk'; // 중복 선언 됨 (?)

let kwk = 0;
let kwk = 'aiden'  //X 중복 선언 안됨

const bmw = 'm3'; // 상수 선언&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;JavaScript의 자료형&lt;/h4&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot; colspan=&quot;2&quot;&gt;자료형&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot; rowspan=&quot;5&quot;&gt;기본형&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;number(숫자)&lt;/td&gt;
&lt;td&gt;따옴표 없이 표기한 숫자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;string(문자열)&lt;/td&gt;
&lt;td&gt;작은따옴표(')나 큰따옴표(&quot;)로 묶어 나타낸 문자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;boolean(논리형)&lt;/td&gt;
&lt;td&gt;참(true)과 거짓(false)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;undefined&lt;/td&gt;
&lt;td&gt;자료형을 지정하지 않았을 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;null&lt;/td&gt;
&lt;td&gt;값이 유효하지 않을 때의 유형입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot; rowspan=&quot;2&quot;&gt;복합형&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;array(배열)&lt;/td&gt;
&lt;td&gt;하나의 변수에 여러 값을 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;object(객체)&lt;/td&gt;
&lt;td&gt;함수와 속성이 함께 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외 내용은 너무 기초적인 것이라, 생략&lt;/p&gt;</description>
      <category>Web/풀스택 100시간 과정</category>
      <author>Aidengoldkr</author>
      <guid isPermaLink="true">https://aidengoldkr.tistory.com/19</guid>
      <comments>https://aidengoldkr.tistory.com/19#entry19comment</comments>
      <pubDate>Tue, 31 Mar 2026 07:35:32 +0900</pubDate>
    </item>
    <item>
      <title>Actonix에서 ToDit으로 &amp;mdash; 포지셔닝 실패를 인정하고 피벗하기 까지</title>
      <link>https://aidengoldkr.tistory.com/18</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;OG.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vdc90/dJMcabKhyNT/EHlF7TOOgNKx7WeeLGekE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vdc90/dJMcabKhyNT/EHlF7TOOgNKx7WeeLGekE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vdc90/dJMcabKhyNT/EHlF7TOOgNKx7WeeLGekE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvdc90%2FdJMcabKhyNT%2FEHlF7TOOgNKx7WeeLGekE0%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;1200&quot; height=&quot;630&quot; data-filename=&quot;OG.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시작: inPaser라는 아이디어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 이걸 만들겠다고 생각한 건 단순한 질문에서였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;문서를 올리면 그냥 할 일 목록이 나오면 안 되나?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회의록을 읽고 태스크를 뽑고, 메모를 정리하고 일정을 잡는 그 반복 작업이 너무 비효율적으로 느껴졌다. 요약이 아니라 실행. 읽는 게 아니라 움직이는 것. 그 차이가 핵심이라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 나온 게 &lt;b&gt;inPaser&lt;/b&gt;였다. 문서 &amp;rarr; 파싱 &amp;rarr; 외부 시스템 실행까지 이어지는 인프라 엔진 개념. 근데 솔직히 말하면 그건 너무 큰 그림이었다. 고등학생 혼자서 처음부터 풀 엔진을 만들 수는 없었다. 그래서 MVP가 필요했다. inPaser의 첫 번째 레이어를 증명할 서비스. 그게 &lt;b&gt;Actonix&lt;/b&gt;였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Actonix: 맞는 제품, 틀린 포지셔닝&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Actonix는 이미지나 문서를 올리면 AI가 의도를 추론해서 할 일 목록을 생성해주는 서비스였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술적으로는 꽤 잘 동작했다. Google Cloud Vision OCR과 OpenAI를 분리해서 비용을 최적화했고, AI 응답에 JSON 스키마 검증을 걸어서 파싱 실패 시 과금이 안 되게 막았다. 학교 안내문에 &quot;3/15&quot;처럼 연도가 빠진 날짜가 오면 오늘 기준으로 6개월 초과 미래면 전년도로 추론하는 로직도 있었다. Google Calendar 연동 버튼도 UI에 올라가 있었다 &amp;mdash; 실제로는 클릭해도 아무 일이 없었지만.&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;베타 유저도 약 20명 모았다. 근데 문제가 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;유저들이 Actonix를 &quot;문서 요약 도구&quot;로 인식하고 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이거 AI 요약 서비스죠?&quot;라는 질문이 반복됐다. '할 일 생성'이 아니라 '요약'으로 읽히고 있었다. 즉, 제품이 하는 일과 유저가 인식하는 일 사이에 갭이 생긴 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인을 뜯어봤다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 &quot;설명을 잘 못했다&quot;는 말로 끝낼 수가 없었다. 구체적으로 뭐가 문제였는지 분석했다.&lt;/p&gt;
&lt;table style=&quot;height: 106px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;th style=&quot;height: 22px;&quot;&gt;실패 원인&lt;/th&gt;
&lt;th style=&quot;height: 22px;&quot;&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;정보 위계 역전&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;문서 메타데이터가 결과 최상단. 가장 중요한 Todo가 아래 있었음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;추상적 카피&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&quot;의도 기반 실행&quot;은 유저한테 와닿지 않는 표현&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Calendar 버튼 미작동&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;실행 자동화를 주장하면서 핵심 버튼이 동작 안 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;브랜드 이름 문제&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&quot;Actonix&quot; &amp;mdash; 뭘 하는 서비스인지 이름에서 안 읽힘&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 하나였다. &lt;b&gt;제품이 실제로 하는 것보다 더 많은 걸 주장하고 있었고, 동시에 그 주장을 뒷받침하는 실행이 빠져 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포지셔닝 실패의 본질은 항상 이거다. 제품의 실제 능력과 외부에 드러나는 메시지 사이의 간극.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;피벗: Todit&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리브랜딩을 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 단순히 이름만 바꾸는 게 아니었다. 포지셔닝 자체를 다시 설계했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 메시지 변경:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Before: &quot;문서에서 실행 계획을&quot;&lt;/li&gt;
&lt;li&gt;After: &quot;사진 및 문서만 올리면 바로 Todo 생성&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전자는 무슨 뜻인지 생각해야 한다. 후자는 그냥 읽히는 동사다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;브랜드:&lt;/b&gt;&lt;br /&gt;Todit. To-do + do it의 조합. 기능이 이름에 있다. 처음 들었을 때 &quot;아 할 일 관련 서비스구나&quot;가 읽혀야 했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과 페이지도 뜯어고쳤다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정보 위계를 완전히 뒤집었다.&lt;/p&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt;Before: [문서 정보] &amp;rarr; [분석 결과] &amp;rarr; [Todo]
After:  [Todo + 일정] &amp;rarr; [Google Calendar 버튼] &amp;rarr; [문서 정보 (접힘)]&lt;/code&gt;&lt;/pre&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Todo &amp;harr; 일정 연결 (같은 항목이 양쪽에서 참조됨)&lt;/li&gt;
&lt;li&gt;Google Calendar 버튼 &amp;mdash; 이번엔 실제로 동작함. 할 일의 제목과 마감일을 추출해서 &lt;code&gt;calendar.google.com/calendar/render&lt;/code&gt; URL로 인코딩, 클릭 한 번으로 일정 등록&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;Calendar가 처음부터 됐어야 할 기능이었다. Actonix에서 가장 먼저 지적받았던 지점이기도 했다.&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;p data-ke-size=&quot;size16&quot;&gt;원래 크레딧 방식을 생각했다. 1회 파싱 = 1크레딧.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 한국 PG사 정책을 파고들다가 알게 됐다. &lt;b&gt;크레딧 선충전 방식은 PG사가 전자화폐로 분류&lt;/b&gt;한다. 즉, 단순 결제가 아니라 자산 발행이 돼버리는 것이다. 규제 리스크가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 구독으로 피벗했다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;플랜&lt;/th&gt;
&lt;th&gt;가격&lt;/th&gt;
&lt;th&gt;주요 기능&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;₩0&lt;/td&gt;
&lt;td&gt;월 20회, &lt;code&gt;gpt-4o-mini&lt;/code&gt;, 이미지 분석, 광고 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;₩2,900/월&lt;/td&gt;
&lt;td&gt;무제한, &lt;code&gt;gpt-4o&lt;/code&gt;, PDF 지원, 광고 제거, 상세도 조절, 커스텀 카테고리, 우선순위 자동 할당&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;구독은 사용자 입장에서도 예측 가능한 비용이다. 서비스 입장에서도 MRR 기반의 예측 가능한 수익이다. 크레딧보다 양쪽 다 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;월 사용량은 &lt;code&gt;last_refill_at&lt;/code&gt;의 달(Month) 정보를 현재와 비교해서 달이 바뀌면 자동으로 초기화된다. 별도 배치 없이 API 호출 시점에 인라인으로 처리하는 구조다.&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;p data-ke-size=&quot;size16&quot;&gt;Actonix에서 Todit으로 넘어오면서 코어 로직(&lt;code&gt;/lib&lt;/code&gt;, &lt;code&gt;/api&lt;/code&gt;)은 그대로 이전했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI는 의도적으로 버렸다. 기존 UI를 그대로 옮기면 Actonix의 정보 위계 문제가 그대로 따라온다. 처음부터 다시 짜는 게 맞다고 판단했다.&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 14 App Router, TypeScript, Supabase(DB + Storage), Google Cloud Vision, OpenAI. 달라진 건 구조가 아니라 각 레이어가 하는 역할의 명확성이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇 가지 구현 포인트를 정리하면:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OCR + LLM 분리 유지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google Cloud Vision으로 텍스트를 뽑고, OpenAI로 의도를 추론한다. 하나로 합치지 않는 이유는 비용과 정확도 모두 때문이다. OCR은 Vision이 훨씬 싸고 정확하다. LLM은 그 텍스트를 받아서 할 일로 변환하는 데만 집중한다.&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;AI 프롬프트의 5단계 프로토콜&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;프로젝트 완료&quot;라는 목표가 들어오면 AI가 스스로 &quot;자료 조사&quot;, &quot;초안 작성&quot;, &quot;최종 검토&quot;로 쪼갠다. 이게 원자적 분해(Atomic Decomposition)다. 요약이 아니라 실행 단위로 분해하는 것 &amp;mdash; 이게 일반 요약 도구와 기술적으로 다른 지점이다. 유저가 선택한 상세도(&lt;code&gt;Brief / Normal / Detailed&lt;/code&gt;)에 따라 &quot;가장 필수적인 3~5개만&quot; 또는 &quot;최대한 잘게 쪼개기&quot; 명령어가 동적으로 프롬프트에 추가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스키마 검증이 비즈니스 로직&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 응답은 확률적이다. 타입이 틀리거나 배열이 누락될 수 있다. &lt;code&gt;validateActionPlan&lt;/code&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;code&gt;${userId}/&lt;/code&gt;로 시작하는지 반드시 검증한다. Directory Traversal도 정규식으로 차단. 혼자 만들 때 빠뜨리기 쉬운 부분인데, 상용 서비스라면 이게 없으면 안 된다.&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;code&gt;finally&lt;/code&gt; 블록에서 업로드 파일을 스토리지에서 즉시 삭제한다. 사용자 이미지가 서버에 남아있지 않는다는 게 개인정보 관점에서 중요한 포인트다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;inPaser와의 관계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Todit이 최종 목적지가 아니라는 걸 처음부터 알고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Todit은 inPaser의 1레이어다. 의도 추출 능력을 실제 유저로 검증하는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Actonix의 취약점은 명확했다 &amp;mdash; 실행 루프가 불완전했다. 문서 &amp;rarr; JSON까지는 되는데, 시스템 통합이 없었다. 그러면 GPT한테 그냥 물어보는 것과 기능적으로 차이가 없어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;inPaser는 그 루프를 닫는다. 파싱 &amp;rarr; 실행 &amp;rarr; 시스템 통합. Google Calendar 연동이 그 첫 번째 훅이다. 다음은 n8n 웹훅, 그 다음은 B2B API 접근. 이게 완성되면 대체 불가능해진다. 일반 AI 도구가 넘볼 수 없는 워크플로 락인이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 Todit은 그 엔진을 증명하기 위한 첫 번째 서비스다.&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;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;제품이 실제로 하는 것을 정확히 말하지 못하면, 유저는 그걸 더 단순한 것으로 분류해버린다. Actonix가 할 일 생성기였는데 요약 도구로 읽혔던 것처럼.&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;Calendar 버튼이 UI에 있는데 동작 안 하는 건, 없는 것보다 나쁘다. 유저는 클릭했다가 실망한다. 동작하는 기능 하나가 동작 안 하는 기능 열 개보다 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실행 루프의 완성도가 방어 가능성을 결정한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파싱만 하는 서비스는 ChatGPT로 대체된다. 실행까지 이어지는 서비스는 워크플로에 박힌다. 이 차이가 SaaS의 생존을 가른다.&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;수익화 설계 전에 PG 정책부터 읽어야 한다. 구독이 단순히 &quot;자연스러운 선택&quot;이 아니라 규제 관점에서 더 안전한 선택이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 Actonix에서 Todit으로 오는 과정이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제품은 바뀌지 않았다. 내가 무엇을 만드는지에 대한 이해가 더 명확해진 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;Todit은 현재 운영 중입니다. &lt;a href=&quot;https://todit.app&quot;&gt;todit.app&lt;/a&gt;&lt;/i&gt;&lt;/p&gt;</description>
      <category>SaaS/Insight Paser</category>
      <author>Aidengoldkr</author>
      <guid isPermaLink="true">https://aidengoldkr.tistory.com/18</guid>
      <comments>https://aidengoldkr.tistory.com/18#entry18comment</comments>
      <pubDate>Wed, 18 Mar 2026 08:08:35 +0900</pubDate>
    </item>
  </channel>
</rss>