김재희 · Frontend jaeheekim.dev@gmail.com

프로토타입은 빠르게,
완성도는 끝까지.


산출물이 없는 자리에서 시작한 일이 많았습니다. 빠르게 간 건 단계를 건너뛰어서가 아니라, 없는 단계를 직접 만들었기 때문입니다. Digital SAT는 화면 정의가 없어 IA · 스토리보드 · 기능정의서를 Figma로 직접 그려 합의한 뒤 React로 구현했고, 미국 치과의 웹 서비스는 2년 5개월 동안 원격 단독으로 맡아, 환자 문진표가 병원 EMR에 바로 등록되도록 웹폼 API를 연동하고 계약 종료 시점에 인계했습니다.

맡아온 일은 대체로 역할이 다른 사람들이 한 시스템을 함께 쓰는 구조였습니다. 누가 어디까지 보는지를 정하는 일이 매번 가장 어려운 판단이었습니다. 지금은 같은 판단을 사람과 AI 사이에서 하고 있습니다 — 어디까지 모델에 맡기고 어디를 사람 손에 남길지.

409 → 4GraphQL 중첩 조회 쿼리 수. DataLoader 로 배치해 99% 감소 — 스키마부터 직접 구현하고 계측했습니다 기획 → 운영미국 워싱턴 D.C. 권역 의료기관 사이트 한 건을 기획 · IA · 디자인 · 개발 · SEO · 운영까지 단독으로 맡은 2년 5개월. 100% 원격 · 영어 서면 서버 60ms지금 맡은 서비스. 느린 원인이 서버가 아니라는 것부터 확인하고, 병목을 전달 계층으로 좁혀 2MB짜리 폰트 한 개를 찾았습니다

경력
엔텔스 2020.01–2022.09 · 교육공간 2023.06–2024.01 · Precision Micro Endodontics 2024.01–2026.05 · 러닝스파크 2026.06–2026.08 — 총 6년
담당 범위
요구사항 협의 · IA · 화면 설계 → React / Next.js 구현 → API 응답 계약 정의 → 배포 · 운영
학력 · 교육
한국방송통신대학교 컴퓨터과학과 2024.03–2027.03 · 그린아카데미 프로젝트기반 프론트엔드(React · Vue) 2023.01–06
Frontend
React · Next.js · TypeScript · JavaScript(ES6+) · HTML / CSS
Mobile
React Native · Expo · EAS CI/CD
API · 국제화
REST / RPC · Claude API · Supabase Edge Functions · GraphQL(구현 · 저장소 공개) · i18next
품질 · 운영
Lighthouse · Web Vitals · 접근성 · 반응형 · Sentry · 심링크 원자 교체 배포
그 외
Node.js / NestJS · Java / Spring · PHP · MariaDB · MongoDB

01 / 주요 프로젝트주요 프로젝트

askedtech · 러닝스파크

Next.jsTypeScriptNestJSMongoDBClaude APIAWS EC2

에듀테크 뉴스 · 커뮤니티 서비스. 사용자 웹 · 관리자 CMS · API 세 서비스의 화면과 응답 계약을 맡고 있습니다.


문제
자료는 RSS와 메일함으로 나뉘어 들어오고, 발행까지 섹션 분류 · 요약 · 검수를 거쳐야 합니다.
설계
  • 수집은 자동으로 돌고 분류 · 요약은 Claude API가 합니다. 발행 버튼은 사람이 누릅니다. 승인이 번거로우면 사람들은 파이프라인을 우회해 손으로 보내기 때문에, 통제를 조이는 대신 한 번에 훑고 끝나게 만들었습니다.
결정
  • 프롬프트보다 먼저 정한 건 “LLM 없이도 전체를 돌려볼 수 있는가”였습니다. 호출부를 주입 가능한 클라이언트로 분리하고 드라이런 모드를 뒀습니다.
  • API는 모듈별로 생성 · 조회 · 수정을 다른 DTO로 나눴고(28개 중 21개), 두 프론트가 거기서 내린 타입 한 벌을 공유합니다. 한쪽 화면 요구로 응답 모양을 바꾸면 다른 쪽이 조용히 깨지기 때문입니다.
  • 배포할 때마다 처리 중이던 요청이 끊기는 것을 보고 graceful shutdown과 종료 대기 시간 로깅을 넣었습니다. 얼마나 기다려야 안전한지 모르면 대기 시간을 정할 수 없어서, 먼저 재게 만들었습니다.
  • SNS 자동 발행은 만들지 않았습니다. 손익분기가 53주로 나와, 사람이 한 번 누르는 반자동 MVP를 제안해 채택됐습니다.
