<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://billihazero.github.io//atom.xml" rel="self" type="application/atom+xml" /><link href="https://billihazero.github.io//" rel="alternate" type="text/html" /><updated>2026-03-20T15:48:48+09:00</updated><id>https://billihazero.github.io//atom.xml</id><title type="html">Ha Young’s Blog</title><subtitle>조하영의 깃허브 블로그</subtitle><author><name>Cho Ha Young</name></author><entry><title type="html">JWT 도입과 보안 설계</title><link href="https://billihazero.github.io//dev/2026/03/20/jwt-security-from-my-project/" rel="alternate" type="text/html" title="JWT 도입과 보안 설계" /><published>2026-03-20T00:00:00+09:00</published><updated>2026-03-20T00:00:00+09:00</updated><id>https://billihazero.github.io//dev/2026/03/20/jwt-security-from-my-project</id><content type="html" xml:base="https://billihazero.github.io//dev/2026/03/20/jwt-security-from-my-project/"><![CDATA[<p>최근 면접을 보았습니다. 안타깝게도(당연하게도) 떨어졌지만…</p>

<blockquote>
  <p>“JWT 토큰을 사용하셨던데, 추가적으로 보안과 관련해서 고려하신 사항이나, 설계를 하신 부분이 있나요 ?”</p>
</blockquote>

<p>라는 질문에 어버버하며 access token 관련과 https 처리를 진행했다는 멍청한 대답을 하였습니다. 복기를 하면 할수록 떨어질만 했다 라는 생각이 드네요.</p>

<p>이 참에 확실히 정리를 해보자 싶었습니다.</p>

<h2 id="프로젝트에서-진행했던-설계">프로젝트에서 진행했던 설계</h2>

<blockquote>
  <p>JWT 토큰 기반 도메인 분리를 통해, 공장별로 분리되어 있던 작업현황 화면을 단일 페이지로 통합</p>
</blockquote>

<blockquote>
  <ol>
    <li>Problem</li>
  </ol>

  <ul>
    <li>동일한 구조의 작업현황 조회 화면을 3개 공장에서 각각 사용해야 했으며, 공장별로 별도의 페이지를 만들경우 화면 및 로직 중복 증가</li>
    <li>작업자가 본인 소속 공장의 데이터만 조회할 수 있도록 권한 기반 데이터 접근 제어 필요</li>
  </ul>

  <ol>
    <li>Solution</li>
  </ol>

  <ul>
    <li>로그인 시 발급되는 JWT 토큰에 사용자 타입(user_type)과 공장 코드(plant_cd)를 포함하여 사용자 소속 도메인 명확화</li>
    <li>작업 현황 조회 시 JWT토큰의 도메인을 기준으로 자동 필터링되도록 구현하여 공장별로 분리된 조회 화면의 단일 페이지 통합</li>
  </ul>

  <ol>
    <li>Result</li>
  </ol>

  <ul>
    <li>화면 및 로직 중복 제거를 통한 유지보수 효율성 향상</li>
    <li>권한 기반 데이터 조회를 통한 보안성 및 데이터 정합성 확보</li>
  </ul>
</blockquote>

<p>포트폴리오에 작성한 내용 일부입니다.</p>

<h3 id="설계-내용">설계 내용</h3>

<p>해당 프로젝트에서는 기존에 1 ~ 3 공장의 작업현황 화면이 각각 분리되어 있었으며,
사용자는 본인이 속한 공장의 화면만 볼 수 있도록 설계되어 있었습니다.</p>

<p>가장 단순한 방법은 공장별로 동일한 구조의 작업현황 화면을 각각 구현하고,
메뉴 권한을 통해 접근을 제한하는 것이었지만, 이 경우 화면 및 로직의 중복이 발생하여 유지보수 비용이 증가하는 문제가 있었습니다.</p>

<p>이를 해결하기 위해 JWT Access Token에 공장 코드를 포함시키고, 작업 현황 조회 시 해당 공장코드를 기준으로 데이터를 필터링 하도록 설계하였습니다.</p>

<h3 id="설계-시-고려했던-내용">설계 시 고려했던 내용</h3>

<p>1.인증 및 토큰 검증 구조 설계 - 토큰 위변조 감지, 토큰 만료 및 재발급 정책</p>

<p>해당 프로젝트는 작업현황 조회 뿐만 아니라, 로그인 이후 모든 API 요청에 대해 토큰 기반 인증이 필요했습니다.
이를 위해 공통 미들웨어를 구성하여, API 수행 전에 토큰 검증과정을 거치도록 설계하였습니다.</p>

<p>클라이언트 요청 시 Authorization Header에서 Access Token을 추출하고, jwt.verify를 통해 토큰의 서명 및 유효성을 검증하였습니다. 검증에 성공한 경우에는 decoded payload를 req.user에 저장하여, 이후 API 로직에서 사용자 정보를 활용할 수 있도록 구성하였습니다.</p>

<p>Access Token이 만료된 경우에는 Refresh Token을 통해 재검증을 수행하도록 하고, Refresh가 없는 경우에는 인증 실패로 처리하여 로그인 화면으로 이동되도록 처리하였습니다.</p>

<p>2.HTTPS 통신 및 Refresh Token 저장 방식</p>

<p>JWT는 payload가 암호화되지 않는 구조이기 때문에, 토큰이 네트워크 구간에서 탈취 될 위험이 존재합니다.
이 때문에 HTTPS 기반 설정이 필수적이고, Refresh Token 전송 및 저장 방식에 대한 고려가 필요합니다.</p>

<p>초기 개발단계에는 HTTPS 적용을 하지 않은 상태였기에 쿠키에 httpOnly 및 secure 옵션을 적용하지 않았습니다. 이후 운영 단계에서 HTTPS 적용이 된 이후 개발/운영 환경을 구분하여 운영 환경일 경우, httpOnly, secure 옵션이 적용된 쿠키로 관리하였습니다.</p>

<p>3.로그 저장을 통한 사용자 행위 추적</p>

<p>인증을 통해 접근을 제어하는 것 뿐만 아니라, 인증 이후 사용자가 어떤 기능을 수행했는지 추적할 수 있어야 한다고 생각했습니다.
이를 위해 API가 호출될 때 마다 로그를 저장하는 미들웨어를 구성하였습니다.</p>

<p>로그에는 USER_ID, PATH, HOST_NAME, PROGRAM_ACTION, PROGRAM_CONTROLLER, PROGRAM_FUNCTION, 등의 정보를 기록하도록 하였으며, 이를 통해 사용자별 요청 이력과 기능 수행 내역을 확인할 수 있도록 하였습니다.
로그를 통해 권한 오남용이나 비정상 접근 발생 시 추적이 가능하도록 설계하였습니다.</p>

<h2 id="추가적으로-고려했어야-할-보안-요소">추가적으로 고려했어야 할 보안 요소</h2>

<p>1.Rate Limiting</p>

