서비스 학습
SERVICE STUDY · 2026-09-06

Gemma 서비스가 만들어지는
과정을 코드와 함께 이해한다

실제 fe/be/ 구조를 따라가며 왜 파일이 필요한지, 데이터가 어떻게 흐르는지, 실행 결과가 무엇을 증명하는지 기록하는 오프라인 학습 사이트다.

React FastAPI Docker Compose TurboFieldfare Apple Pencil notes
Start here

먼저 연결도를 보면 전체가 한눈에 들어온다

파일 14개와 그 사이를 요청이 옮겨 가는 25개의 경로를 한 장에 그린 지도다. 범례에서 GET /healthz·POST /api/v1/chat·실패 같은 흐름을 고르면 그 경로만 남고, 선마다 언제 그 이동이 일어나는지가 나타난다.

요청 흐름 연결도 열기 →

Mental model

먼저 “서비스”와 “모델”을 분리해서 생각한다

LLM을 웹에서 사용한다고 해서 React가 Gemma의 가중치를 직접 읽는 것은 아니다. 사용자가 보는 채팅 화면, 요청을 통제하는 API 서버, 실제 행렬 연산을 수행하는 추론 서버는 서로 다른 책임을 가진 프로그램이다. 세 프로그램을 분리하면 한 부분을 교체해도 나머지 부분을 최대한 유지할 수 있다.

Presentation
Frontend

텍스트 입력, 메시지 목록, 로딩 표시처럼 사용자가 보고 조작하는 상태를 관리한다.

Application
Backend

요청 형식을 검사하고, 동시 접속과 오류를 통제하며, 추론 결과를 브라우저에 중계한다.

Inference
TurboFieldfare

토큰을 숫자로 바꾸고 Gemma 4를 실행해 다음 토큰의 확률을 계산한다.

왜 브라우저가 TurboFieldfare에 바로 요청하지 않나?

학습용 단일 사용자 환경에서는 직접 호출도 가능하다. 그러나 서비스가 되면 인증, 입력 길이 제한, 요청 대기열, 대화 저장, RAG와 MCP, 모델 교체 같은 정책이 필요해진다. 접근 권한의 최종 판단을 React에만 두면 사용자가 JavaScript 검사나 UI 제한을 우회해 API를 직접 호출할 수 있고, 모바일 앱 등 다른 client가 생길 때 같은 코드를 다시 만들어야 한다. FastAPI를 중간 경계로 두면 모든 client가 하나의 규칙을 공유할 수 있다.

Target architecture · 연결 일부 미구현

Container는 웹 서비스를, macOS는 Metal 추론을 담당한다

아래는 목표 연결 구조다. 3단계에서 BE의 채팅 route까지 완성했고 실제 Gemma 생성을 POST /api/v1/chat으로 확인했다. 남은 것은 FE의 전송을 실제 요청으로 바꾸는 일, token streaming, 그리고 Compose 통합이다. FE의 전송 버튼은 현재도 사용자 문장을 화면에 추가하는 UI 동작이다.

React FE

화면과 사용자 입력

FastAPI BE

요청과 streaming 중계

TurboFieldfare

macOS · Metal · Gemma 4

경계를 나눈 이유: FE와 BE는 Linux container에서 재현 가능하게 실행하지만, TurboFieldfare는 Apple Metal에 직접 접근해야 하므로 macOS host에서 실행한다.
Completed unit 01

Frontend production container

Image build 성공

local-moe-fe:study

Container health healthy

/healthz

Home HTTP 200

/

Image size 26.0 MB

26,017,241 bytes

Dockerfile이 이 결과를 만드는 전체 과정 읽기 →

Backend step 03 · 채팅 API

Backend inference adapter와 채팅 route

Automated tests76 passed

설정·오류·입력 상한·실제 TCP·수명

ReadinessHTTP 200

/readyz

초기 Live inferenceREADY.

Host와 Docker

초기 실제 연결연결 성공

host.docker.internal

생성·상태 확인 연결을 분리하고 오류 분류, 실제 시간 초과·취소 테스트, 서비스 문맥 용어 설명을 보완했다. 초기 Gemma 생성 기록과 이번 회귀 검증은 구분해 기록했다.

범용 계약과 TurboFieldfare Adapter가 데이터를 번역하는 과정 읽기 →

Target flow · 일부 미구현

완성 후 질문 한 문장이 답변 토큰으로 돌아오는 과정

사용자가 전송 버튼을 누른다

React는 입력 문자열과 이전 대화를 하나의 요청 객체로 만들고 화면에는 사용자 메시지를 즉시 추가한다.

FastAPI가 요청의 경계를 검사한다

빈 질문과 입력 형식·접근 권한 같은 서비스 정책을 검사한 뒤 TurboFieldfare 형식으로 변환한다. 정확한 context 길이는 tokenizer·chat template에 좌우되므로 현재는 Swift 서버가 검증하고 Adapter가 문맥 초과 오류를 전달한다. FastAPI에 동일 tokenizer가 구현됐다는 뜻은 아니다.

TurboFieldfare가 Gemma 4를 실행한다

prompt를 token ID로 변환하고 prefill을 수행한 뒤 decode loop에서 다음 token을 하나씩 생성한다.

생성 중인 조각을 streaming한다

완성된 답변을 한 번에 기다리지 않고 token 조각을 FastAPI와 React로 전달한다. React는 같은 assistant message의 내용을 계속 이어 붙인다.

핵심 관찰: 모델의 생성 속도와 웹 화면의 반응성은 같은 문제가 아니다. 추론이 빨라도 streaming 처리가 막히면 화면은 멈춘 것처럼 보이고, 반대로 첫 token을 빨리 표시하면 전체 생성 시간이 같아도 사용자는 더 빠르다고 느낀다.
현재 구현 경계: Browser가 부를 수 있는 채팅 route까지 완료했고 비스트리밍 실제 생성을 확인했다. 다만 token streaming이 없어 답변이 완성될 때까지 아무것도 보이지 않는다. 기본값 128 토큰에서도 응답에 약 29초가 걸렸고, 실용적인 길이로 올리면 몇 분이 된다. React 연결과 streaming은 아직이므로 위 그림의 마지막 두 칸은 목표 흐름이다.
Learning path

이 문서를 읽는 순서

  1. 요청 흐름 연결도에서 파일 사이를 요청이 어떻게 옮겨 다니는지 전체 지도를 먼저 본다.
  2. Frontend 구조에서 React source가 브라우저 화면이 되는 큰 흐름을 잡는다.
  3. Dockerfile에서 개발 도구와 production 실행 환경을 왜 분리하는지 확인한다.
  4. Nginx에서 URL 하나가 어떤 파일과 HTTP 응답으로 바뀌는지 추적한다.
  5. Backend 2단계에서 범용 inference 계약, TurboFieldfare Adapter, readiness를 이해한다.
  6. base.pyturbofieldfare.py를 비교해 내부 데이터와 wire JSON의 경계를 추적한다.
  7. 다음 단계에서는 이 Adapter를 사용하는 저장 없는 단일 채팅 API를 만든다. Streaming은 그 다음 단계에서 별도로 검증한다.

각 문서는 실제 source file 하나를 기준으로 작성한다. 따라서 개념 설명을 읽은 직후 실제 코드의 어느 줄이 그 개념을 구현하는지 확인할 수 있다.

다음 →Frontend 구조