뉴스레터 파이프라인의 승인 게이트 RSS와 메일에서 자료를 모으고, LLM이 분류하고 요약한 다음, 사람이 승인하는 칸을 통과해야만 Maily API로 발행됩니다. 승인 칸을 통과하지 못하면 아무것도 발행되지 않습니다. 발행 뒤에는 되돌릴 수 없습니다. RSS · 메일 LLM 분류 · 요약 승인 · 사람 발행 · Maily API 막히면 여기서 멈춤 발행 뒤에는 되돌릴 수 없음
자동으로 흘러가다가 승인 칸에서 한 번 멈춥니다. 사람이 누르지 않으면 아무것도 나가지 않습니다.

인계를 앞두고 재봤습니다. 서버 응답은 60ms인데 화면이 느려 병목을 전달 계층으로 좁혔습니다. 전송량 18MB 가운데 폰트 한 개가 2MB였습니다. 남은 기간에 손댈 범위는 폰트 서브셋과 이미지 지연 로딩까지로 보고, 스크립트 48건은 회귀 위험이 커서 측정값과 원인만 남겼습니다.

EC2 3대 · 심링크 원자 교체 배포 · graceful shutdown 도입

askedtech.com

GraphQL은 따로 확인했습니다. 출시한 게임의 Postgres 스키마를 옮긴 로컬 DB에 GraphQL 계층을 얹고 계측기를 붙여, 리그 8개 × 25명 중첩 조회에서 409번 나가던 쿼리를 DataLoader로 배치해 4번으로 줄였습니다. 로더를 요청 단위로 만들지 않으면 캐시가 요청 사이로 샌다는 것도 확인했습니다 — github.com/JayKim-v/graphql-n-plus-one

precisionrct.com

PHPJSONjQueryBootstrap 5Open Dental API

미국 버지니아(워싱턴 D.C. 권역) 근관치료 전문 치과의 웹 서비스. 홈페이지와 함께 환자 문진 시스템을 만들어 병원 EMR에 연동했습니다. 기획 · IA · 디자인 · 개발 · SEO · 운영까지 2년 5개월간 100% 원격으로 맡았고, 현지 담당자와 요구사항 합의부터 인수인계까지 영어 서면으로 진행했습니다.


한 일
  • 환자가 작성한 문진표가 병원 EMR(Open Dental)에 바로 등록되도록 웹폼 API를 연동했습니다. 그 전에는 종이로 받아 데스크에서 다시 입력하던 구간이었습니다. EMR이 받는 필드가 정해져 있어서, 문진 항목을 그 구조에 맞춰 설계한 뒤에 화면을 그렸습니다. 환자 정보가 병원 시스템으로 넘어가는 경로라 한 번 잘못 들어가면 되돌리기 어렵다고 보고, 보내기 전에 검증하는 쪽에 시간을 썼습니다.
  • 환자와 의뢰 치과의를 두 사이트로 나누지 않고, 진입점 하나에 버튼 두 개로 갈랐습니다. 나누면 운영도 검색 노출도 같이 쪼개지는데, 혼자 2년 넘게 볼 사이트였습니다.
  • 콘텐츠를 JSON으로 갈라 화면과 데이터를 분리했습니다. 영어 문구 하나가 곧 UI인 제품이라, 가장 자주 바뀌는 층을 개발 없이 고칠 수 있어야 했습니다.
  • 계약 종료 시점에 예약 발행을 넣어 인계했습니다. 글과 공개일을 JSON에 함께 적어두면 그날 올라갑니다.

반응형 / 크로스브라우징 · 메타데이터 · 사이트맵 · OG · 호스팅 · SSL · 백업 · 장애 대응까지 단독

