jongchan.kim kickbelldev@gmail.com

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

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

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

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

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

A를 열고 B로 이동한 뒤 다시 A로 돌아오면, 처음 연 A의 응답이 새 화면보다 늦게 도착할 수 있습니다. 대상 ID가 같다는 이유만으로 그 응답을 반영하면 이전 실행이 지금 보고 있는 상태를 덮어씁니다.

요청의 순서

같은 대상의 요청은 순서를 지킵니다. 이전 종료가 끝난 뒤 새 시작을, 저장이 끝난 뒤 조회를 보냅니다. 다른 대상까지 막지는 않습니다.

응답을 반영할 권한

A1과 A2는 같은 대상이지만 다른 실행입니다. 실행 구간마다 토큰을 두어 현재 실행만 화면 상태를 바꿀 수 있게 합니다.

// 왜 이 구조였나

문제
영상을 전환하거나 다시 열면 진도와 이어보기 위치가 실제 재생과 어긋났습니다. 같은 콘텐츠의 요청 순서가 지켜지지 않은 탓이었습니다. 이전 실행의 종료와 새 실행의 시작이 엇갈리거나, 저장이 끝나기 전의 조회 결과가 늦게 도착해 위치를 되돌렸습니다.
순서
같은 콘텐츠의 요청은 순서를 지킵니다. 이전 실행의 종료가 끝난 뒤 새 실행을 시작하고, 진도는 저장이 끝난 뒤에 조회합니다. 다른 영상으로 넘어가는 것까지 늦어지지 않도록 같은 콘텐츠에만 적용했습니다.
권한
콘텐츠 ID가 같아도 실행이 다르면 다른 것으로 봅니다. 이전 실행의 응답은 그 실행을 마무리하는 데만 씁니다.
단순하게
숫자로 ‘세대’를 세던 방식 대신, 실행 하나를 하나의 수명으로 보고 끝난 실행의 늦은 결과는 반영하지 않는 기준으로 정리했습니다.
결과
세션 복구 보완과 함께 2주간 접수된 관련 문의 3건을 해소했습니다.

저장이 끝난 뒤에 조회합니다

진도를 저장하는 동안 같은 영상의 진도를 다시 조회하면, 저장 전에 읽은 값이 늦게 도착해 이어보기 위치를 되돌릴 수 있습니다. 같은 콘텐츠의 조회는 저장이 끝난 뒤에 보내고, 재생 중의 저장은 다시 조회하는 대신 저장 응답으로 바로 반영하도록 했습니다.

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

// 왜 이 구조였나

상황
강사는 여러 학습자를 골라 점수와 피드백을 한 번에 등록합니다. 점수와 피드백은 서로 다른 API라 둘 중 하나만 성공할 수 있습니다.
문제
실패한 뒤 그대로 다시 등록하면 이미 반영된 점수까지 다시 보냅니다. 그렇다고 무조건 건너뛰면, 그사이 선택한 학습자나 점수, 서버 쪽이 바뀐 경우를 놓칩니다.
판단
성공한 점수 요청을 누구에게, 어떤 값으로, 어떤 서버 기준에서 보냈는지 기록합니다. 셋이 그대로일 때만 건너뛰고 피드백만 다시 보냅니다.

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

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

여러 제품이 같은 UI를 써도 입력 상태와 저장 순서는 각 화면과 제품 코드에 묶여 있었습니다. 한 번에 보여주던 화면을 단계별 화면으로 바꾸면 같은 동작을 다시 연결해야 했습니다. 공유해야 하는 것은 화면만이 아니었습니다.

// 왜 이 구조였나

문제
공유하던 것은 두 제품의 공통 UI뿐이었습니다. 새 제품은 같은 콘텐츠를 다른 화면으로 보여줘야 했습니다. 예를 들면 설문을 한 페이지에 한 문항씩.
질문
‘어떤 화면을 공유할까’ 대신 ‘화면이 달라도 같아야 하는 것은 무엇인가’를 물었습니다. 답은 입력 상태와 작업 순서였고, 이를 헤드리스 코어로 뽑았습니다.
기준 1
검증 함수만 떼어내는 공통화는 하지 않았습니다. 쓰는 쪽이 같은 흐름을 다시 조립하게 되므로 저장·제출 순서까지 코어에 담았습니다.
기준 2
코어가 제품 상태를 중계만 하는 중간 계층이 되지 않도록, 상태 자체를 코어가 소유하게 했습니다.
Architecture책임 경계
<ViewA>전체 항목 화면
<ViewB>단계별 입력 화면
shared core초안 · 탐색 · 저장 중 입력 보존 · 저장 후 제출 순서
product adapter데이터 변환 · 인증·권한 · API · 완료 기준
제품이 저장·제출 함수를 제공하고, 코어가 그 호출 순서를 조율합니다.

코어에 모은 것

함께 바뀌는 상태와 순서. 초안과 편집 여부, 항목 이동, 저장 중 추가 입력 보존, 저장 후 제출 순서.

제품에 남긴 것

제품마다 다른 이유가 분명한 것. 서버 데이터 변환, 인증·권한, 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만 500kB 가까이 됐습니다. 기본 분할을 막던 설정을 걷어내고, MSW와 분석 도구는 필요한 시점에만 불러오게 했습니다.
인증
클라이언트와 서버가 인증을 각자 처리해 같은 코드가 두 벌이었고, 토큰은 스크립트에서 접근할 수 있었습니다. Route Handler 한 경로로 모으고 토큰을 HttpOnly 쿠키로 옮겼습니다.
Network상품 상세 첫 진입 · JS
PR #245 전
648 kB

35 requests · 648 kB transferred

PR #245 후
510 kB

32 requests · 510 kB transferred · −21%

모든 의존성을 묶던 번들 설정을 제거하고 MSW·Analytics를 동적으로 불러오도록 바꿨습니다. 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 쪽을 확인해 원인을 확정하고 해결했습니다.확인
273운영 오류 이벤트

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

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

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

이번 조사에서 가장 오래 걸린 건, 오류 코드 하나로는 앱·네트워크·파일·CDN을 가를 수 없다는 점이었습니다. 그래서 영상만이 아니라 학습에 필요한 경로 전체의 기록을 다시 설계했습니다. 실패, 오래 걸림(진행 중과 끝남을 구분), 요청은 성공했지만 상태 규칙이 어긋남을 나누고, 어떤 미디어를 어떤 조건에서 재생하다 직전에 무엇을 했는지 함께 남기도록 했습니다.

02

ios · webkit · ai

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

특정 iOS 버전에서 자막이 붙은 영상만 처음부터 재생되지 않았습니다. 앱 WebView와 Safari가 같았고, 이전 iOS와 PC는 정상이었습니다. 고치는 것에서 끝내지 않고, 왜 그 수정이 통하는지 엔진 소스로 확인했습니다.

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

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

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