블로그로 돌아가기

UnivMind 개발기 — 대학생을 위한 AI 학습 플랫폼을 만들기까지

2026-03-27develop

UnivMind 개발기

안녕하세요, Sungblab입니다! 이번 글에서는 제가 만들고 있는 UnivMind라는 서비스의 개발 이야기를 해보려고 합니다. 대학생을 위한 AI 기반 학습·지식 관리 플랫폼인데, 지금까지 만든 것 중 가장 큰 프로젝트이기도 하고, 기술적으로도 제가 가장 많이 성장한 프로젝트라서 꼭 한번 정리해보고 싶었습니다.

왜 만들게 되었나

대학교에 오니까 자료 관리가 진짜 문제였습니다. 교수님마다 PDF를 주시는 분, 판서만 하시는 분, 유튜브 링크를 던져주시는 분... 한 학기만 지나도 강의 자료가 여기저기 흩어져 있더라고요. 노션에 정리해보기도 하고, 옵시디언도 써봤는데, 결국 정리는 사람이 해야 하니까 시간이 지나면 또 쌓이기만 합니다.

근데 진짜 답답했던 건 시험 기간이었습니다. "이거 어디서 봤는데..." 하면서 폴더를 뒤지는데, 분명 경영학에서 배운 '한계비용'이 경제학에서도 나왔거든요. 근데 그 연결은 제 머릿속에만 있고, 노트에는 각각 따로 적혀 있어요. 이걸 AI가 자동으로 연결해주면 어떨까? 이게 시작이었습니다.

처음부터 이렇게 크진 않았다

솔직히 처음엔 그냥 "PDF 올리면 AI가 요약해주는 서비스" 정도를 생각했습니다. 근데 만들다 보니까 요약만으로는 부족하더라고요. 요약본을 또 정리해야 하니까요. 그래서 점점 기능이 붙기 시작했습니다.

처음에는 courses(과목)라는 개념으로 시작했다가, 더 유연한 구조가 필요해서 folders(폴더)로 마이그레이션했고, 개념을 별도 테이블(concepts, relations)로 관리하다가 옵시디언처럼 모든 걸 노트로 통합하는 구조로 다시 리팩토링했습니다. 거의 두세 번은 갈아엎은 것 같아요.

지금 돌이켜보면 이 과정이 가장 중요했습니다. 처음부터 완벽한 구조를 짜려고 했으면 아마 시작도 못 했을 거예요.

기술 스택

왜 이 스택인가

제가 고등학교 때부터 Next.js를 써왔기 때문에, 프론트엔드는 자연스럽게 Next.js 14로 갔습니다. 풀스택 프레임워크라 API Routes + Server Actions로 백엔드까지 한 번에 처리할 수 있다는 게 1인 개발에서 큰 장점이었습니다.

  • 프론트엔드: Next.js 14, React 18, Tailwind CSS, Zustand
  • 에디터: TipTap (리치텍스트), Monaco (코드)
  • 백엔드: Next.js API Routes + Server Actions, Fastify 5 (워커)
  • 데이터베이스: PostgreSQL (Supabase) + Drizzle ORM, pgvector
  • AI: Google Gemini API, LangChain + LangGraph
  • 인증: Better Auth (이메일 + Google OAuth)
  • 스토리지: Cloudflare R2
  • 잡 큐: BullMQ + Redis
  • 데스크톱: Tauri 2 (Rust)
  • 배포: Vercel (웹), Railway (워커)

AI 모델로 Gemini를 선택한 이유가 있는데, 일단 가격이 압도적으로 쌉니다. 대학생 대상 서비스라 가격을 낮게 잡아야 하는데, OpenAI 기준으로 계산하면 도저히 수지타산이 안 맞더라고요. Gemini Flash가 성능 대비 가격이 제일 좋았고, 멀티키 로테이션을 구현해서 레이트리밋도 분산시켰습니다.

모노레포 구조

프로젝트가 커지면서 모노레포로 전환했습니다. Turborepo + pnpm 워크스페이스로 관리하고 있고, 구조는 이렇습니다:

apps/
  web/        — Next.js 메인 웹앱
  worker/     — Fastify + BullMQ 백그라운드 워커
  desktop/    — Tauri 2 데스크톱앱
packages/
  db/         — Drizzle ORM + PostgreSQL 스키마
  ai/         — LangGraph 에이전트 + Gemini API
  jobs/       — 잡 타입, 파이프라인 결과
  ui/         — shadcn/ui 공유 컴포넌트
  config/     — ESLint, TypeScript 설정

packages/db에 스키마를 한 곳에 모아두니까 웹앱이든 워커든 같은 타입과 쿼리 빌더를 공유할 수 있어서 좋았습니다. AI 관련 코드도 packages/ai로 분리해서 웹에서든 워커에서든 같은 에이전트를 호출할 수 있게 했습니다.

핵심 기능: 자동 지식 그래프

이게 UnivMind의 가장 차별화되는 기능입니다.

인제스트 파이프라인

사용자가 PDF, 녹음 파일, 이미지 등을 업로드하면, LangGraph로 만든 인제스트 파이프라인이 자동으로 처리합니다:

  1. 분류 — 파일 타입 판별 (텍스트/PDF/오디오/이미지)
  2. 추출 — OCR, STT(음성인식), 텍스트 파싱
  3. 분석 — AI가 내용 분석, 요약 생성
  4. 개념 추출 — 핵심 개념들을 뽑아내서 개념 노트로 생성
  5. 관계 발견 — 개념들 사이의 관계(선행, 인과, 대조 등)를 자동으로 연결

이게 BullMQ 잡 큐로 비동기 처리되기 때문에, 사용자는 파일 올리고 다른 작업을 하면 됩니다. 처리 완료되면 WebSocket으로 실시간 알림이 옵니다.

옵시디언 스타일 지식 시스템

처음에는 concepts, relations라는 별도 테이블을 만들었는데, 이러니까 노트와 개념이 이원화돼서 복잡해졌습니다. 고민하다가 옵시디언의 접근 방식을 따르기로 했습니다.

  • 개념도 그냥 노트다 (source_type = 'concept')
  • MOC(Map of Content)도 노트다 (source_type = 'moc')
  • 관계는 note_links 테이블 하나로 통합
  • [[위키링크]] 문법 지원