precisionrct.com 첫 화면. 진료실 사진 위에 Got Tooth Pain? 문구와 예약 · 환자 의뢰 두 개의 행동 버튼, 그 아래 전화번호 · 주소 · 요일별 진료시간.
모바일 첫 화면. 전화번호 · 주소 · 요일별 진료시간을 스크롤 없이 보이는 자리에 뒀습니다 — 문의의 대부분이 그 둘이라, 보여주고 싶은 것보다 찾으러 온 것을 먼저 올렸습니다.

precisionrct.com

Digital SAT 웹앱 · 교육공간

ReactJavaMariaDBFigma

학원에 공급하고 학생이 쓰는 SAT 모의고사 운영 웹앱 — 자사 학원에서도 같은 제품을 썼습니다. 학원 · 강사 · 학생 세 주체가 같은 응시 데이터를 서로 다른 범위로 봅니다. IA · 화면 설계부터 React 구현까지 맡았습니다.


문제
학생 · 시험 · 강의 · 포인트 네 모듈을 만들어야 하는데 화면 정의가 없었습니다.
설계
IA를 잡고 스토리보드와 기능정의서를 Figma로 그려 합의한 뒤 React로 구현했습니다. 놓고 볼 것이 없으면 논의가 겉돌아, 초안을 먼저 올리고 그 위에서 맞췄습니다.
결정
  • 응시 상태 자동저장 · 복원은 실패 시나리오에서 출발했습니다. 시험 중 브라우저가 닫히면 답안이 사라지므로 저장 책임을 학생이 아니라 시스템으로 옮겼습니다.
Figma로 만든 강의관리 대시보드 스토리보드와 기능정의서. 왼쪽은 반 평균 성적 카드 12개와 최근 시험 도넛 차트, 학생별 성적 순위, 회차별 타 학원 대비 비교 막대그래프를 배치한 화면 스토리보드. 오른쪽은 각 영역의 데이터 범위와 계산 규칙을 적은 기능정의서.
쓸 사람들을 직접 만나 물어가며 정한 산출물입니다. 왼쪽이 스토리보드, 오른쪽이 각 영역의 데이터 범위와 계산 규칙을 적은 기능정의서입니다. 학원 · 강사 · 학생이 같은 성적 데이터를 서로 다른 범위로 보기 때문에, 화면을 그리기 전에 어느 지표를 누구에게 어디까지 보여줄지부터 정하고 그것을 한 장에서 합의했습니다.

IA · 스토리보드 · 기능정의서 · UX/UI를 직접 만들고 구현까지 담당

디자인 시스템 · 다국어

mStone · 제품 라인업 사이트

제품 3종을 빌드 시점에 정적으로 만들었습니다 — 갱신되지 않는 시안이라 재검증 주기를 정할 근거가 없었고, 갱신이 생기면 App Router에서 라우트 단위로 켤 수 있는 형태까지만 열어뒀습니다.

Next.js · App Router · Vercel — 데스크톱 100 / 100 / 100 / 100 · LCP 0.5s · CLS 0.003 — 모바일 성능 71 · LCP 5.0s

mstone-demo.vercel.app

실존 브랜드와 무관한 팬 제작 시안입니다.

할리스 리뉴얼

일반 고객과 창업 예정 사업자를 두 사이트로 나누지 않고, 진입점은 하나로 두고 그 뒤부터 동선을 갈랐습니다. 스크롤 인터랙션은 유료 라이브러리 대신 직접 구현했습니다. 제어권을 외부에 넘기면 요구가 들어올 때마다 우회 코드가 쌓이고, 팀원 3명이 유지보수해야 했기 때문입니다.

React · JavaScript · 카카오맵 API

projectteamtwo.github.io 정리 노트

디자인 시스템 · 다국어

Apple Pop 사과게임

색 · 타이포 · 라운드 · 간격 · 모션을 토큰으로 먼저 정하고 그 위에 재사용 컴포넌트를 얹었습니다. 여섯 개 언어를 붙이면 번역 파일보다 레이아웃이 먼저 깨집니다 — 같은 버튼이 언어에 따라 두 배로 길어지므로, 문자열을 전부 분리하고 폰트 폴백 순서를 정한 다음에 번역을 넣었습니다.