<p>로그인 or 토큰 재발급 API에 대해 요청 횟수를 제한하는 Rate Limiting 전략이 필요합니다. 사용자가 상대적으로 적다보니 고려하지 않았지만, 탈취 등의 보안 이슈가 생길 경우 무차별 대입공격이나 토큰 재발급 시도를 줄이는 등의 인증 시스템 오남용을 방지할 수 있습니다.</p>

<p>2.Reverse Proxy 필터링</p>

<p>Reverse Proxy(Nginx)를 통해 클라이언트 요청을 백엔드로 전달할 때
Host, Client IP, 프로토콜 정보를 설정하여 백엔드에서 인증처리 할때 활용할 수 있도록 할 수 있습니다.</p>

<h2 id="결론">결론</h2>

<p>프로젝트를 진행하면서 JWT에 대한 개념을 이해하고 적용했다고 생각했는데, 막상 면접 과정에서 해당 구조를 조리있게 설명하지 못한게 매우 아쉽습니다.
그래도 면접을 통해 단순히 기능을 구현하는 것 뿐만 아니라, 설계 의도를 명확하게 설명하는 것도 중요하다는 것을 깨달았습니다. <br />
다음부턴 타인에게도 위처럼 설명할 수 있도록 충분히 공부하고 이해하는 방향으로 개발하고자 합니다.</p>

<hr />]]></content><author><name>Cho Ha Young</name></author><category term="DEV" /><category term="JWT" /><category term="security" /><summary type="html"><![CDATA[최근 면접을 보았습니다. 안타깝게도(당연하게도) 떨어졌지만…]]></summary></entry><entry><title type="html">Prisma + Timestamp 날짜 조건 검색하기</title><link href="https://billihazero.github.io//dev/2026/03/20/prisma-timezone/" rel="alternate" type="text/html" title="Prisma + Timestamp 날짜 조건 검색하기" /><published>2026-03-20T00:00:00+09:00</published><updated>2026-03-20T00:00:00+09:00</updated><id>https://billihazero.github.io//dev/2026/03/20/prisma-timezone</id><content type="html" xml:base="https://billihazero.github.io//dev/2026/03/20/prisma-timezone/"><![CDATA[<p>대부분 DB 테이블에 필수로 들어가는 컬럼 “createdAt” 이 있습니다. 이 컬럼은 보통 Timestamp 타입으로 저장되며, 기본적으로 UTC를 기준으로 기록됩니다.</p>

<p>Prisma는 timezone 설정을 따로 지원하지 않기 때문에, 날짜 조건을 이용한 조회를 구현할 때 고민이 필요했습니다.</p>

<hr />

<h2 id="문제상황">문제상황</h2>

<p>예를 들어, “2026-03-20의 하루 데이터” 를 조회한다고 하면, 이 기준은 한국 시간(KST) 기준입니다. 하지만 DB에는 시간이 UTC 기준으로 저장되어 있기 때문에, 단순히 “2026-03-20” 을 그대로 비교하면 원하는 결과가 나오지 않습니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>KST 기준 하루 : 2026-03-20 00:00:00 ~ 2026-03-21 00:00:00 (KST)

UTC로 보았을 때 : 2026-03-19 15:00:00 ~ 2026-03-20 15:00:00 (UTC)

</code></pre></div></div>

<p>하루의 기준이 달라집니다.</p>

<h2 id="해결방법">해결방법</h2>

<p>그래서 where 조건으로 날짜를 넣을 때, KST 기준 하루 범위를 만든 후 이를 Date 객체로 변환하여 넣는 방식으로 처리 하였습니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import tz from 'dayjs/plugin/timezone';

dayjs.extend(utc);
dayjs.extend(tz);

const KST = 'Asia/Seoul';

export function kstDayRange(target) {
  const base = target ? dayjs.tz(target, KST) : dayjs().tz(KST);
  const start = base.startOf('day');
  const end = start.add(1, 'day');

  return {
    startDate: start.toDate(),
    endDate: end.toDate(),
  };
}

</code></pre></div></div>

<p>그리고 Prisma에서는 이렇게 사용하였습니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>const { startDate, endDate } = kstDayRange('2026-03-20');
const result = await prisma.post.findMany({
    where: {
        createdAt: {
            gte: startDate,
            lt: endDate,
        },
    },
});
</code></pre></div></div>

<h2 id="구현-포인트">구현 포인트</h2>

<ul>
  <li>날짜 기준은 KST로 계산합니다.</li>
  <li>실제 비교는 UTC기반 Date 객체로 수행합니다.
    <ul>
      <li>문자열 포맷으로 바꿀 경우, timezone 정보가 없기 때문에 Date 객체로 변환하여 사용</li>
    </ul>
  </li>
</ul>

<h2 id="번외-timezone-지원-orm">번외) Timezone 지원 ORM</h2>

<h3 id="1-typeorm">1. TypeORM</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TypeOrmModule.forRoot({
  type: 'mysql',
  timezone: 'Z',
})

</code></pre></div></div>

<ul>
  <li>DB 연결 시 timezone 지정이 가능합니다.</li>
  <li>조회할 때 자동 변환되어 조회됩니다.</li>
</ul>

<h3 id="2-sequelize">2. Sequelize</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>new Sequelize(db, user, pass, {
  timezone: '+09:00'
});
</code></pre></div></div>

<ul>
  <li>저장/조회 시 timezone 적용이 가능합니다.
    <ul>
      <li>저장 시 timezone을 적용하면, +09:00 기준으로 해석해서 DB에 저장됩니다.</li>
    </ul>
  </li>
</ul>

<h2 id="reference">Reference</h2>

<p>https://github.com/prisma/prisma/discussions/15329</p>]]></content><author><name>Cho Ha Young</name></author><category term="DEV" /><category term="Prisma" /><category term="Timezone" /><summary type="html"><![CDATA[대부분 DB 테이블에 필수로 들어가는 컬럼 “createdAt” 이 있습니다. 이 컬럼은 보통 Timestamp 타입으로 저장되며, 기본적으로 UTC를 기준으로 기록됩니다.]]></summary></entry><entry><title type="html">면접을 보고 왔습니다.</title><link href="https://billihazero.github.io//my/2026/03/05/Interview/" rel="alternate" type="text/html" title="면접을 보고 왔습니다." /><published>2026-03-05T00:00:00+09:00</published><updated>2026-03-05T00:00:00+09:00</updated><id>https://billihazero.github.io//my/2026/03/05/Interview</id><content type="html" xml:base="https://billihazero.github.io//my/2026/03/05/Interview/"><![CDATA[<h2 id="면접에서-받은-질문-중-대답하지-못했거나-애매하게-대답했던-부분">면접에서 받은 질문 중 대답하지 못했거나 애매하게 대답했던 부분</h2>

<ul>
  <li>Index 관련</li>
  <li>pnpm의 장점 (+ 단점)</li>
  <li>nest 에서의 crud 자동화</li>
  <li>fk로 묶여있을 때 user가 탈퇴할경우 작성했던 문서 처리</li>
  <li>jwt와 ncp 인프라를 통한 보안 관련</li>
  <li>trigger나 polling과 같은 기능을 서비스 단에서 처리할 수 없는지</li>
