jongchan.kim kickbelldev@gmail.com

$ whoami  → 김종찬 · frontend-centric software engineer · ~5y

AI와 빠르게 만들고,
시스템은 끝까지 이해합니다.

제품의 방향성을 함께 고민하고, 이를 뒷받침하는 구조와 사용자 경험을 설계하는 프론트엔드 중심 소프트웨어 엔지니어입니다. AI 에이전트와 함께 설계·구현·검증을 수행하며, 시스템의 구조와 동작에 대한 이해를 바탕으로 개발 방향을 정하고 변경의 영향을 직접 확인합니다.

cases/01-async-consistency · 비동기 일관성

늦은 응답이 지금 화면을 덮어쓰지 않도록

영상을 보는 동안에는 재생 위치가 계속 바뀌고, 진도는 일정 주기로 서버에 저장됩니다. 저장과 조회가 겹칠 때의 진도 동기화, 영상을 전환하거나 다시 열 때의 실행 구분을 함께 다뤘습니다.

요청의 순서

같은 영상의 진도 조회는 진행 중인 저장이 끝난 뒤에 보냅니다. 재진입할 때도 이전 종료 뒤 새 시작을 보냅니다. 이 대기는 재생이나 다른 영상의 요청을 막지 않습니다.

응답을 반영할 권한

A를 보고 B로 옮겼다가 다시 A를 열면, 첫 방문 A1과 재방문 A2는 다른 실행입니다. 실행마다 토큰을 두어 현재 실행만 화면 상태를 바꾸게 합니다.

// 요청 순서만 맞춰서는 해결되지 않았습니다

한 영상을 계속 보는 중에도, 늦게 도착한 진도 조회가 최대 학습위치를 과거 값으로 덮어쓸 수 있었습니다. 이 때문에 정상 재생 중인데도 위치 이동을 제한하는 토스트가 나타났습니다. 영상 전환과 재진입에서는 이전 실행의 종료와 새 실행의 시작이 엇갈리는 문제도 있었습니다.

콘텐츠 ID만으로는 이전 방문과 현재 방문을 구분할 수 없었습니다. 각 실행에 토큰을 두어 현재 실행의 응답만 화면에 반영했습니다. 이전 실행의 늦은 응답도 세션을 종료하는 데는 사용하되, 지금 보고 있는 화면은 바꾸지 않게 했습니다.

세션 복구 보완과 함께, 2주간 접수된 관련 문의 3건을 해소했습니다.

재생은 이어가고, 진도 저장·조회는 순서대로

정상적이지 않은 강제 위치 변경을 막기 위해, 플레이어는 timeupdate에서 재생 위치가 허용된 최대 학습위치를 넘는지 검사했습니다. 그런데 저장·조회가 엇갈려 최대 학습위치가 과거 값으로 덮이면, 정상적으로 재생 중인 위치도 제한을 넘은 것으로 판단해 이동할 수 없다는 토스트를 띄웠습니다.

예를 들어 2:00의 진도를 저장하는 동안 조회 1이 저장 전의 1:59를 읽습니다. 저장 1이 끝난 뒤에도 이 조회가 응답을 기다리는 사이 재생은 계속되고, 다음 주기의 저장 2도 시작됩니다. 이때 늦게 도착한 조회 1이 최대 학습위치를 1:59로 되돌리면, 다음 timeupdate 검사에서 정상 재생을 잘못 차단할 수 있었습니다.

재생은 계속 진행하되, 같은 콘텐츠의 저장·조회 순서를 맞췄습니다. 주기 저장의 결과는 저장 응답으로 바로 반영하고, 별도로 조회해야 할 때는 진행 중인 저장이 끝난 뒤에 요청합니다. 현재 재생 위치와 서버에 저장된 진도를 구분해, 응답을 기다리는 동안에도 재생이 이어지도록 했습니다.

아래는 같은 영상에서 세 번의 저장·조회를 비교하는 설명용 데모입니다. 양쪽 모두 저장 간격은 3초, 저장 요청은 1.2초, 조회 요청은 2.8초로 같습니다. 조회 1이 끝나기 전에 저장 2가 시작하며, 조회 2와 저장 3도 겹칩니다. 아래쪽은 각 조회를 진행 중인 저장이 끝난 뒤에 전송합니다. 재생 위치는 실제 시간과 같은 속도로 움직입니다.

일부만 실패했다면, 일부만 다시 보냅니다

// 재시도할 때 이미 저장한 점수를 다시 보내지 않도록

강사는 여러 학습자의 점수와 피드백을 한 번에 등록하지만, 두 항목은 서로 다른 API로 저장됩니다. 점수만 저장되고 피드백이 실패한 경우, 전체 요청을 반복하면 점수까지 다시 보냅니다.