React Native · TypeScript · Supabase Edge Functions(REST / RPC) · i18next 6개 로케일 · EAS CI/CD · Sentry · iOS · Android 양대 마켓 출시

App StoreGoogle Play

그 밖에: AI Usage — Claude Code · Codex CLI의 토큰 사용량을 계측해 macOS 메뉴바와 iOS 위젯에 표시. Swift / SwiftUI / WidgetKit / GRDB, TestFlight 배포 중.

02 / 성능 · 접근성 실측성능 · 접근성 실측

제가 만든 결과물을 같은 조건으로 재고, 편차마다 원인을 붙였습니다.

측정: Chrome Lighthouse (모바일 / 데스크톱 프로파일), 시크릿 창 · 캐시 비활성 · 2026년 8월 측정. 표 안의 는 측정하지 않은 항목입니다.
대상 환경 성능 접근성 모범사례 SEO LCP · CLS
mStone 개인 · Next.js 정적 생성 데스크톱 100 100 100 100 0.5s · 0.003
처음부터 Next.js로 만든 경우.
mStone 개인 · Next.js 정적 생성 모바일 71 100 100 100 5.0s · 0
CLS 0 · TBT 0ms. 느린 구간은 첫 렌더 하나이고, 원인은 서브셋을 뜨지 않은 폰트 423KB입니다.
precisionrct.com 모바일 60 80 100 92 11.7s · 0.005
jQuery · Bootstrap 5 기반 자산을 2년 5개월 운영한 사이트. 계약 종료 시점의 1순위는 예약 발행 인계였고 성능은 뒤로 미뤘습니다. 미룬 것도 판단입니다.
jaykim-v.github.io/fe 데스크톱 100 100 100 100 0.5s · 0
jaykim-v.github.io/fe 모바일 100 100 100 100 1.4s · 0
위 진단을 그대로 적용한 페이지입니다. 인터랙션 스크립트는 직접 짠 2.5KB 한 벌 — 같은 동작을 라이브러리로 붙이면 35KB에 외부 도메인 세 곳을 더 거칩니다. 웹폰트는 self-host 서브셋 4파일 125KB, 도판은 크기를 미리 잡아 CLS 0입니다.

점수가 갈리는 자리마다 원인을 함께 적었습니다. 무엇이 느린지 모르면 무엇을 먼저 고칠지도 정할 수 없습니다.

접근성도 같은 방식입니다. 색은 코어 6개로 닫고 대비비 전 조합을 sRGB 상대휘도로 계산해 --ink on --bg 18.03:1 · --accent on --bg 9.47:1을 확인했고, AAA에 못 미치는 조합에는 사용 규칙을 붙였습니다. 표는 scope로 헤더를 묶고 영문 토큰명에 lang을 지정했습니다 — 지정하지 않으면 스크린리더가 한국어 발음으로 읽습니다. 건너뛰기 링크와 :focus-visible · prefers-reduced-motion도 넣었습니다.

03 / 디자인 시스템디자인 시스템

규칙을 먼저 정하고 컴포넌트를 얹습니다. 이 문서의 색 토큰은 새로 만들지 않고 기존 포트폴리오에서 가져왔습니다 — 값은 다시 쓰일 때 생깁니다.

색은 코어 6개로 닫고, 나머지 2개는 같은 계열에서 한 단계 낮춘 파생값입니다. 파생값에 새 색을 넣지 않습니다.

컴포넌트는 색을 받지 않고 의미를 받습니다. 색을 받는 순간 그 컴포넌트가 쓰인 모든 자리가 토큰 교체의 예외가 됩니다.

투명도로 색을 만들지 않습니다. 반투명 텍스트는 배경이 바뀌는 순간 대비비가 계산에서 빠져나갑니다. 흐리게 보여야 하면 흐린 색을 토큰으로 만듭니다.

색 스킴은 하나로 고정했습니다. 이 페이지는 인쇄해서 제출하는 문서이기도 해서, 화면과 종이가 같은 색으로 보여야 합니다.