</ul>

<hr />]]></content><author><name>Cho Ha Young</name></author><category term="My" /><category term="Interview" /><summary type="html"><![CDATA[면접에서 받은 질문 중 대답하지 못했거나 애매하게 대답했던 부분]]></summary></entry><entry><title type="html">ChatGPT 앱 기반 MCP 서버 구축과 Docker 배포</title><link href="https://billihazero.github.io//project/2026/02/12/docker-nginx-project-setting/" rel="alternate" type="text/html" title="ChatGPT 앱 기반 MCP 서버 구축과 Docker 배포" /><published>2026-02-12T00:00:00+09:00</published><updated>2026-02-12T00:00:00+09:00</updated><id>https://billihazero.github.io//project/2026/02/12/docker-nginx-project-setting</id><content type="html" xml:base="https://billihazero.github.io//project/2026/02/12/docker-nginx-project-setting/"><![CDATA[<p>스위프 웹 프로젝트에 참여하면서 단순한 웹 서비스 개발을 넘어,
ChatGPT 앱 기반으로도 사용할 수 있는 구조를 설계하고 구현하는 경험을 하게 되었습니다.</p>

<p>이번 프로젝트에서는 웹 서비스(Next.js)와 API 서버(Java) 뿐만 아니라,
ChatGPT 앱과 연동하여 앱 형태로도 활용할 수 있는 MCP 서버 (NestJS)까지 함께 구축했습니다.</p>

<p>또한 각 서비스를 Docker로 컨테이너화 하고, PostgresSQL과 Nginx를 포함한 전체 인프라를 docker-compose로 통합 구성한 뒤, Ncloud 환경에 배포하는 과정을 담당했습니다.</p>

<p>이 글에서는 해당 프로젝트의 전체 아키텍처 구성부터 Docker 기반 서비스 분리, Proxy 설정, 그리고 클라우드 배포 과정까지 정리해보려고 합니다.</p>

<h2 id="1-프로젝트-구성">1. 프로젝트 구성</h2>

<p>이번 프로젝트는 다음과 같은 구조로 진행했습니다.</p>

<table>
  <thead>
    <tr>
      <th>구성 요소</th>
      <th>포트</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Backend (Java)</td>
      <td>8080</td>
      <td>REST API 서버</td>
    </tr>
    <tr>
      <td>Frontend (Next.js)</td>
      <td>3000</td>
      <td>사용자 웹 서비스</td>
    </tr>
    <tr>
      <td>MCP (NestJS)</td>
      <td>3001</td>
      <td>별도 서비스 서버</td>
    </tr>
    <tr>
      <td>PostgreSQL</td>
      <td>5432</td>
      <td>데이터베이스</td>
    </tr>
    <tr>
      <td>Nginx</td>
      <td>80</td>
      <td>Reverse Proxy</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="2-docker로-배포한-이유">2. Docker로 배포한 이유</h2>

<p>여러 구성 요소를 하나의 서버에서 함께 운영해야 했습니다. <br />
이 과정에서 실행 환경을 통일하고, 배포를 단순화하기 위해 Docker를 선택했습니다.</p>

<h3 id="docker의-장점">Docker의 장점</h3>

<ol>
  <li>개발 환경과 운영 환경을 동일하게 운영 할 수 있습니다.
    <ul>
      <li>로컬에서 정상 동작하던 서비스가 서버에서 오류가 나는 이유는 단순한 버전 차이뿐 아니라, 운영체제, 환경변수, 네트워크 구조 등 실행 환경의 차이 때문입니다. Docker는 이러한 실행 환경 자체를 이미지로 고정하여 환경 불일치 문제를 최소화해줍니다.</li>
    </ul>
  </li>
  <li>여러 서비스를 구조적으로 관리할 수 있습니다.
    <ul>
      <li>Docker가 없었다면 위의 내용을 구성하기 위해 Node, Java, PostgreSQL, Nginx, 환경변수설정, 포트 충돌 관리를 서버에 직접 세팅해야 했습니다.</li>
      <li>Docker를 사용하면 각 서비스를 컨테이너 단위로 격리할 수 있고, 서버에는 Docker만 설치하면 됩니다.</li>
    </ul>
  </li>
  <li>배포 과정을 단순화 할 수 있습니다.</li>
</ol>

<ul>
  <li>Docker를 사용하지 않았다면 의존성 설치 -&gt; 빌드 -&gt; 프로세스 실행 -&gt; 서비스별로 반복</li>
  <li>Docker를 사용하면, “docker-compose up -d”</li>
</ul>

<h3 id="docker-composeyml">docker-compose.yml</h3>

<p>컨테이너를 각각 띄우면 옵션이 많아지고 실수하기 쉽습니다. <br />
서비스 간 네트워크 연결, 의존성 순서, 환경변수, 볼륨을 한 파일에서 관리 할 수 있습니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>version: "3.8"

services:
  frontend:
    build: ./frontend
    container_name: frontend
    expose:
      - "3000"

  backend:
    build: ./backend
    container_name: backend
    expose:
      - "8080"
    environment:
      DB_HOST: postgres

  mcp:
    build: ./mcp
    container_name: mcp
    expose:
      - "3001"

  postgres:
    image: postgres:15
    container_name: postgres
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    volumes:
      - postgres-data:/var/lib/postgresql/data

  nginx:
    image: nginx:latest
    container_name: nginx
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - frontend
      - backend
      - mcp

volumes:
  postgres-data:

</code></pre></div></div>

<h3 id="자동-배포를-위한-docker-hub-사용">자동 배포를 위한 Docker hub 사용</h3>

<ul>
  <li>CI/CD로 전환하기 위해 Docker hub를 사용하였습니다.</li>
</ul>

<h4 id="추후-작성할-내용">추후 작성할 내용</h4>

<ul>
  <li>Nginx Proxy 문제</li>
  <li>Next.js 빌드 시 환경변수 적용 문제</li>
  <li>Ncloud 적용기</li>
</ul>]]></content><author><name>Cho Ha Young</name></author><category term="Project" /><category term="ChatGPT" /><category term="Docker" /><category term="Ncloud" /><summary type="html"><![CDATA[스위프 웹 프로젝트에 참여하면서 단순한 웹 서비스 개발을 넘어, ChatGPT 앱 기반으로도 사용할 수 있는 구조를 설계하고 구현하는 경험을 하게 되었습니다.]]></summary></entry><entry><title type="html">자료구조 이론 정리</title><link href="https://billihazero.github.io//cs/2026/01/08/Data-Structure/" rel="alternate" type="text/html" title="자료구조 이론 정리" /><published>2026-01-08T00:00:00+09:00</published><updated>2026-01-08T00:00:00+09:00</updated><id>https://billihazero.github.io//cs/2026/01/08/Data-Structure</id><content type="html" xml:base="https://billihazero.github.io//cs/2026/01/08/Data-Structure/"><![CDATA[<p>신찬수 교수님의 유튜브 자료구조 강의를 시청하며 강의 내용을 정리한 글입니다. 강의를 시청하면서 이해한 개념들을 하나씩 정리하고, 이후에도 계속해서 내용을 추가해 나갈 예정입니다.</p>

