이 글에서 다루는 것
이미 만들어 둔 기능 하나를 다른 프로젝트로 옮기고 싶은데, 코드를 그대로 복사하고 싶지는 않은 상황에서 배운 두 가지를 정리한다. 기능은 "화면에 보이는 행만 그리는(가상화) 무한 스크롤 리스트 + 뒤로가기/새로고침해도 위치가 복원되는" 것이었다.
첫째는 어떻게 옮길 것인가 — 구현 코드가 아니라 "이 기능은 겉으로 어떻게 행동하나"를 계약으로 적는 클린룸 방식. 둘째는 그 과정에서 다시 확인한 무한 스크롤의 핵심 데이터 문제 — 목록을 offset으로 자르느냐 keyset(커서)으로 자르느냐. 특정 회사·제품 이야기는 없고, 어디서나 통하는 일반 개념만 다룬다.
오늘 다룬 것
| 작업 | 무엇을 하고 싶었나 | 한 일 | 결과 |
|---|---|---|---|
| 클린룸 계약 | 기능을 코드 복사 없이 다른 스택으로 이관 | 구현 코드를 빼고 "겉보기 행동 + 실패 보장"만 계약으로 문서화 | 옮기는 쪽이 라이브러리 선택에 안 묶임 |
| keyset 페이지네이션 | 무한 스크롤이 삽입/삭제에도 안 깨지게 | offset의 문제를 정리하고 양방향 커서 계약으로 대체 | 중복·누락 제거, 깊은 페이지도 일정한 성능 |
1. 코드가 아니라 "행동"을 옮기는 클린룸 방식
어떤 상황이었나. 잘 동작하는 기능을 다른 프로젝트로 옮기려는데, 그 프로젝트는 우리와 다른 라이브러리를 쓸 수도 있다. 코드를 그대로 복붙하면 우리의 도구 선택(특정 데이터 페칭 라이브러리, 특정 가상화 라이브러리)을 강제하게 된다.
핵심 개념. 클린룸(clean-room) 이관은 원본 코드를 한 줄도 복사하지 않고, "이 기능이 겉으로 어떻게 행동하는가"만 문서로 넘겨 상대가 처음부터 새로 구현하게 하는 방식이다. 여기서 구분이 중요한데, 능력(capability) 은 "무한히 스크롤되고 뒤로가면 위치가 복원된다"는 겉보기 행동이고, 도구(tooling) 는 그걸 이루는 특정 라이브러리다. 능력만 적으면 도구 선택은 옮기는 쪽 자유다. 요리에 비유하면 "따뜻한 국물 요리"가 능력, "이 냄비"가 도구다 — 레시피(능력)만 건네고 남의 주방 도구는 안 가져간다.
한 일. 기능을 네 계층의 행동 계약으로 분해했다: (1) UI/가상화 — 보이는 행만 DOM에 두고 행 높이는 측정값 → 저장된 높이 → 평균 추정 순으로 정한다, (2) 도메인/모델 — 양방향 페이지 배열과 메모리 상한, (3) 전송/API — 쿼리 조립과 실패 시 throw, (4) 복원 — 스냅샷 스키마와 복원 판정. 각 계약 항목에는 "직접 확인함 / 관찰함 / 추론함 / 모름" 라벨을 붙여, 읽는 사람이 검증된 사실과 추측을 구분하게 했다. 구현 코드 블록은 한 줄도 넣지 않고, 대신 "실패하면 무엇을 보장하나"(실패 시나리오)와 "이건 하지 않는다"(non-goal)를 명시했다.
교훈. 기능 이관 문서의 핵심은 우리가 쓴 코드가 아니라 겉보기 행동 + 실패 시 보장이다. 근거마다 확신 라벨을 붙이면 옮기는 쪽이 위험을 관리할 수 있다.
2. 무한 스크롤의 안정성은 페이지네이션에서 온다
어떤 상황이었나. 리스트를 계속 아래로(또는 위로) 이어 붙이는데, 그 사이 새 글이 추가되거나 삭제되면 목록이 어긋난다. 방금 본 마지막 글이 다시 나오거나(중복), 있던 글을 건너뛴다(누락).
핵심 개념. 원인은 대개 페이지네이션 방식이다. offset 페이지네이션은 "11번째부터 10개"처럼 건너뛸 개수로 페이지를 요청한다(LIMIT 10 OFFSET 10). 뒤로 갈수록 앞 행을 전부 세느라 느려지고(O(offset)), 목록이 실시간으로 바뀌면 항목이 밀려 중복·누락된다. keyset(cursor) 페이지네이션은 "마지막으로 본 글의 정렬키 이후를 줘"처럼 마지막 본 값을 좌표로 쓴다. 인덱스로 그 지점을 바로 찾으니 빠르고(O(log n)), 사이에 삽입·삭제가 있어도 "이 값 다음"은 변하지 않아 안정적이다. 책에 비유하면 offset은 페이지 번호(누가 중간에 페이지를 끼우면 어긋남), keyset은 책갈피(마지막 읽은 문장 기준으로 이어 읽기)다.
한 일. 무한 스크롤 API를 커서 기반 양방향 계약으로 정리했다. 위로 스크롤하면 before(더 최신), 아래로 스크롤하면 after(더 오래된) 커서를 쓰고, 첫 요청은 커서 없이 시작한다. 잘못된 커서/범위 밖 limit은 조용히 첫 페이지로 폴백하지 않고 400으로 즉시 실패시켜 클라이언트가 위치 상실을 알게 했다. 계속 쌓이는 페이지는 유지 개수 상한을 둬 메모리를 bound했다.
교훈. 무한 리스트의 안정성 대부분은 offset이냐 keyset이냐에서 갈린다. 잘못된 입력은 조용히 삼키지 말고 빠르게 실패시키는 게 위치 상실을 숨기지 않는 길이다.