배경 인쇄가 꺼지면 색은 사라지고 값은 남습니다.

접근성은 말이 아니라 대비비로 적습니다. AAA에 못 미치는 조합은 사용 규칙을 함께 적었습니다. 위 값은 sRGB 상대휘도로 계산했습니다.
조합 대비비 판정 사용 규칙
--ink on --bg 18.03:1 AAA 본문 · 제목
--ink-soft on --bg 15.06:1 AAA 본문 보조 · 각주
--accent on --bg 9.47:1 AAA 링크 · 강조
--bg on --accent 9.47:1 AAA 화면에서 04 밴드의 제목과 본문. 인쇄본은 배경을 빼 --ink on --bg 18.03:1이 됩니다 — 배경 박스가 페이지 경계에서 잘리기 때문입니다
--line on --accent 6.59:1 비텍스트 3:1 통과 04 밴드 안의 구분선
--muted on --bg 5.47:1 AA (AAA 미달) 라벨 · 메타에만. 이 색에 투명도를 걸지 않습니다
--muted on --bg-elev 4.62:1 AA 이전 팔레트에서는 AA 미달이라 금지했는데, Champagne으로 바꾸며 통과했습니다. 그래도 이 조합은 라벨에만 씁니다

대비비 말고 이 문서에서 실제로 한 것 — 첫 초점에 본문 바로가기 링크를 두고, 윤곽선은 :focus-visible에만 줬습니다. 마우스로 눌렀을 때도 뜨면 키보드 사용자에게만 필요한 신호가 아니게 됩니다. 표는 scope로 헤더를 묶고, 도식에는 titledesc를 달았습니다. 영문 토큰명에 lang을 지정하지 않으면 스크린리더가 한국어 발음으로 읽습니다.

/* 이 페이지의 토큰 — 코어 6 + 파생 2 */
:root {
  --bg:#FDFCF8;  --bg-elev:#F8E7C9;  --ink:#0E1512;
  --muted:#6F6752;  --line:#E4D3B0;  --accent:#064E3B;
  --ink-soft:#1F2622;  --line-soft:#F0E4D0;
}
/* 반전 면은 색을 다시 고르지 않고 변수만 뒤집는다. */
.band { --band-fg: var(--bg); --band-line: var(--line);
        background: var(--accent); color: var(--band-fg); }