<p><a href="https://www.youtube.com/watch?v=PIidtIBCjEg&amp;list=PLsMufJgu5933ZkBCHS7bQTx0bncjwi4PK">신찬수 교수님 유튜브</a></p>

<h2 id="list">List</h2>

<p>Python에서의 List는 읽기/쓰기 이외에 유연한 삽입/삭제 연산을 지원합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>A.append(value)         #맨 오른쪽에 새로운 값 value 를 삽입
A.pop()                 #맨 오른쪽 값을 지우고, 그 값을 리턴
A.pop(i)                #A[i] 값을 지운 후, 그 값을 리턴. 오른쪽 값들은 왼쪽으로 이동
A.insert(i, value)      #A[i] 에 새로운 값 value를 삽입, 나머지 오른쪽으로 이동
A.remove(value)         #value를 찾아 제거, 여러개라면 제일 왼쪽 값을 제거
A.index(value)          #value 값이 처음으로 등장하는 index 리턴
A.count(value)          #value 값이 몇 번 등장하는지 횟수를 리턴
A[i:j]                  #A[i] ~ A[j-1] 까지 복사하여 새로운 리스트 리턴
value in A              #A에 value 가 있으면 True, 없으면 False를 리턴
</code></pre></div></div>

<h2 id="queue">Queue</h2>

<blockquote>
  <p>queue는 먼저 저장한 데이터가 먼저 출력되는 선입선출 FIFO 형식으로 데이터를 저장하는 자료구조입니다. <br />
queue의 뒤에서 데이터를 추가하는 것을 enqueue, 앞에서 데이터를 꺼내는 것을 dequeue라고 합니다.(stack에서 enqueue = push, dequeue =pop)</p>
</blockquote>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Index:  0   1   2   3   4
        ┌───┬───┬───┬───┬───┐
Array:  │(5)│ 8 │ 3 │ 7 │ 2 │
        └───┴───┴───┴───┴───┘

enqueue(5)
enqueue(8)
enqueue(3)
enqueue(7)
enqueue(2)

dequeue(5)
</code></pre></div></div>

<hr />

<p>Queue를 클래스로 구현해봅니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class Queue:
    def __init__(self):
        self.items = [] #빈 리스트
        self.front_index = 0
    def enqueue(self, val):
        self.items.append(val)
    def dequeue(self):
        if(self.front_index == len(self.items)):
            print("Queue is Empty")
        else:
            X = self.items[self.front_index]
            self.front_index += 1
            return X
</code></pre></div></div>

<p>Queue 클래스를 구현한 뒤 이를 이용하여 요세푸스 문제를 풀어보았습니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>n, k = map(int, input().split())

class Queue:
    (위의 구현 내용...)
    def size(self):
        return len(self.items) - self.front_index

def joseph(n, k):
    q = Queue() # 사람들을 list에 삽입

    for i in range (1, n+1):
        q.enqueue(i)

    while q.size() &gt; 1: # 한명 남을 때 까지 반복
        for _ in range(k-1):     # k-1번까지
            q.enqueue(q.dequeue())

        removed = q.dequeue() #k번째는 제거
        print("제거된 사람: ", removed)
    return q.dequeue()

result = joseph(n, k)
print("마지막 생존자: " , result)
</code></pre></div></div>

<h2 id="linked-list-연결-리스트">Linked List (연결 리스트)</h2>

<blockquote>
  <p>Linked List는 Node(key, link)라는 구조체가 연결되는 형식으로 데이터를 저장하는 자료구조 입니다.</p>
</blockquote>

<p><img src="/post_images/list_img.png" alt="linkedlist" /></p>

<h3 id="arraylist와-linkedlist의-차이점">ArrayList와 LinkedList의 차이점</h3>

<p>Array는 메모리 상에서 연속되 공간을 차지하기 때문에, 각 요소는 인덱스를 통해 직접 접근할 수 있습니다. <br /></p>

<p>배열의 시작 주소를 알고 있다면 i 번째 인덱스의 요소를 읽거나 수정하는데 O(1)의 시간복잡도를 가집니다.<br /></p>

<p>조회 성능이 매우 빠른 것이 장점이라면, 중간에 요소를 삽입하거나 삭제를 하기 위해선 뒤에 있는 요소들을 모두 이동(O(n))시켜야 하므로, 이 경우에는 성능이 저하될 수 있습니다.</p>

<p>LinkedList는 Node라는 단위로 구성되며, 각 Node는 데이터 값과 다음 노드를 가리키는 주소를 함께 저장합니다.<br /></p>

<p>이 구조는 메모리 상에서 연속성을 유지하지 않으며, 특정 인덱스의 요소로 접근하기 위해서는 Head Node부터 시작하여 순차적으로 다음 노드를 따라가야 합니다. <br /></p>

<p>두 번째 요소에 접근하려면 Head Node 부터 시작하여 두 번 이동해야 하므로, O(n)의 시간복잡도를 가집니다. <br /></p>

<p>노드 간의 연결만 변경하면 되기 때문에 중간 삽입이나 삭제는 O(1)에 가깝게 처리할 수 있습니다. <br /></p>

<p>LinkedList에는 한방향 연결리스트, 양방향 연결리스트가 있습니다.</p>

<hr />

<p>한방향 연결리스트(Singly Linked List)는 Node 객체들이 연결된 구조로 이루어져 있습니다.</p>

<p>Node 클래스를 구현해 봅니다.</p>

<h3 id="singly-linked-list-기본">Singly Linked List 기본</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class Node:
    def __init__(self, key=None):
        self.key = key (값 저장)
        self.next = None (주소 저장)
    def __str__(self):
        return str(self.key)

a = Node(3)
b = Node(9)
c = Node(-1)
d = Node(2)
</code></pre></div></div>

<p>각 Node는 각각의 변수에 할당되어 있지만, 실제로 연결리스트를 순회하는 데에는 모든 노드의 변수를 알고 있을 필요가 없습니다. <br /></p>

<p>첫 번째 노드인 Head Node만 알고 있으면, next 참조를 따라가면 나머지 모든 노드에 접근 할 수 있습니다.</p>

<p>SinglyLinkedList 클래스를 구현합니다. 기본 구성요소를 정의하고, 노드의 삽입과 삭제를 처리하는 메서드를 구현합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Class SinglyLinkedList:
    def __init__(self):
        self.head =None  #Head Node의 주소
        self.size = 0    #LinkedList를 구성하는 Node의 개수
    def__len__(self):
        return self.size
</code></pre></div></div>

