const developer = {
name: "이해람 (Haeram Lee)",
role: "Frontend Lead",
experience: "6+ years",
philosophy: "사람을 위한 프로덕트를 만듭니다",
};6년 차, 사람을 위한 프로덕트를 만드는 프론트엔드 개발자입니다.
저의 모든 실행 동기는 사람에 대한 관심에서 비롯됩니다.
함께 일하는 동료와 실제 사용자의 경험을 개선하는 것에 집중합니다.
"개발도 사람이 하는 일, 프로덕트는 사용자를 위한 것"
- 🤝 Developer Experience: 팀원들의 생산성과 행복을 위한 개발 문화 조성
- 👥 User Experience: 현장 리서치를 통한 실사용자 중심의 제품 개선
- 🏗️ Architecture: 지속 가능하고 확장 가능한 코드베이스 설계
더 자세한 내용이 궁금하시다면 포트폴리오를 확인해보세요!
개인적으로 진행 중인 사이드 프로젝트입니다.
Tech Stack: React 19 · React Native · Three.js (WebGPU/TSL) · Go 1.26 · Connect-RPC · PostgreSQL + pgvector · Claude API
📝 프로젝트 컨셉
일기를 쓰면 AI가 그 경험을 별로 띄웁니다. 별은 사람·장소·주제 같은 작은 조각들로 되어 있고, 같은 조각을 가진 별끼리는 선으로 이어집니다. 같이 떠올린 기억은 선이 굵어지고, 오래 안 떠올린 별은 어두워집니다. 그래도 사라지지는 않습니다.
통계도 조언도 없습니다. 내 기억이 만든 우주를 떠다니며 지난 삶을 돌아보는 3D 일기입니다.
기억을 자료구조로 옮기기
- 🌟 기억의 별 (
EpisodicMemory) — 일기 한 편이 여러 기억으로 쪼개지고, 그 하나하나가 별이 됩니다 - ✨ 조각 (
Neuron) — 별을 이루는 재료입니다. 주제 · 장소 · 사람 세 종류이고, 여러 별이 같은 조각을 함께 씁니다 - 🔗 선 (
Synapse) — 선은 조각과 조각 사이에만 놓입니다. 별과 별을 바로 잇는 선은 아예 없습니다 - 🌠 별자리와 성운 — 만들어서 저장하지 않습니다. 조각을 많이 나눠 쓰는 별들이 가까이 모이면 별자리처럼 보이고, 옆에 있는 별빛 색이 섞이면 성운처럼 보입니다
💡 제일 오래 붙잡고 있었던 건 "무엇을 저장하지 않을까"였습니다. 별자리에 테이블을 하나 주는 순간, 없애려고 했던 별↔별 연결이 슬그머니 되살아납니다.
뇌를 따라간 규칙들
- 같이 떠오른 조각끼리는 연결이 세지고, 안 쓰인 연결은 약해집니다. 어느 쪽이든 연결을 지우지는 않습니다
- 기억을 떠올릴 때마다 문장이 조금씩 바뀝니다. 대신 사용자가 쓴 원본 일기는 손대지 않습니다
- 잊는 건 지우는 게 아닙니다. 끝까지 어두워지기만 하고 0이 되지 않습니다
- 기억이 요약되는 것과 흐려지는 것은 따로 갑니다. 뜻만 남았는데 밝은 별도, 문장은 남았는데 어두운 별도 있습니다
- 잠든 사이에는 모든 연결을 조금씩 같이 낮춥니다. 안 쓰여서 약해지는 것과는 다른 방식입니다
규칙을 사람이 아니라 CI가 지키게
이름은 하나만 씁니다
Go 타입 · DB 테이블 · proto · 프론트 도메인이 모두 같은 이름을 씁니다. 한 가지에 이름 하나입니다.
그 목록을 spec/ubiquitous-language.md에 적어두고, 직접 만든 린터가 어긋난 이름을 CI에서 잡습니다.
화면용 단어(star · filament · nebula)는 프론트 안에서만 씁니다. 도메인이나 DB로 넘어오면 린트가 막습니다.
잘 지키자고 약속하는 대신, 못 하게 만들었습니다.
| 검사 | 막는 것 |
|---|---|
lint:language |
같은 걸 다른 이름으로 부르거나, 화면 단어를 도메인에 쓰기 |
lint:fsd+boundaries |
아래 레이어가 위 레이어를 가져다 쓰기 |
lint:persistence |
도메인 코드에 SQL이나 DB 타입이 새어 들어오기 |
lint:raw-strings |
화면 문장을 코드에 그대로 박아서 번역을 건너뛰기 |
lint:comments |
"원래는 ~였다"처럼 시간이 지나면 틀려지는 주석 |
린터마다 일부러 틀린 코드를 하나씩 두고, 그걸 제대로 잡는지 같이 확인합니다. 검사기가 조용히 고장 난 채로 초록불만 켜져 있는 게 제일 위험합니다.
한 군데만 고치면 나머지가 따라옵니다
proto/하나에서 Go 서버 코드와 TS 클라이언트 코드가 같이 나옵니다- 숫자 상수는
spec/values.yaml한 파일에만 두고, 거기서 TS와 Go 파일을 만들어 냅니다 - 기억 계산식은 서버(Go)와 브라우저 데모(TS) 양쪽에 있어서, 같은 입력에 같은 값이 나오는지 테스트로 묶어 둡니다
3D 화면은 웹과 앱이 같은 코드입니다
@cosimosi/3d-renderer 하나를 웹(WebGPU)과 React Native가 그대로 씁니다.
셰이더도 문자열이 아니라 TypeScript로 써서 타입 검사를 받습니다.
패키지 19개 — 두 번 이상 쓰이는 걸 확인한 다음에 공용으로 내립니다. 미리 빈 패키지를 만들어 두지 않습니다.
문서를 먼저 쓰고 코드를 씁니다
무엇을 왜 어떻게 만드는지가 전부 spec/에 있습니다. 코드와 문서가 어긋나면 문서가 맞습니다.
- 📚 문서 256편, 약 59,000줄
- 🔄 기획 문서 → 작업 문서 → 구현 순서로 가고, 다 만들면 결과를 기획 문서에 되돌려 적습니다
- ✅ 끝낸 작업 99편. 통과 기준을 다 맞추고 코드 리뷰를 넘겨야 닫힙니다
- 🤖 규칙을 문서에 못 박아 두면, 사람이 쓰든 AI 에이전트가 쓰든 같은 규칙으로 코드가 나옵니다
하면서 알게 된 것
- 규칙을 문서에만 적어두면 언젠가 어긋납니다. 검사기로 만들어야 남습니다
- 어려운 건 3D도 뇌과학도 아니었고, 무엇을 데이터로 만들지 않을지 고르는 일이었습니다
- 서버와 프론트, 웹과 앱이 같은 단어를 쓰면 회의도 코드 리뷰도 짧아집니다
Tech Stack: React 19 · TypeScript · Tailwind v4 · TanStack Router/Query · FSD · Go 1.26 · Connect-RPC · SQLite · OpenRouter · Cloudflare Workers
📝 프로젝트 컨셉
사진 몇 장과 "무슨 일이 있었는지" 메모를 넣으면, 내 말투로 쓰인 블로그 초안이 나옵니다. 이름은
post+autopilot— 포스팅의 자동조종 장치입니다.결과는 하나의 블록 배열로 저장되고, 거기서 네이버 · 티스토리 · 독립 사이트 HTML · Markdown 네 가지 형식이 브라우저에서 바로 만들어집니다.
"AI가 알아서 다 해줍니다"가 아닙니다. 어떤 모델로 언제 돌릴지는 사용자가 고릅니다.
보는 일과 쓰는 일을 나눴습니다
- 👀 관찰 → 글쓰기 순서 — 사진을 보는 모델과 글을 쓰는 모델을 따로 고릅니다. 이미 뽑아 둔 관찰 결과는 재사용해서, 같은 사진을 두 번 보지 않습니다
- 🧱 결과는 HTML이 아니라 블록 배열 — 텍스트 · 제목 · 인용 · 목록 · 이미지 다섯 종류만 통과시킵니다. 모델이 무슨 태그를 뱉든 서버에서 걸러집니다
- ✏️ AI 수정은 요청한 문장만 — 수정 작업도 전체 블록을 되돌려주지만, 건드려 달라고 하지 않은 문장은 그대로 두는 게 계약입니다
- ⚔️ A/B는 진짜 블라인드 — 판정 전에는 모델 이름을 브라우저에도 보내지 않습니다. 결과는 단계별 Elo와 승패 수치로 쌓입니다
- 🤫 페이지를 열었다는 이유로 모델을 부르지 않습니다 — 목록을 읽거나 화면을 여는 행위에는 AI 호출이 없습니다
말투 · 템플릿 · 지침, 세 층의 책임이 다릅니다
| 층 | 맡는 것 |
|---|---|
| 말투(Voice) | 문장이 어떻게 들리는지 (어조) |
| 템플릿(Template) | 글이 어떤 모양인지 (구조) |
| 지침(Guideline) | 무엇을 피하고 조심할지 (내용) |
- 🔒 말투는 서로 격리됩니다 — 한 계정이 말투를 여러 개 가질 수 있고, 어떤 말투도 다른 말투의 자료를 물려받지 않습니다
- 🎓 학습은 확정과 별개의 선택입니다 — 글을 확정했다고 자동으로 말투에 먹이지 않습니다. 명시적으로 골라야 학습됩니다
- 🧩 템플릿 문법 파서가 두 개 — Go와 TypeScript 양쪽에 있고, 같은 테스트 자료로 판정이 갈리지 않는지 묶어 둡니다
- 🌏 목표 언어가 세 층보다 셉니다 — 안에 상충하는 언어 지시가 적혀 있어도 목표 언어를 따릅니다. 대신 언어를 감지하거나 번역하지는 않습니다
사진은 브라우저에서 끝냅니다
- 📱 HEIC · HEIF는 Web Worker에서
libheif-js(WASM)로 풀고, JPEG · PNG · WebP는 브라우저 디코더를 씁니다 - 📐 긴 변 1,024px로 줄이고 JPEG 품질 0.85로 다시 인코딩해서 R2로 직접 올립니다. 원본은 서버에 도착하지 않습니다
- 🧹 이 과정에서 휴대전화가 넣어 둔 위치 · 기기 · 촬영 시각 메타데이터가 함께 사라집니다
- 🐳 Go로 HEIC를 풀려면 사실상 cgo가 필요합니다. 이미지 처리를 프론트로 밀어 둔 덕분에 백엔드 이미지는 CGO 없는 distroless 정적 바이너리로 남습니다
네이버 발행은 서버가 아니라 내 Mac이 합니다
- 🔑 VPS는 네이버 쿠키도 비밀번호도 받지 않습니다. 페어링된 Mac이 전용 Chromium 프로필과 macOS Keychain의 기기 토큰으로 작업을 가져갑니다
- 📤 서버는 발행 작업의 진실만 갖고, Mac은 그걸 빌려 실행하는 쪽입니다. 밖으로 나가는 Connect 호출만 하고 공개 포트를 열지 않습니다
- ☝️ 마지막 버튼은 정확히 한 번 — 제목 · 블록 · 사진과 캡션 순서 · 태그 · 카테고리 · 공개 범위를 먼저 검증하고, 서버가
committing상태를 영구 저장했다고 확인한 뒤에 누릅니다 - ❓ 그 뒤 결과를 확인하지 못하면 자동 재시도하지 않고
outcome_unknown으로 남깁니다. 중복 발행을 조용히 숨기지 않으려고 일부러 그렇게 뒀습니다 - 🚧 서버 · 프론트 · 발행 큐 · Mac 동반 앱의 기반은 끝났고, 네이버 편집기를 다루는 드라이버와 실제 발행 검증은 아직 진행 중입니다
비용을 눈에 보이게, 규칙은 CI가
크레딧은 먼저 잡고 나중에 정산합니다
- 작업을 시작할 때 최악의 비용을 먼저 보류하고, 끝나면 실제 사용량과 맞춰 남은 몫을 돌려줍니다
- 잔액이 모자라면 작업 레코드를 만들기도 전에 거절합니다. 반쯤 진행되고 멈추는 상태를 안 만듭니다
- 사용자에게 보이는 크레딧과, 실제 공급자에게 나간 마이크로달러 비용은 원장을 따로 씁니다
- 모델은 운영자가 목적별(사진 분석 · 문체 분석 · 글쓰기 · 이미지 · 비디오)로 등록해야 화면에 뜹니다. 숨은 기본 모델이 없습니다
경계는 린트가 지킵니다
| 검사 | 막는 것 |
|---|---|
lint:fsd + boundaries |
FSD 레이어를 거꾸로 참조하거나 슬라이스 내부를 직접 열기 |
lint:style |
@theme에 등록되지 않은 색·임의값 쓰기 (스톡 Tailwind 색 금지) |
gen:proto + git diff --exit-code |
손으로 고친 생성 코드가 몰래 들어오기 |
- 🧭 proto가 유일한 이음매 — Go 핸들러와 TS 클라이언트가
proto/postpilot/v1하나에서 나오고, 생성 코드는 그걸 소유한 어댑터를 통해서만 쓰입니다 - 🧱 의존은 안쪽으로만 — 도메인은 proto도 SQL 로우도
database/sql도 모릅니다. 핸들러와 스토어 두 곳의 매퍼가 그 경계를 지킵니다 - 🚀 프론트는 Cloudflare Workers 정적 배포, 백엔드는 GHCR 이미지 → VPS docker compose.
main→ prod,develop→ staging이고 헬스 게이트가 실패하면 롤백합니다
문서를 먼저 쓰고 코드를 씁니다
무엇을 왜 어떻게 만드는지가 spec/에 있습니다. 코드와 문서가 어긋나면 문서가 맞습니다.
- 📚 문서 218편, 약 34,000줄. 도메인 SSOT 16개(인증 · 요금 · 글 · 말투 · 생성 · 모델 · 템플릿 · 지침 · 내보내기 · 발행 · 언어 · 테마 · 소개 페이지 · 결제 …)
- ✅ 끝낸 작업 문서 86편. 통과 기준과 검증 명령을 다 통과해야 닫힙니다
- 🗺️
spec/STATE.md한 장이 관제탑입니다. 어느 SSOT가 몇 판이고 무엇이 아직 태스크가 안 됐는지가 거기 있습니다 - 🤖 규칙을 문서에 못 박아 두면, 사람이 쓰든 AI 에이전트가 쓰든 같은 규칙으로 코드가 나옵니다
하면서 알게 된 것
- "실패를 성공으로 가장하지 않기"가 제일 중요한 설계 원칙이었습니다. 재시작 때 실행 중이던 작업은 성공이 아니라 실패로 돌리고, 확인 못 한 발행은 모른다고 남깁니다
- LLM을 쓰는 제품에서 진짜 어려운 건 프롬프트가 아니라 비용과 실패의 경계를 어디에 그을지였습니다
- 모델 이름을 도메인 코드가 모르게 해두니, 공급자가 바뀌어도 흔들릴 곳이 한 폴더로 줄었습니다
Tech Stack: Node.js 20 · ESM · npm · Claude Code / Codex Agent Skills · GitHub Actions (OIDC Trusted Publishing)
📝 프로젝트 컨셉
위 두 프로젝트가
spec/으로 굴러가는 이유가 이겁니다. 아이디어 → 기획 → 작업 계획 → 구현 → 리뷰를 하나로 잇는 Agent Skill 8종입니다.AI에게 "잘 좀 해줘"라고 부탁하는 대신, 무엇을 물어보고 무엇을 남길지를 스킬로 못 박았습니다. 새 채팅을 열어도, 다른 프로젝트로 옮겨도, 같은 순서로 같은 모양의 문서가 나옵니다.
에이전트에게 역할을 나눠줬습니다
한 명이 다 하면 기획 문서에 기술 결정이 섞이고, 구현하다 말고 기획을 다시 엽니다. 그래서 스킬마다 직업을 줬습니다.
| 스킬 | 역할 | 묻는 것 |
|---|---|---|
ideation |
기획 전문가 | 흐릿한 아이디어를 발산·수렴으로 함께 좁힙니다 |
create-architecture |
엔지니어 | spec/ 초기화 · 사용자 수준 캘리브레이션 · 아키텍처 인터뷰 |
create-ssot · update-ssot |
기획자 | 목적 · 타겟 · 범위 · 플로우 · 정책 (기술은 안 묻습니다) |
create-task · implement-task |
엔지니어 | 데이터 모델 · API · 엣지 · 마이그레이션 (기획은 안 엽니다) |
review-code |
시니어 리뷰어 | 리팩토링을 우선순위 붙여 주장합니다 (코드는 안 고칩니다) |
create-narrative |
작가 | spec 전체를 사람이 읽을 이야기로 엮습니다 |
- ⚖️ 제안하는 쪽과 결정하는 쪽을 나눴습니다 — 기획에
ideation이 있다면 개발에는review-code가 있습니다. 둘 다 능동적으로 주장하지만, 채택은 언제나 사용자입니다 - 🎚️ 사람 수준에 맞춰 묻습니다 —
expert에겐 용어만 나열하고 엣지까지 직접 묻고,novice에겐 "무엇이 어떻게 되는지"를 물은 뒤 기술 결정은 에이전트가 내리고 한 줄로 보고합니다 - 🌏 문서는 영어로 남기고(오독 방지·토큰), 대화는 사용자의 언어로 합니다
기억을 사람이 아니라 파일에 둡니다
- 🗺️
spec/STATE.md한 장이 관제탑 — 어느 기획이 몇 판(rev)이고 그중 어디까지 작업으로 쪼개졌는지(tasked)가 거기 있습니다. **그 차이가 곧 "아직 구현에 반영 안 된 기획 변경"**이라, 어느 세션에서든 "변경분 태스크로 쪼개줘"가 바로 통합니다 - 📌 모든 스킬은 묻기 전에 먼저 적습니다 — 대화가 끊겨도, 창을 여러 개 띄워도, 다음 사람이 STATE만 보면 상황을 압니다
- 🧼 SSOT에는 결과만 남습니다 — 결정과 이유와 제약만. "사용자가 이렇게 요청해서…" 같은 대화의 흔적은 남기지 않습니다. 맥락이 필요한 곳은 작업 문서입니다
- 🔎 약속이 아니라 검사입니다 —
lint가 ID 형식,rev/tasked정합, 참조 무결성, 고아 파일, 골격 밖 섹션과 산문을 CI에서 잡습니다. 규칙을 문서에만 적어두면 언젠가 어긋납니다
💡 두 프로젝트를 만들면서 매번 같은 순서를 손으로 반복하고 있다는 걸 알았습니다. 반복되는 건 도구로 내리는 게 맞았습니다.