@media print {
  .band { --band-fg: var(--ink); --band-line: var(--line-soft);
          background: none; border-block: 2pt solid #064E3B; }
}

인쇄에서 바뀌는 것은 변수 두 줄이고 컴포넌트 코드는 그대로입니다.

04 / 일하는 방식일하는 방식

같은 백엔드 위에서 화면을 가릅니다

한 시스템을 여러 역할이 함께 쓰는 화면을 계속 맡았습니다 — 멘토와 학교(교육부 원격영상 진로멘토링), 학원과 강사와 학생(Digital SAT), 환자와 의뢰 치과의(precisionrct), 사용자와 운영자(askedtech).

어려운 건 화면을 두 벌 만드는 일이 아니라 어디서 가를지 정하는 일이었습니다. 화면을 나누는 기준이 곧 권한 모델이고, 권한 모델은 나중에 붙일 수 없습니다.

그래서 혼자 정하지 않았습니다. 진로멘토링은 두 포털이 같은 백엔드를 써서 백엔드 담당과 응답 계약을 먼저 맞췄고, Digital SAT는 어느 지표를 누구에게 어디까지 보여줄지 쓸 사람들과 합의한 뒤 화면을 그렸습니다.

쓰이지 않으면 만든 것이 아닙니다

자동화를 넓힐수록 사람이 손댈 수 있는 구간은 줄어듭니다. 어디까지 맡길지를 기술이 되는 범위가 아니라 그 시스템을 매일 쓸 사람의 하루로 정했습니다. 승인 한 번을 남긴 것도, 손익분기가 나오지 않는 자동화를 만들지 않은 것도 같은 기준입니다.

05 / 핵심 역량과 근거핵심 역량과 근거

역량마다 그 일을 실제로 한 서비스와, 거기서 무엇을 했는지를 함께 적었습니다.

왼쪽이 역량, 오른쪽이 그 역량을 실제로 쓴 서비스와 한 일입니다.
역량 근거
React · Next.js askedtech 사용자 웹 · 관리자 CMS 두 Next.js 앱을 동시에 운용 · Digital SAT를 React로 전면 구현
TypeScript askedtech 전 구간. 두 프론트가 API 응답 타입을 각자 선언하지 않고 DTO에서 내린 한 벌을 공유합니다
디자인 · 기획 의도의 기술 구현 엔텔스에서는 발주처가 정한 화면 정의를 받아 구현했고, Digital SAT에서는 산출물이 없어 IA · 스토리보드 · 기능정의서를 직접 만들어 합의한 뒤 구현했습니다 — 받는 쪽과 만드는 쪽을 모두 했습니다
웹 성능 · 접근성 askedtech 인계 전 측정으로 느린 원인이 서버가 아님을 확인하고 전달 계층으로 좁혔고, precisionrct에서는 2년 5개월간 재면서 무엇을 먼저 고치고 무엇을 미룰지 정했습니다. 접근성은 대비비 전 조합 계산 · 미달 조합 사용 규칙 · :focus-visible · scope 구현(웹 상세 — jaykim-v.github.io/fe)
REST API 연동 askedtech에서 세 서비스를 잇는 응답 계약을 정하고 운용 · Apple Pop은 Supabase Edge Functions로 REST / RPC 작성
GraphQL 스키마 · 리졸버 · DataLoader를 직접 구현해 중첩 조회 409쿼리를 4쿼리로 줄인 것을 계측 — github.com/JayKim-v/graphql-n-plus-one
협업 주도 엔텔스에서 두 포털이 같은 백엔드를 쓰는 구조라 백엔드 담당과 응답 계약을 조율 · SNS 자동 발행의 손익분기를 53주로 계산해 반자동 MVP를 제안하고 채택까지 이끌었습니다
글로벌 유저 경험 미국 클라이언트 사이트를 2년 5개월 원격 단독 운영 — 합의부터 인계까지 영어 서면 · Apple Pop 6개 로케일, 문자열 외부화 후 폰트 폴백 순서를 정하고 번역 적용

06 / GraphQLGraphQL — 쿼리 수로 확인한 것

REST로 세 서비스를 잇다 보니 화면마다 필요한 필드가 달라 엔드포인트가 늘고 안 쓰는 필드가 쌓였습니다. 리졸버 단위의 N+1은 직접 세어보기 전에는 감이 오지 않아, 제가 출시한 게임의 Postgres 스키마를 그대로 옮긴 로컬 DB 위에 GraphQL 계층을 얹고 계측기를 붙였습니다. 리그 8개 × 25명 중첩 조회에서 409번 나가던 쿼리를 DataLoader로 배치해 4번으로 줄였고, 로더를 요청 단위로 만들지 않으면 캐시가 요청 사이로 샌다는 것도 확인했습니다. 계측되지 않는 경로를 남기지 않으려고 모든 조회를 함수 하나로 통과시켰습니다.

github.com/JayKim-v/graphql-n-plus-one

07 / 연락처연락처

김재희 · jaeheekim.dev@gmail.com · 010-9025-9880
github.com/JayKim-v · 전체 이력 jaykim-v.github.io

열심히 노력하다가 갑자기 나태해지고,
잘 참다가 조급해지고,
희망에 부풀었다가 절망에 빠지는 일을 또다시 반복하고 있다.
그래도 계속해서 노력한다면 수채화를 더 잘 이해할 수 있겠지.
그게 쉬운 일이었다면, 그 속에서 아무런 즐거움도 얻을 수 없었을 것이다.
그러니 계속해서 그림을 그려야겠다.

빈센트 반 고흐가 동생 테오에게

부풀던 날에도, 무너지던 날에도,
오래 곁에 두고 꺼내 보는 문장입니다.

완벽하지 않은 그 날이 시작만 하는 사람과 끝까지 완주하는 사람을 결정짓는 날이다.
완주하는 개발자, 김재희입니다.