<h4 id="singly-linked-list-삽입-삭제">Singly Linked List 삽입, 삭제</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    def pushFront(self, key):
        new_node = Node(key)
        new_node.next = self.head
        self_head = new_node
        self.size +=1

    def pushBack(self, key):
        if len(self) == 0:
            return None
        else:
            new_node = Node(key)
            tail = self.head
            while(tail.next !== None):
                tail.next = tail
            tail.next = new_node
            self.size -= 1
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    def popFront(self, key):
        if self.head is None:
            return None
        else:
            removed = self.head
            self.head = self.head.next
            self.size -= 1
            return removed.key
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    def popBack(self):
        if len(self) == 0 :
            return None

        #노드가 2개 이상인 경우
        prev = None
        tail = self.head

        # tail.next가 None이 될때까지
        while tail.next is not None:
            prev = tail
            tail = tail.next

        # tail = 마지막 노드, prev = 마지막 바로 앞 노드
        # tail.Next = None (tail 끝까지 온 것)
        if len(self) == 1 :
            self.head = None
        else:
            prev.next = None
            key = tail.key
            del tail
            self.size -= 1
            return key

</code></pre></div></div>

<h4 id="singly-linked-list-탐색--제너레이터">Singly Linked List 탐색 + 제너레이터</h4>

<p>한방향 연결리스트에서 노드를 탐색하는 방법과, 탐색 로직을 <strong>제너레이터(generator)</strong> 로 구현하는 메서드를 구현합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def search(self,key):
    #key값의 node를 return, key값이 없다면 None return
    v = self.head
    while v is not None:
        if v.key == key:
            reuturn v
        else:
            v = v.next
    return None (or v = None)
</code></pre></div></div>

<p>제너레이터 함수는 반복 가능한 (iterable) 객체를 만들기 위해 사용하는 함수로, “for x in list” 와 같은 반복문에서 특히 유용합니다. 파이썬의 for문은 내부적으로 객체의 <strong>이터레이터(iterator)</strong>를 자동으로 요청하여 순차적으로 값을 가져옵니다. <br /></p>

<p>따라서 for x in SinglyLinkedList 와 같은 형태의 반복을 지원하려면, 파이썬이 해당 객체를 어떻게 순회해야 하는지 알 수 있도록 이터레이터를 제공해야합니다. 이를 위해 연결리스트를 구현할 때 특수 메서드인 <strong>iterator</strong>를 정의하는 것이 바람직 합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def __iter__(self):
    v = self.head
    while v is not None:
        yield v
    v = v.next

</code></pre></div></div>

<p>yield는 함수의 실행을 완전히 종료하지 않고, 현재 상태를 유지한 채 값을 반환하는 키워드 입니다. yield가 포함된 함수는 <strong>제너레이터(generator)</strong>가 되며, 반복 요청이 있을 때 마다 값을 하나씩 반환합니다. <br /></p>

<p>제너레이터 함수에서 더이상 yield 할 것이 없고 함수의 실행이 끝나면, 파이썬은 내부적으로 StopIteration 예외를 발생시켜 반복이 종료됩니다.<br /></p>

<p>(순차적 자료구조에서 iterator 메서드를 구현해놓는 것이 좋습니다.)</p>

<h3 id="doubly-linked-list-기본">Doubly Linked List 기본</h3>

<p>한방향 연결리스트(Singly Linked List)는 각 노드가 다음 노드만을 가리키는 구조입니다. 이 구조에서는 <strong>tail 노드의 이전 노드(prev)를 알기 위해 처음부터 순회해야 하므로 O(n)의 시간이 소요되는 단점</strong>이 있습니다.</p>

<p>이러한 단점을 보완하기 위해 등장한 자료구조가 <strong>양방향 연결리스트(Double Linked List)</strong> 입니다. 양방향 연결 리스트는 각 노드가 이전 노드(prev)와 다음 노드(next)를 모두 가리키도록 구성되어 있어, 앞뒤 방향으로의 이동이 가능하며 특정 노드의 삭제나 삽입을 더 효율적으로 처리할 수 있습니다.</p>

<p>다만, 노드마다 관리해야 할 링크가 하나 더 늘어나기 때문에 메모리 사용량이 증가하고 구현 복잡도가 높아질 수 있다는 단점도 존재합니다.</p>

<h4 id="원형-양방향-연결-리스트-circularly-doubly-linked-list">원형 양방향 연결 리스트 (Circularly Doubly Linked List)</h4>

<p>양방향 연결리스트를 확장한 형태로 <strong>원형 양방향 연결 리스트 (Circularly Doubly Linked List)</strong> 가 있습니다.</p>

<p>이 구조에서는</p>

<ul>
  <li>tail 노드의 next가 head 노드를 가리키고</li>
  <li>head 노드의 prev가 tail 노드를 가리키는 방식</li>
</ul>

<p>으로 연결되어, 리스트의 시작과 끝이 하나의 원으로 이어집니다.</p>

<p>이 구조를 구현할 때는 보통 <strong>빈 리스트를 하나의 노드로 표현</strong>하는데, 이를 dummy node라고 부릅니다.</p>

<p>dummy node는 실제 데이터를 저장하지 않으며, 리스트에서 <strong>어디가 시작인지 표시해주는 기준(마커)역할</strong>을 합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class Node:
    def __init__(self, key=None):
        self.key = key
        self.next = self
        self.prev = self

class DoublyLinkedList:
    def __init__(self):
        self.head = Node()
        self.size = 0
    def __iter ...
    def __str ...
    def __len ...

</code></pre></div></div>

<h4 id="양방향-연결리스트에서-splice-연산-적용">양방향 연결리스트에서 Splice 연산 적용</h4>

<p>splice(a, b, x): 현재 리스트에서 연속 구간 [a,,,b] (a부터 b까지)를 잘라낸 다음, 노드 x 에 그 구간을 그대로 붙입니다.</p>

<h5 id="splice-연산의-조건">Splice 연산의 조건</h5>

<p>Splice 연산이 정상적으로 동작하기 위해서는 다음 조건들이 반드시 만족되어야 합니다.</p>

<ul>
  <li>조건 1: a -&gt; … -&gt; b 관계가 성립해야합니다.</li>
  <li>조건 2: a와 b 사이에 head 노드가 존재하면 안됩니다.</li>
</ul>

<p><img src="/post_images/circlyList.png" alt="Circularly Doubly Linked List" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def __splice__(self, a, b, x):

    # [a..b] 구간을 떼어내기
    a_prev = a.prev
    b_next = b.next
    a_prev.next = b_next
    b_next.prev = a_prev

    # [a..b] 구간을 x 뒤에 삽입하기
    x_next = x.next

    x.next = a
    a.prev = x

    b.next = x_next
    x_next.prev = b
</code></pre></div></div>

<h4 id="splice-연산을-활용한-삽입-이동-탐색-삭제-연산">Splice 연산을 활용한 삽입, 이동, 탐색, 삭제 연산</h4>

<h5 id="이동연산">이동연산</h5>