이미 저장한 점수를 건너뛰려면, 재시도 조건이 같은지 확인해야 했습니다. 성공한 요청의 학습자·점수·서버 버전을 기록하고, 셋이 그대로일 때만 피드백을 다시 보냅니다. 그사이 대상이나 점수, 서버 버전이 바뀌었다면 점수부터 다시 저장합니다.

cases/02-core-sdk-boundary · 코어와 SDK

화면이 달라도 같은 작업 흐름을 쓰려면

학습자용 콘텐츠 화면을 강사용 제품으로 확장하며 공통 UI를 분리했습니다. 이후 새 제품에서는 설문을 한 페이지에 한 문항씩 보여줘야 해, 기존 화면을 그대로 재사용하기 어려웠습니다.

// 화면이 달라져도 입력·저장 흐름은 재사용하려면

공통 UI를 가져와도 답안을 관리하고 저장·제출을 연결하는 코드는 각 화면과 제품에 묶여 있었습니다. 설문 화면을 한 문항씩 이동하는 방식으로 만들면, 새 화면에서도 이 흐름을 다시 연결해야 했습니다.

그래서 답안 유지, 문항 이동, 저장 중 추가 입력 보존, 저장 후 제출을 화면 없이 동작하는 헤드리스 코어로 분리했습니다. 검증 함수만 공유하면 각 화면이 저장·제출 흐름을 다시 조립해야 하므로, 상태와 실행 순서까지 코어가 맡도록 했습니다.

화면은 코어의 상태를 보여주고 입력·저장 명령을 전달합니다. 제품마다 다른 API와 권한, 완료 기준은 제품에 남겨, 화면을 바꿀 때 공통 흐름까지 다시 만들지 않도록 했습니다.

Architecture책임 경계
<ViewA>전체 항목 화면
<ViewB>단계별 입력 화면
shared core초안 · 탐색 · 저장 중 입력 보존 · 저장 후 제출 순서
product adapter데이터 변환 · 인증·권한 · API · 완료 기준
제품이 저장·제출 함수를 제공하고, 코어가 그 호출 순서를 조율합니다.

코어에 모은 것

초안·편집 여부·항목 이동을 관리합니다. 저장 중 들어온 입력을 보존하고, 저장이 끝난 뒤 제출하도록 순서를 조율합니다.

제품에 남긴 것

서버 데이터 변환, 인증·권한, API와 캐시를 관리합니다. 학습 완료를 인정하는 조건도 제품 정책에 따라 결정합니다.

Architecture패키지 구성과 저장 한 번의 흐름
host app인증 · 라우팅만 연결
product sdk · 공개 진입점컴포넌트 · 스타일 · 타입제품 어댑터: 서버 데이터 변환 · API · 권한 · 완료 기준
shared ui프레임 · 툴바 · 입력 영역
headless core답안 · 편집 여부 · 편집 순번 · 저장 큐
  1. product → core문항 규칙, 초기 답안, onSave·onSubmit 함수를 넘깁니다.
  2. core → view현재 답안과 명령을 돌려줍니다. 화면은 value와 setAnswer만 연결합니다.
  3. core저장 요청을 큐에서 하나씩 보내고, 저장 중 들어온 입력은 편집 순번으로 보존합니다.
  4. core → product제출은 앞선 저장이 끝난 뒤에만 호출합니다.
  5. product결과 콜백에서 알림 · 캐시 갱신 · 화면 이동을 처리합니다.
코어는 API를 모릅니다. 제품이 넘긴 함수를 언제, 어떤 순서로 부를지만 정합니다.

새 기능이 어디에 들어갈지 정하는 기준

경계는 새 기능이 들어올 때마다 흔들립니다. 기능의 자리는 아래 기준으로 판단했고, 공통 계약은 여러 제품에서 같은 요구가 반복될 때만 넓혔습니다.

질문위치예
화면이 달라도 같은 상태·명령·요청 순서를 쓰는가?core저장 중 편집 보존, 요청 상태
여러 제품이 같은 표현·상호작용을 쓰는가?shared ui입력 영역, 툴바, 포커스·접근성
제품의 권한·API·완료 기준에 따라 달라지는가?product sdk완료 인정, 제출 가능 시간
14종콘텐츠 코어 분리·검증
301개기존 검사를 포함한 코어 테스트 통과
9 → 3SDK 전환 후 호스트 앱의 직접 참조 파일

강사 앱에는 제품 SDK를 적용해, 내부 UI를 직접 조합하던 방식에서 공개 컴포넌트·스타일·타입을 사용하는 방식으로 전환했습니다.

AI의 설명을 그대로 받아들이지 않았습니다