TipTap 에디터에서 [[를 입력하면 노트 제목 자동완성이 뜨고, 저장하면 note_links에 wikilink 타입으로 동기화됩니다. AI가 추출한 관계도 같은 테이블에 들어가니까, 사용자가 직접 만든 연결과 AI가 발견한 연결이 하나의 그래프에 공존합니다.

Graph RAG — 가장 자랑하고 싶은 것

일반 RAG(Retrieval-Augmented Generation)는 질문과 비슷한 텍스트 조각을 벡터 검색으로 찾아오는 겁니다. 근데 이것만으로는 한계가 있어요. "미분방정식과 열전달의 관계가 뭐야?"라고 물으면, 두 개념이 각각 다른 노트에 있으니까 벡터 검색만으로는 연결을 못 찾거든요.

그래서 Graph RAG를 직접 구현했습니다. Neo4j 같은 별도 그래프 DB 없이, PostgreSQL + 인메모리 BFS로 만들었습니다.

3-Phase 검색 파이프라인

Phase 1 (병렬 실행):
  ├─ Vector Search  — pgvector 코사인 유사도 (의미 검색)
  ├─ BM25 Search    — PostgreSQL tsvector (키워드 검색)
  └─ Graph Topology — 개념 노트 + 관계를 인메모리 adjacency list로 로드

Phase 2 (인메모리, <1ms):
  └─ Graph Search   — vector 결과를 진입점으로 → BFS 탐색

Phase 3:
  └─ Merge          — 질문 유형별 가중치로 세 결과 병합

Phase 1에서 세 가지 검색을 Promise.all로 동시에 돌립니다. 그래프 토폴로지는 5분간 캐싱해서 반복 호출 시 DB를 안 칩니다.

Phase 2가 핵심인데, 벡터 검색으로 찾은 노트에 연결된 개념들을 시작점으로 삼아서, 관계 타입별 가중치를 곱하면서 BFS로 탐색합니다. 예를 들어 prerequisite(선행 관계) 가중치는 0.9, related_to는 0.5 이런 식으로요. 이렇게 하면 직접 매칭되지 않는 연관 지식까지 끌어올 수 있습니다.

질문 유형별 적응

재밌는 건, 질문 유형에 따라 검색 전략이 바뀐다는 겁니다:

| 질문 유형 | Vector | BM25 | Graph | BFS 깊이 | |-----------|--------|------|-------|----------| | 사실형 | 65% | 25% | 10% | 1홉 | | 비교형 | 30% | 15% | 55% | 2홉 | | 심층형 | 35% | 15% | 50% | 2홉 |

"OOP가 뭐야?"같은 단순 질문은 벡터 검색 위주로, "절차지향과 객체지향의 차이"같은 비교 질문은 그래프 탐색 비중을 높여서 관련 개념들의 관계까지 찾아옵니다. 비교 질문에서는 contrasts(대조) 관계의 가중치를 0.75에서 0.95로 부스트하는 식으로요.

AI 에이전트 — LangGraph로 7개

AI 기능은 전부 LangGraph 에이전트로 구현했습니다. 단순 프롬프트 체이닝이 아니라 상태 기반 그래프로 에이전트를 만들었는데, 복잡한 멀티스텝 작업에서 확실히 안정적입니다.

  1. 인제스트 파이프라인 — 문서 업로드 시 자동 처리 (가장 큰 에이전트)
  2. QA 에이전트 — Graph RAG 기반 Q&A, 인용 포함
  3. 스터디 코치 — 설명/퀴즈/토론 모드
  4. 강의 모드 — 실시간 강의 처리
  5. 주간 다이제스트 — 한 주간 학습 요약
  6. 교차 도메인 — 다른 과목 간 연결 발견
  7. 딥 리서치 — 멀티스텝 심층 조사

AI 채팅에서는 Gemini의 Combined Tool Use를 활용해서, Google Search(웹 검색), Code Execution(파이썬 실행), URL Context(URL 크롤링)를 토글로 켜고 끌 수 있게 했습니다. 학습 도우미 모드도 있어서 퀴즈 생성, 플래시카드, 시험 대비, 교수님 메일 초안 같은 기능도 AI 채팅 안에서 바로 사용할 수 있습니다.

Gemini API 멀티키 로테이션

1인 개발 서비스에서 AI API 비용은 생존 문제입니다. Gemini API 키를 여러 개 발급받아서 콤마로 구분해 환경변수에 넣으면, 자동으로 로테이션하면서 레이트리밋을 분산시키는 구조를 만들었습니다. 한 키가 리밋에 걸리면 60초 쿨다운 후 다음 키로 넘어가고, 모든 요청의 토큰 사용량과 비용을 DB에 기록해서 사용량 관리도 합니다.

데스크톱 앱 — Tauri 2

웹만으로는 부족한 기능이 있었습니다. 바로 로컬 음성인식이었는데, 강의 중에 실시간으로 음성을 텍스트로 변환하려면 API를 계속 쏘는 건 비용도 문제고 지연도 심하거든요.

그래서 Tauri 2로 데스크톱 앱을 만들었습니다. Electron 대신 Tauri를 선택한 이유는 번들 크기가 압도적으로 작고, Rust 기반이라 성능도 좋기 때문입니다.

  • 로컬 STT: Whisper.cpp + Sherpa-ONNX로 오프라인 음성인식
  • 빠른 캡처: 핫키 누르면 즉시 노트 생성
  • 스크린캡처: 화면 캡처해서 바로 노트에 붙이기
  • 트레이 앱: 백그라운드에서 상주하면서 필요할 때 빠르게 접근

CI/CD도 GitHub Actions로 desktop-v* 태그 푸시하면 자동으로 Windows 빌드 → R2 업로드 → GitHub Release가 됩니다.

데이터베이스 — 39개 테이블

Supabase PostgreSQL + Drizzle ORM을 쓰고 있는데, 하나 중요한 게 있습니다. Supabase를 쓰지만 RLS(Row Level Security)를 사용하지 않습니다. Drizzle ORM으로 쿼리를 짜기 때문에, 모든 쿼리에 userId 필터를 직접 넣어야 합니다. 이걸 빠뜨리면 다른 사용자의 데이터가 노출되니까, 개발할 때 가장 신경 쓰는 부분입니다.

테이블은 8개 도메인으로 나뉘어 있고, 구독 시스템도 있습니다. Free / Pro(₩9,900) / Max(₩19,900) 세 플랜인데, 무료에서도 핵심 기능은 다 쓸 수 있고 AI 사용량 한도만 다릅니다.

실시간 — WebSocket + Redis

파일 인제스트 같은 무거운 작업은 워커에서 처리되는데, 처리 상태를 실시간으로 사용자에게 알려줘야 합니다. 이걸 Redis pub/sub + WebSocket으로 해결했습니다.

워커에서 잡이 완료되면 Redis로 이벤트를 발행하고, 웹앱의 WebSocket 서버가 이걸 받아서 해당 사용자에게 푸시합니다. 클라이언트 쪽에서는 자동 재연결 로직도 넣어서 네트워크가 끊겨도 다시 붙습니다. WebSocket 인증은 HMAC 토큰으로 처리합니다.

삽질기

Drizzle ORM과 pgvector

pgvector 768차원 벡터를 Drizzle로 다루는 게 쉽지 않았습니다. Drizzle이 vector 타입을 네이티브로 지원하지 않아서, sql 템플릿 리터럴로 직접 쿼리를 짜야 했습니다. 특히 코사인 유사도 계산(<=> 연산자)이나 벡터 리터럴 캐스팅 부분에서 삽질을 꽤 했습니다.

concepts 테이블 통합

아까 말했듯이 concepts, relations, concept_notes 세 개의 테이블을 따로 두었다가, 전부 notes + note_links로 통합하는 대규모 마이그레이션을 했습니다. 기존 데이터를 옮기면서 관계가 꼬이지 않게 하는 게 진짜 까다로웠고, 이 과정에서 옵시디언의 "모든 것은 노트다"라는 철학이 왜 좋은지 체감했습니다.

토폴로지 캐싱

Graph RAG에서 매 검색마다 DB에서 그래프를 다시 불러오면 느려서, 인메모리 캐싱을 넣었는데 캐시 무효화 타이밍이 애매했습니다. 너무 짧으면 캐시 효과가 없고, 너무 길면 새로 인제스트한 노트가 검색에 안 잡히거든요. 결국 5분 TTL + 인제스트 완료 시 수동 무효화로 해결했습니다.

현재 상태와 앞으로

지금은 MVP를 넘어서 실사용 가능한 수준까지 왔습니다. 웹앱, 워커, 데스크톱앱 모두 동작하고, 서버 액션만 50개 이상, AI 에이전트 7개, 워커 6개가 돌아가고 있습니다.

앞으로 하고 싶은 것들:

  • Deep Research 고도화 — 멀티스텝 리서치 에이전트 개선
  • Quick Capture — 데스크톱 앱에서 핫키로 즉시 캡처 → 노트 생성
  • CAG(Cache-Augmented Generation) — Gemini 컨텍스트 캐싱 활용
  • 모바일 앱 — React Native나 Flutter로

마무리

고등학교 때 HTML로 사이트 만들던 게 엊그제 같은데, 어느새 모노레포에 39개 테이블, 7개 AI 에이전트를 돌리는 서비스를 만들고 있네요. 기술적으로 가장 뿌듯한 건 Graph RAG를 별도 그래프 DB 없이 PostgreSQL만으로 구현한 것이고, 서비스적으로 가장 뿌듯한 건 "자료 올리면 AI가 알아서 연결해준다"는 경험을 실제로 만들어냈다는 겁니다.

아직 갈 길이 멀지만, 이 글을 읽는 분들에게 조금이나마 영감이 되었으면 좋겠습니다. 궁금한 점이 있으시면 편하게 연락주세요!

Ssungblab

Building the future with AI, one project at a time.

© 2026 Sungblab. All rights reserved.Made with ❤️ & AI