<p>이동연산에는 총 4개의 함수가 있습니다.
moveAfter/Before, insertAfter/Before</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def moveAfter(self, a, x): #노드a를 노드x 다음으로 이동
    =&gt; splice(a, a, x) # a ~ a 까지 x의 뒤로 이동

def moveBefore(self, a, x): #노드 a를 노드 x 전으로 이동
    splice(a, a, x.prev) # a~a 까지 x 전으로 이동

def insertAfter(self, x, key): #노드 x 다음에 노드(key) = v를 삽입
    =&gt; moveAfter(v, x) =&gt; splice(v, v, x)

def insertBefore(self, x, key): #노드 x 전에 노드(key) = v 를 삽입
    =&gt; moveBefore(v, x) =&gt; aplice(v, v, x.prev)
</code></pre></div></div>

<h5 id="탐색연산">탐색연산</h5>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def search(self, key):
    v = self.head #dummy
    while.next !== self.head: #마지막이 아닐경우
        if v.key == key:
            return v
        v = v.next
    return None # 못 찾았을 때

</code></pre></div></div>

<h5 id="삭제연산">삭제연산</h5>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def remove(x): #노드 x 를 삭제
    if x is None or x == self.head # x가 없거나, head를 지우면 안됨
        return
    x.prev.next = x.next
    x.next.prev = x.prev
</code></pre></div></div>

<h2 id="해시-테이블-hash-table">해시 테이블 (Hash Table)</h2>

<p>해시 테이블(Hash Table)은 <strong>삽입, 삭제, 탐색</strong> 연산을 <strong>평균적으로 O(1)</strong>이라는 매우 빠른 시간 복잡도로 수행할 수 있는 자료구조 입니다.</p>

<p>파이썬에서는 해시 테이블이 <strong>dictionary(dict)</strong> 현태로 구현되어 있습니다.</p>

<h3 id="파이썬-dictionary-구조">파이썬 Dictionary 구조</h3>

<p>dictionary는 <strong>key-value 쌍</strong> 으로 데이터를 저장합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>D = {
    2017276: "홍길동",
    2018209: "김철수",
    2018229: "톰 크루즈"
}
</code></pre></div></div>

<p>key: 학번 - value: 이름 형태로 선언되었습니다.</p>

<p>여기서 각 key는 해시 함수(hash function)을 통해 특정 인덱스로 변환되어 테이블에 저장됩니다.</p>

<h3 id="해시-함수와-인덱싱">해시 함수와 인덱싱</h3>

<p>예를 들어 해시함수가 다음과 같다고 가정해봅니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>f(k) = k % 10
</code></pre></div></div>

<p>이 경우, 해시 테이블의 인덱스는 0~9가 됩니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>key = 2017276 value = 홍길동 f(k) = 6
key = 2018209 value = 김철수 f(k) = 9
key = 2018229 value = 톰 크루즈 f(k) = 9
</code></pre></div></div>

<p>이 때 김철수와 톰크루즈는 동일한 인덱스인 9에 매핑됩니다.</p>

<h3 id="collision">Collision</h3>

<p>이미 9번 인덱스에 김철수가 저장되어 있는데, 같은 위치에 톰 크루즈를 저장하려고 하면 문제가 발생합니다.</p>

<p>이처럼 서로 다른 <strong>key가 동일한 해시 값(인덱스)을 가지는 현상을 Collision(충돌)</strong> 이라고 합니다.</p>

<p>충돌은 해시테이블에서 피할 수 없는 문제이며, 이를 해결하기 위한 전략을 <strong>Collision Resolution Method</strong> 라고 합니다.</p>

<h3 id="해시-테이블의-구성-요소">해시 테이블의 구성 요소</h3>

<ol>
  <li>Table(보통 배열 or 리스트)</li>
  <li>Hash Funciton</li>
  <li>Collision Resolution Method</li>
</ol>

<h3 id="좋은-해시-함수란-">좋은 해시 함수란 ?</h3>

<p>해시 함수는 다음과 같은 성질을 갖는 것이 이상적입니다.</p>

<ul>
  <li>충돌이 적을 것</li>
  <li>Key들이 테이블 전체에 <strong>고르게 분포</strong> 될 것</li>
</ul>

<p>이론적으로 <strong>충돌이 절대 발생하지 않는 해시 함수, Perfect Hash Function</strong>이 가장 이상적입니다.</p>

<p>그러나, PHF는</p>

<ul>
  <li>모든 key 집합을 미리 알고 있어야 하고,</li>
  <li>동적 데이터에는 적용하기 어렵기 때문에</li>
</ul>

<p>현실적으로 사용하기란 어렵습니다.</p>

<h3 id="universal-hash-funtion">Universal Hash Funtion</h3>

<p>현실적인 대안으로 <strong>Universal Hash Function</strong>이 사용됩니다.</p>

<p>서로 다른 두 key x, y에 대해</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Pr(f(x) == f(y)) = 1 / m (m = 테이블의 크기)
</code></pre></div></div>

<p>이 성질을 만족하면 universal hash function 이라고 합니다.</p>

<p>만약 충돌 확률이</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Pr(f(x) == f(y)) ≤ c / m   (c &gt; 0)
</code></pre></div></div>

<p>라면 이를 c-universal hash function 이라고 합니다.</p>

<h3 id="대표적인-해시-함수-기법">대표적인 해시 함수 기법</h3>

<ol>
  <li>Division method</li>
  <li>Multiplication method</li>
  <li>Folding</li>
  <li>Mid-square method</li>
</ol>

<h3 id="충돌-회피-방법">충돌 회피 방법</h3>

<p>앞서 살펴본 것 처럼 Perfect Hash Function(PHF)이 아닌 이상, 해시 테이블에서는 <strong>충돌(Collision)</strong> 이 반드시 발생합니다.</p>

<p>이미 어떤 slot에 다른 key가 저장되어 있을 때,</p>

<p><strong>이 데이터를 어디에 저장할 것인가?</strong></p>

<p>이 문제를 해결하는 방법을 <strong>충돌 회피 방법(Collision Resolution Method)</strong> 이라고 합니다.</p>

<h3 id="open-addressing-기법">Open Addressing 기법</h3>

<p>충돌이 발생했을 때, <strong>테이블 내부에서 다른 빈 공간을 찾아 저장하는 방식</strong>을 <strong>Open Addressing</strong> 이라고 합니다.</p>

<p>Open Addressing에는 대표적으로 다음과 같은 기법들이 있습니다.</p>

<ul>
  <li>Linear Probing</li>
  <li>Quadratic Probing</li>
  <li>Double Hashing</li>
</ul>

<h4 id="linear-probing">Linear Probing</h4>

<p>Linear Probing은 충돌이 발생하면 <strong>현재 위치에서 아래 방향(다음 인덱스)으로 한 칸씩 이동</strong> 하며 비어 있는 slot을 찾는 방식입니다.</p>