AI가 “콘텐츠가 바뀌어도 코어 인스턴스가 유지되므로 세대 구분이 필요하다”고 설명했을 때, 그 수명 구조가 이상하다고 보고 다시 확인하게 했습니다. 실제로는 호스트가 이미 React key로 콘텐츠마다 컴포넌트를 새로 만들고 있었습니다. 훅 안에서 콘텐츠 교체를 한 번 더 처리하던 중복을 걷어내고, 같은 콘텐츠 안에서 필요한 복원과 영상 전체·미니 전환의 수명만 남겼습니다.

cases/03-first-load-js · 공개 프로젝트 · 지름알림

첫 화면에 필요한 코드부터 전달하기

커뮤니티 핫딜 정보를 모아 보여주는 팀 사이드 프로젝트입니다. 의존성을 한데 묶는 번들 설정 때문에 상품 상세의 첫 진입 JS가 커져 있었고, 화면은 모든 데이터를 기다린 뒤에야 내용을 보여줬습니다.

// 첫 화면에 필요하지 않은 코드도 함께 내려오고 있었습니다

node_modules를 하나로 묶는 설정 때문에 상품 상세에 처음 진입할 때 받는 전체 JS 전송량이 648kB였습니다. 기본 코드 분할을 막던 설정을 제거하고, MSW와 분석 도구는 필요한 시점에 불러오도록 바꿨습니다.

Network상품 상세 첫 진입 · JS
PR #245 전
648 kB

35 requests · 648 kB transferred

PR #245 후
510 kB

32 requests · 510 kB transferred · −21%

변경 후 같은 화면의 첫 진입 JS 전송량은 510kB로, 약 21% 줄었습니다. PR #245

준비된 영역부터 보여줍니다

데이터 조회 컴포넌트와 영역별 Suspense 경계를 나눠, 데스크톱 상세에서 한 영역이 기다리는 동안 다른 영역을 먼저 보여줍니다. PR #268

요청과 인증 경로를 한곳으로

브라우저와 서버에서 인증을 각각 처리해 로직이 중복되어 있었고, 토큰은 브라우저 스크립트에서 접근할 수 있었습니다. GraphQL 요청과 토큰 갱신을 Next.js Route Handler로 모으고, 토큰을 HttpOnly 쿠키로 옮겼습니다. PR #277

Architecture요청 경로
clientBrowser
next.jsRoute Handler
serverGraphQL API
토큰은 HttpOnly 쿠키에 두고, 갱신도 Route Handler에서 처리합니다.

beyond-frontend/

웹 프론트엔드를 넘어선 문제들

원인이 화면 코드 밖에 있던 문제들입니다. 운영 로그와 CDN, 브라우저 엔진, 네이티브 WebView까지 따라가며 가설을 하나씩 지웠습니다.

01

logs · media · cdn

영상이 늘 같은 지점에서 멈춘다

여러 사용자가 같은 교육 영상의 같은 지점에서 더 넘어가지 못한다는 문의가 들어왔습니다. 브라우저를 바꿔도 증상은 이어졌고, 화면에는 아무 안내도 뜨지 않았습니다.

IssuesMedia가설과 확인
  1. 앱이 재생을 멈췄나?무입력·백그라운드·중복 세션으로 멈출 때는 모두 안내창이 뜹니다. 이번에는 안내 없이 멈췄습니다.배제
  2. 네트워크가 느렸나?오류는 MEDIA_ERR_DECODE(3)였고, 버퍼는 멈춘 지점보다 200초 넘게 앞서 받아 둔 상태였습니다.배제
  3. 파일 자체가 깨졌나?원본의 처음 4분을 FFmpeg와 VideoToolbox로 디코딩하고 프레임 간격도 확인했지만 이상이 없었습니다.배제
  4. 그 구간의 데이터가 문제인가?운영 오류 273건을 콘텐츠·재생 위치·환경별로 다시 묶었습니다. 183건이 두 영상의 216.5~216.8초, 0.3초도 안 되는 구간에 몰려 있었고 Windows·Mac·Android 모두에서 나왔습니다.유지
  5. CDN이 내려준 데이터인가? 직접 세운 가설같은 구간을 CDN이 응답하는 주소 8곳에서 받아 해시를 비교했습니다. 장애 당시의 CDN 응답은 클라이언트에서 다시 확인할 수 없어, 유관 팀과 함께 CDN 구간을 확인해 원인을 확정하고 해결했습니다.확인
273운영 오류 이벤트

두 영상의 216초대나머지 콘텐츠에 흩어진 오류

216초대 183건의 환경 · Windows 156 · Mac 25 · Android 2

// 다음에는 더 빨리 좁히도록

오류 코드만으로는 앱·네트워크·파일·CDN 중 어디에서 문제가 생겼는지 구분하기 어려웠습니다. 이를 계기로 학습 진입·재생·저장·제출 전반의 기록 보강 계획을 정리했습니다.