<p><img src="/post_images/linear_probing.png" alt="linear probing" /></p>]]></content><author><name>Cho Ha Young</name></author><category term="CS" /><category term="Data Structure" /><category term="신찬수 교수님 유튜브" /><summary type="html"><![CDATA[신찬수 교수님의 유튜브 자료구조 강의를 시청하며 강의 내용을 정리한 글입니다. 강의를 시청하면서 이해한 개념들을 하나씩 정리하고, 이후에도 계속해서 내용을 추가해 나갈 예정입니다.]]></summary></entry><entry><title type="html">ChatGPT Apps SDK 예제 실행해보기</title><link href="https://billihazero.github.io//project/2025/12/26/openai-apps-sdk-examples/" rel="alternate" type="text/html" title="ChatGPT Apps SDK 예제 실행해보기" /><published>2025-12-26T00:00:00+09:00</published><updated>2025-12-26T00:00:00+09:00</updated><id>https://billihazero.github.io//project/2025/12/26/openai-apps-sdk-examples</id><content type="html" xml:base="https://billihazero.github.io//project/2025/12/26/openai-apps-sdk-examples/"><![CDATA[<p>ChatGPT에 앱을 등록해보기 위해, <br />
공식문서에서 제공하는 예제 코드를 먼저 실행해보았습니다. <br /></p>

<p>이 글에서는 예제 코드를 실행하면서 GPT에 앱을 등록하고 실제로 동작시키기까지의 과정을 정리합니다.</p>

<h2 id="openai-apps-sdk-examples">openai-apps-sdk-examples</h2>

<blockquote>
  <p><a href="https://github.com/openai/openai-apps-sdk-examples?tab=readme-ov-file">openai-apps-sdk-examples</a> <br />
전체적인 진행 과정은 공식문서에 제공된 README를 참고하여 진행했습니다.</p>
</blockquote>

<h2 id="프로그램-세팅-과정">프로그램 세팅 과정</h2>

<p>아래는 openai-apps-sdk-examples 예제를 로컬에서 실행하기 위한 전체 흐름입니다.
<br /></p>

<ol>
  <li>코드 clone 후 의존성 설치(pnpm install)를 진행합니다.</li>
  <li>UI를 빌드합니다. (pnpm run build)</li>
  <li>assets 폴더를 정적 서버로 띄웁니다. (pnpm run serve)</li>
  <li>예제 중 pizzaz_server_node 서버로 진행하기 위해 해당 폴더로 이동하여 서버를 실행합니다.
    <ol>
      <li>cd pizzaz_server_node</li>
      <li>pnpm start</li>
    </ol>
  </li>
</ol>

<h3 id="전체-구조">전체 구조</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[ build ]
src/  ──▶ assets/        (UI 정적 파일)

[ serve ]
assets/ ──▶ :4444        (UI 서버)

[ start ]
pizzaz_server_node ──▶ :8000 (MCP 서버)

</code></pre></div></div>

<h2 id="ngrok-설정-과정">ngrok 설정 과정</h2>

<p>ngrok은 로컬 서버에서 실행중인 앱을 외부에서 접근 가능한 공개 HTTPS 주소로 만들어주기 위해 사용하는 도구입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>내 PC에서 실행 중인 app (localhost:8000)
        ↓
ngrok
        ↓
공개 HTTPS 주소 (https://xxxx.ngrok-free.dev)

</code></pre></div></div>

<p>=&gt; GPT와 로컬서버를 이어주는 중간 다리 역할을 해줍니다.</p>

<h3 id="ngrok-실행-순서">ngrok 실행 순서</h3>

<ol>
  <li>ngrok 설치
    <ol>
      <li>ngrok.exe 위치로 이동</li>
      <li>”.\ngrok.exe version” 명령어를 통해 실행여부를 확인합니다.</li>
    </ol>
  </li>
  <li>ngrok 대시보드에 로그인 한 후 토큰을 등록합니다.
    <ol>
      <li><a href="https://dashboard.ngrok.com/get-started/your-authtoken">ngrok대시보드 사이트</a></li>
      <li>”.\ngrok.exe config add-authtoken YOUR_AUTH_TOKEN “ 명령어를 통해 토큰을 등록합니다.</li>
      <li>“Authtoken saved to configuration file”이 출력되면 정상 등록이 되었습니다.</li>
      <li>”.\ngrok.exe config check” 명령어를 통해 설정을 확인할 수 있습니다.</li>
    </ol>
  </li>
  <li>ngrok으로 8000 포트 포워딩을 진행합니다.
    <ol>
      <li>”.\ngrok.exe http 8000” 명령어를 통해 포워딩을 진행합니다.
<img src="/post_images/portforwading.png" alt="ngrok terminal 화면" /></li>
      <li>Forwarding 결과 생성된 URL을 복사합니다.</li>
    </ol>
  </li>
</ol>

<h2 id="gpt-apps-등록">GPT Apps 등록</h2>

<ol>
  <li>GPT 홈페이지에 접속한 뒤 -&gt; 앱 -&gt; 설정 -&gt; 고급 설정 -&gt; 앱만들기 를 클릭합니다.
<img src="/post_images/gpt.png" alt="gpt app등록화면" /></li>
  <li>ngrok에서 생성한 URL을 “MCP 서버 URL”에 작성합니다.</li>
  <li>인증은 “인증없음”을 선택합니다.</li>
</ol>

<h2 id="실행-주의할-점">실행 (주의할 점)</h2>

<p><img src="/post_images/ui_error.png" alt="error" /></p>

<p>실행해 보니 서버 자체는 정상적으로 동작했지만, ChatGPT 화면에서는 UI가 출력되지 않는 문제가 발생했습니다.
원인을 확인해 보니, Chrome을 사용 중이고 최근 버전(142 이상) 으로 업데이트된 경우에는 보안 정책 변경으로 인해 로컬 네트워크 접근이 기본적으로 차단되어 있었습니다.
<br /></p>

<p>이로 인해 UI가 여전히 localhost 기반의 로컬 서버를 바라보고 있었고, 해당 요청이 차단되면서 UI가 정상적으로 렌더링되지 않았던 것입니다.</p>

<p><strong>비활성화 방법</strong></p>

<ol>
  <li><a href="chrome://flags/">ChromeFlagUrl</a> 접속</li>
  <li>“Local Network Access Checks” Disabled로 변경
<img src="/post_images/flags.png" alt="flag화면" /></li>
</ol>

<h2 id="결과">결과</h2>

<p><img src="/post_images/success.png" alt="app실행결과" /></p>

<p>Chrome의 “local-network-access” 관련 플래그를 disabled 처리하니, 로컬 서버 접근이 가능했고, UI가 정상적으로 출력됐습니다.</p>

<p>OpenAI의 Apps SDK는 MCP 구조를 기반으로 앱을 구성하기 때문에, 일반적인 웹 애플리케이션 개발과는 다른 관점에서 접근할 필요가 있었습니다. <br />
단순히 서버를 실행하고 화면을 띄우는 것이 아니라, ChatGPT가 어떤 방식으로 서버와 통신하고, UI를 구성하는지 <br /> <strong>실행환경에 대한 이해가 중요하다는 점</strong> 을 느꼈습니다.</p>

<p><br /></p>

<p>이번 예제를 통해 MCP 서버의 개념과 실행흐름을 정리해보았습니다.</p>

<p><img src="/post_images/mcp_architecture.png" alt="mcp architecture" /></p>]]></content><author><name>Cho Ha Young</name></author><category term="Project" /><category term="ChatGPT" /><category term="Apps-SDK" /><summary type="html"><![CDATA[ChatGPT에 앱을 등록해보기 위해, 공식문서에서 제공하는 예제 코드를 먼저 실행해보았습니다.]]></summary></entry><entry><title type="html">(회고) 깃허브 블로그를 리뉴얼 했습니다.</title><link href="https://billihazero.github.io//my/2025/12/23/First-Post/" rel="alternate" type="text/html" title="(회고) 깃허브 블로그를 리뉴얼 했습니다." /><published>2025-12-23T00:00:00+09:00</published><updated>2025-12-23T00:00:00+09:00</updated><id>https://billihazero.github.io//my/2025/12/23/First-Post</id><content type="html" xml:base="https://billihazero.github.io//my/2025/12/23/First-Post/"><![CDATA[<p><img src="/post_images/kurimanju.png" alt="" /></p>

<h2 id="리뉴얼-했습니다">리뉴얼 했습니다</h2>

<p>2024년 11월, 국비학원을 수료한 뒤 현재 회사에 재직한 지 1년이 지났습니다.
1년을 되돌아보면 회사 업무를 한다는 것에 안주하며 실력 향상, 역량 강화를 위한 학습에는 안일한 태도를 보였습니다.</p>

<p>지금이라도 늦지 않기 위해, 마음가짐부터 리뉴얼 하자는 다짐과 함께 묵혀뒀던 블로그 리뉴얼을 진행했습니다.</p>

<p>본격적으로 올해의 반성에 들어가기 전, 미미하게라도 올해 이뤄냈던 것들을 먼저 정리해봅니다.</p>

<p><br /></p>

<h3 id="회사-프로젝트-3개-완수">회사 프로젝트 3개 완수</h3>

<p>처음 입사했을 당시에는 사수 옆에서 보조 역할을 수행하며 업무를 따라가는 것만으로도 벅찼던 시기였습니다.
이후에 점차 혼자 맡아 진행하는 프로젝트들이 늘었고, 결과적으로 올해 3개의 프로젝트를 단독으로 마무리 할 수 있었습니다.</p>

<h3 id="sqld-자격증-취득">SQLD 자격증 취득</h3>

<p>올해 8월, 뭐라도 공부하자는 마음으로 부랴부랴 SQLD 자격증 시험을 신청했습니다.
사실 작년 8월에 한 번 신청했던 적이 있습니다.
그 당시에는 쿼리에 익숙하지 않던 취준생이었고, 학습하는데 매우 어려웠던 기억이 있었습니다.
그래도 입사 후 쿼리를 만져보게 되면서,학습 과정에서 쿼리를 해석하는데 큰 어려움은 없었습니다.
2주 정도의 짧은 학습시간이었지만 무난하게 합격하였습니다.(사실 문닫고 들어왔어요)</p>

<h3 id="토스-러너스-하이-2기-합격">토스 러너스 하이 2기 합격</h3>

<p>마음 가짐을 바꾸게 된 결정적인 계기입니다.</p>

<p>앞서 얘기한 것 처럼, 지난 1년동안 스스로 역량을 충분히 키우지 못했다는 생각에 자신감이 바닥을 치던 상태였습니다.
그 때 우연히 토스에서 러너스 하이라는 교육 프로그램을 진행한다는 글을 보게 되었습니다.</p>

<p>지원 요건 중 하나가 경력기술서 제출이었는데, 1년동안의 경험을 처음으로 정리해보았습니다.
또한, 소정의 사비를 들여 멘토링을 진행하며 부족한 부분을 채우고 수정했습니다.</p>

<p>비록 토스에 합격한 것은 아니지만,
어딘가에는 합격했다 는 사실만으로도 큰 의미가 있었습니다.
1년동안 했던 것들이 무의미하지는 않았다는 안도감과, 노력하면 할 수 있구나라는 자신감을 얻게 되었습니다.</p>

<p><br /></p>

<h2 id="반성">반성</h2>

<h3 id="퇴근-후-자기계발-미흡">퇴근 후 자기계발 미흡</h3>

<p>퇴근 후의 시간은 대부분 쉬는 데 사용했습니다.
하루의 업무를 마쳤다는 이유로 저녁을 먹고나선 게으름의 연속이었습니다.</p>

<p>이러한 패턴을 바꾸기 위해,
퇴근 후의 <strong>학습시간을 확보</strong>하려 합니다.
운동을 하러가는 화,목을 제외하고는 퇴근 후 곧바로 <strong>도서관으로 이동해 학습한 뒤
공부한 내용을 반드시 커밋하는 것을 목표로 삼을 예정입니다.</strong></p>

<p>도서관에는 자기계발에 열정적인 사람들이 모여 있습니다. (중국어 공부하는 할아버지, 의학서적 읽는 어머니 등등…)
환경의 힘을 빌려 동기부여를 얻고, 꾸준히 학습하고자 합니다.</p>

<h3 id="의미-없는-강의-내용-옮겨적기">의미 없는 강의 내용 옮겨적기</h3>

<p>과거에 운영했던 블로그는 강의를 들으며 내용을 그대로 옮겨적는 글을 주로 작성했습니다.
리뉴얼 전 글을 읽어보았을 때 그 글들은 기록보다는 받아쓰기에 가까웠습니다.</p>

<p>보여주는 것을 목적으로 하는것이 아닌 배우고 성장하기 위해 작성하고자 합니다.</p>

<h2 id="목표">목표</h2>

<h3 id="1일-1커밋">1일 1커밋</h3>

<p>과거에 한 때 유행했던 1일 1커밋을 실천해보고자 합니다.
처음에는 이미 많은 사람들이 커밋 수 관리 자체는 의미가 없고, 결국 중요한 건 실력이라는 생각을 했습니다.
또, 1커밋에 집착하다 보면 오히려 의미 없는 커밋을 반복하지 않을까 하는 걱정도 있었습니다.</p>

<p>그렇지만,,,아무런 실천을 하지 않으면 아무것도 변하지 않는다는 생각이 들었습니다.</p>

<p>뭐라도 하고 보자 라는 마음으로 당분간은,
<strong>꾸준히 커밋하는 습관</strong>을 만들어 보고자 합니다.</p>

<hr />

<h3 id="마무리">마무리</h3>

<p>뭐라도 시작하자.
26년에는 부지런하게 발전하도록 하겠습니다.</p>]]></content><author><name>Cho Ha Young</name></author><category term="My" /><category term="Diary" /><summary type="html"><![CDATA[]]></summary></entry></feed>