최종 실패와 지연을 나누고, 지연된 작업이 진행 중인지 끝났는지 연결해 볼 수 있도록 설계했습니다. API 요청이 성공한 뒤 화면에 결과를 반영하다 발생한 오류는 기존 실패 기록에 포함하도록 했습니다. 미디어 오류에는 재생 조건과 오류 직전 동작을 함께 남기도록 계획했습니다.

02

ios · webkit · ai

iOS에서 자막 영상만 열리지 않는다

특정 iOS 버전에서 자막이 붙은 영상만 처음부터 재생되지 않았습니다. 앱 내 WebView와 Safari에서 모두 재현됐고, 이전 iOS 버전과 PC에서는 정상 재생됐습니다. 수정 후에는 왜 재생이 가능해졌는지 WebKit 엔진 소스로 확인했습니다.

SourcesWeb InspectorWebKit
  1. 실패 조건 좁히기같은 기기·화면에서 <track>을 둔 채 다시 불러오면 계속 MEDIA_ERR 4, 트랙을 지우면 재생됐습니다. 자막 없는 영상은 정상이었습니다.확인
  2. 자막을 빼면 끝인가?재생은 살아나지만 자막은 꼭 필요한 기능이라 해결책이 될 수 없었습니다.배제
  3. 붙이는 시점을 바꾸기영상 메타데이터를 받은 뒤 자막 트랙을 붙이도록 바꾸고, 같은 영상·실기기에서 수정 전후를 비교했습니다.확인
  4. 왜 통하는가AI와 WebKit 공개 소스를 추적했습니다. 영상 로드 시점에 있는 자막 트랙은 미디어 요소가 수집해 네이티브 영상 객체의 생성 옵션에 포함했습니다. 자막 표시를 꺼도 생성 옵션에는 남아 있었습니다. 메타데이터를 받은 뒤 트랙을 추가하면, 영상 객체를 처음 만들 때 전달하는 입력에서 자막 트랙이 빠집니다.확인

처음부터 자막을 붙일 때

<video> + <track>초기 자막 수집영상 객체 생성 옵션에 포함MEDIA_ERR 4

메타데이터 뒤에 붙일 때

<video>만 로드loadedmetadata<track> 부착재생 + 자막
실기기에서 확인한 동작과, 그 동작을 설명하는 엔진 소스의 경로
03

media · webview

화면 모드를 바꿔도 재생은 이어지게

전체 화면과 미니 플레이어는 같은 영상을 보는 두 가지 표시 방식입니다. 화면 모드를 바꿀 때 영상 요소를 새로 만들면 재생 위치·버퍼·선택한 자막이 초기화되고, iOS에서는 사용자 동작 없는 자동 재생이 차단될 수 있습니다. 모바일 WebView에는 전체화면 API가 없거나, 첫 터치가 엉뚱한 버튼을 누르는 문제도 있었습니다.

ElementsEvent Listeners재생 UX
  1. 표시 모드와 재생 객체의 수명을 나누기같은 영상의 전체 ↔ 미니 전환은 영상 요소와 세션을 그대로 옮기고, 콘텐츠가 바뀔 때만 새로 만들었습니다. 검증도 이 기준으로, 전환 뒤 영상 요소가 같은 인스턴스인지 확인하게 했습니다.적용
  2. 전체화면은 단계적으로표준 Fullscreen API, iOS 네이티브 전체화면, 둘 다 안 되면 화면에 고정하는 대체 모드 순으로 연결했습니다. 대체 모드는 배경 스크롤을 잠그고 Safe Area를 반영하며, 나갈 때 원래 스크롤 설정을 되돌립니다.적용
  3. 첫 터치는 컨트롤만 보여주기모바일 브라우저는 터치 뒤에 클릭을 한 번 더 만들어 보냅니다. 첫 터치로 컨트롤이 나타나면 그 클릭이 방금 생긴 재생·다음 영상 버튼에 떨어졌습니다. 첫 입력은 컨트롤 표시에만 쓰고, 그다음 입력부터 버튼이 동작하게 했습니다.적용

전체 ↔ 미니 전환

FullViewMiniView <video> 같은 인스턴스 콘텐츠 교체 → 새 <video>

전체화면 대체 경로

Fullscreen APIiOS 네이티브화면 고정 모드

첫 터치

touch컨트롤 표시합성 click → 새 버튼
touch컨트롤 표시만다음 입력부터 동작

open-source/

오픈소스 기여

Terminal

$ open mailto:kickbelldev@gmail.com

함께 풀고 싶은 제품 문제가 있다면

복잡한 구조를 이해하고, 사용자가 느끼는 동작까지 이어서 만듭니다.