본문 바로가기

Study

가족 아카이브

가족 아카이브를 만들면서

우리 가족은 평일에는 대부분 흩어져서 지내고, 대신 1년에 두세 번 정도로 자주 다 같이 여행을 간다. 그런데 그렇게 찍은 사진들은 각자의 폰 갤러리에, 또는 여러 개의 단톡방에 흩어져 있었다. 즉, 가족이 함께 쌓아온 히스토리를 한군데 모아두고 같이 들여다볼 공간이 없었던 것이다. 또, 톡방에 올라온 사진은 업로드된 그 순간만 보게 되고, 대화에 밀려 올라가고 나면 거의 찾아보지 않게 된 채로 만료기간이 지나는 일이 허다했다. 이런 것들이 문제로 여겨져 가족 아카이브를 기획하게 되었다.

그래서 가족 8명이 사용할 가족 아카이브를 만들었다. 배포/사용/피드백/개선/배포 루프를 벌써 여러 번 돌았다. 그 안에서 가족들의 목소리로 서비스가 변화하는 모습들을 담아 보았다.

기획: MVP를 최대한 좁히기

처음 정한 요구사항은 단순했다.

  • 여행/행사 사진을 앨범 단위로 모아서 본다.
  • 촬영일자(EXIF) 기준으로 1차 자동 분류하고, 사람이 수동으로 2차 확인·보정한다.
  • 비공개 링크 + 간단한 비밀번호로만 접근한다(계정 시스템 없음).

여기서 제일 오래 고민한 것은 사진을 어떻게 시스템에 넣을 것인가였다. 부모님은 카톡으로 사진을 올리는 것 말고는 익숙한 방법이 없기 때문에, "카톡에 올라온 사진을 자동으로 감지해서 저장"하는 자동화를 진지하게 검토했다. 카카오톡 정식 API로는 불가능했고, 로컬 폴더 워처(카톡 PC버전이 사진을 저장하는 폴더를 감시하다가 새 파일이 생기면 자동으로 서버에 올리는 방식)가 그나마 유력한 대안이었는데, 결국 이것도 만들지 않기로 확정했다. 자동화를 붙이는 비용에 비해, 그냥 관리자(나)가 주기적으로 카톡 대화 내보내기를 실행해서 수동으로 업로드하는 편이 훨씬 간단했기 때문이다. 신규 사진이든 기존에 쌓여 있던 사진이든 같은 방식으로 통일하기로 하고, 태그·인물 검색이나 개인별 로그인 같은 것들은 전부 후순위로 미뤘다.

기술 스택은 Next.js(React) + Cloudflare Workers(R2 오브젝트 스토리지 포함) + Supabase(Postgres)로 정했다. 사진(바이너리)과 메타데이터(앨범/날짜 등 구조화 데이터)를 분리해서, 대용량 파일은 R2에(egress가 무료라 가족이 반복해서 사진을 열어보는 트래픽에 유리하다), 조회·필터링이 필요한 데이터는 Supabase에 두는 구조다. CI/CD도 별도 파이프라인을 만들지 않고 플랫폼에 내장된 Git 연동만 쓰기로 했다. 예전에 GitHub Actions + AWS EC2로 배포하면서 서버 인스턴스를 직접 관리해야 했던 경험과 비교해 보면, 관리할 인프라 자체가 없는 쪽이 "본인이 직접 개발·운영하되 운영비는 최소화"라는 제약에 훨씬 잘 맞는 것 같았다.

여기서 한 번 헛다리를 짚었다. 배포 대상을 처음엔 Cloudflare Pages로 적어뒀는데, 실제로 대시보드에서 무엇을 연결해야 하는지 확인하는 과정에서 우리가 쓰기로 한 OpenNext 어댑터(@opennextjs/cloudflare)가 Next.js 앱을 Pages가 아니라 Workers용 워커 번들로 빌드한다는 것을 뒤늦게 알았다. Cloudflare 자체도 Pages의 기능들을 Workers로 흡수 통합하는 중이었다. 배포 대상을 Workers로 정정했는데, 다행히 R2/Supabase 같은 나머지 아키텍처는 그대로 두면 됐고 어떤 제품에 배포되는가만 바뀐 셈이라 크게 돌아가지는 않았다.

AI 시대라 가능했던 빠른 배포→피드백 루프

기획을 확정하고 2~3일 만에 프로토타입을 배포까지 올렸다. 테이블 스키마, 비공개 링크와 비밀번호 인증, 사진 업로드와 EXIF 추출, 촬영일자 기반 자동 클러스터링, 앨범 목록과 그리드 뷰까지 이 안에 들어갔다. 예전 같으면 기획 문서를 붙잡고 며칠은 더 고민했을 결정들(색상 톤을 무엇으로 할지, 홈 화면 구조를 어떻게 짤지)도 일단 만들어서 배포하고 실제로 써보는 쪽으로 넘겼다. AI 페어링으로 프로토타입을 만드는 속도가 예전과 비교가 안 될 만큼 빨라진 덕이다.

그 뒤로 지금까지의 시간은 대부분 고치는 데 썼다. 만드는 것은 며칠이었고 쓸 만하게 만드는 것은 그보다 훨씬 길었다. 완벽하게 기획해서 한 번에 맞추려 하는 대신 빠르게 내놓고 진짜 반응으로 고쳐나가는 쪽이 나았던 것 같은데, 다만 그 "진짜 반응"의 대부분이 내가 직접 써보며 나온 것이었다는 점은 뒤에서 다시 이야기하겠다.

배포하고 나서 코드를 다시 점검해 보니 이미 어긋나 있는 부분이 하나 있었다. 기획 단계에서는 업로드를 관리자 한 명만 할 수 있게 정해뒀는데, 실제 구현된 로그인 구조는 가족 전체가 같은 비밀번호를 공유하는 방식이었고 API 레벨에서 업로드·정리를 제한하는 코드 자체가 없었다. 즉, "가족은 열람만"이라는 전제가 지켜지지 않은 채 방치돼 있었던 것이다. 이 간극을 없애면서 방향은 원래 정했던 것과 반대로 정리했다. 업로드와 앨범 정리는 가족 전체에게 열어주고, 되돌릴 수 없는 유일한 동작인 사진 삭제만 관리자 전용으로 제한했다. 삭제는 R2 원본과 DB 행을 동시에 지우는 동작이라 사고 시 피해가 가장 크고, 반대로 나머지는 잘못해도 다시 고칠 수 있으니 권한 분리 비용을 삭제 하나에만 들이는 것이 적절하다고 판단했다.

화면과 자잘한 에러들

처음 배포된 화면은 Tailwind 기본값 그대로였다. 디자인이라고 부를 만한 것이 아예 없어서 색상·타이포·간격을 정의한 경량 디자인 시스템부터 만들었다. 이때 고른 톤은 따뜻하고 사적인 느낌의 웜 브라운 계열이었는데, 막상 배포된 화면을 보니 배경이 심심했다. 그래서 파스텔 하늘색 그라데이션으로 전면 전환했고, 얼마 지나지 않아 하늘색 하나로 고정하는 대신 분홍·연두·살구·하늘 네 가지를 고를 수 있게 다시 바꿨다. 톤을 두 번 뒤집은 셈인데, 매번 그 시점에서는 그것이 맞다고 판단했던 것이고 실제로 보고 나서야 아니었다는 것을 알았다.

사진 확대 기능에서는 같은 증상이 세 번 반복됐는데, 원인은 매번 달랐다. 처음엔 관리 버튼(세로 점 3개)이 아예 보이지 않았다. PhotoSwipe 라이브러리에 버튼을 등록할 때 appendTo: "bar" 설정을 빼먹어서, 등록은 되는데 화면에 붙지 않은 것이었다. 이것을 고치자 이번엔 버튼이 보이기는 하는데 다크 배경에 다크 아이콘이라 보이지 않았다. 라이브러리 CSS를 직접 열어 보니 아이콘이 currentColor를 쓰고 있어서 앱의 어두운 글자색을 그대로 물려받고 있었다. 그다음엔 관리 메뉴가 사진 뒤에 가려지고 클릭도 먹히지 않는데, 확대된 사진을 닫으면 그제서야 눌리는 증상이 나왔다. 그렇다면 왜 닫아야만 눌리는 것일까? PhotoSwipe의 CSS 변수(--pswp-root-z-index: 100000)를 확인하니 답이 나왔다. 내 메뉴의 z-index가 10000으로 10배 낮게 잡혀 있어서, 사진 레이어가 통째로 메뉴 위에 얹혀 있었던 것이다. 결국 가려 보이는 것도, 클릭이 사진 쪽으로 먹히는 것도, 라이트박스를 닫아야만 눌리는 것도 전부 이 하나로 설명됐다.

사진이 눌려 보이는 버그도 비슷했다. 처음엔 사진 크기를 못 읽어서 임시 정사각형 크기를 쓰고 있나 싶어 프로덕션에 올라온 사진 전부를 curl로 순회해 확인했는데, 전부 정상적으로 다양한 실제 치수가 잡혀 있어서 이 가설은 기각했다. 여기서 다음 의문이 들었다. 치수가 멀쩡한데 왜 비율이 깨질까? 치수를 읽는 라이브러리(image-size) 문서를 다시 읽어 보니, EXIF의 회전 정보를 반영하지 않고 원본 픽셀 크기를 그대로 돌려준다는 것을 확인할 수 있었다. 예를 들어, 세로로 찍은 폰 사진이 실제로는 가로 픽셀로 저장돼 있고 EXIF 태그로만 "세로로 돌려서 보여줘"라고 돼 있는 경우, 브라우저는 돌려서 보여주지만 그 크기를 그대로 받은 라이트박스는 회전 전 비율로 프레임을 계산해 버린다. 회전이 필요한 경우 width와 height를 서로 바꿔주는 로직을 추가해서 고쳤다.

가장 오래 끈 것은 모바일에서만 나는 화면 잘림이었다. 긴 이름의 앨범에 들어가면 화면이 옆으로 밀렸다. 처음엔 앨범 제목에 한 줄로 자르는 CSS(truncate)를 걸고 업로드 버튼 폭을 고정하는 것으로 대응했는데 계속 재발했다. 데스크톱 개발자도구의 반응형 모드로는 재현이 아예 되지 않아서, 갤럭시 화면을 직접 캡처하고 실제로 보이는 영역에 표시를 해가며 비교했다. 핀치줌으로 축소하면 잘려 보이던 부분이 원래 크기로 다 보인다는 것, 이름이 짧은 앨범에서는 재현이 되지 않는다는 것 두 가지를 확인하고 나니 원인이 좁혀졌다. 모바일 브라우저가 페이지를 처음 그릴 때, 줄바꿈이 금지된 긴 글자의 원래 폭을 기준으로 화면 폭 자체를 잘못 계산하고 있었던 것이다. 우선 overflow-x: hidden으로 막았는데 이건 "화면이 확대되는" 증상을 "넘친 부분이 잘려 보이는" 증상으로 바꾼 것에 가까웠고, 근본 원인은 그대로 남아 있었다.

그리고 정말로 다시 터졌다. 앨범 목록 디자인을 개선하면서 제목에 한 줄 고정을 다시 걸었더니 똑같은 버그가 재발했고, 이번엔 하단 버튼까지 잘려 보였다. 프레임워크를 바꿔야 하나 싶은 생각까지 들었는데 답은 CSS 속성 하나였다. 줄바꿈을 강제로 막는 white-space: nowrap을 걷어내고, 자연스럽게 줄바꿈되면서 최대 두 줄까지만 보여주는 line-clamp으로 바꾸고 나서야 사라졌다. 텍스트가 애초에 줄바꿈될 수 있으면 브라우저가 폭을 잘못 잴 소재 자체가 없어지는 것이었다.

프로토타입 배포와 피드백 반영

여기까지가 내가 혼자 붙잡고 고친 것들인데, 그러는 사이 가족에게서 피드백이 왔다. 그리고 가장 크게 바뀐 지점은 업로드와 탐색 흐름 자체였다. 원래는 먼저 업로드하고 나중에 앨범 정리 탭에서 분류하는 구조였는데, 써보니 이 순서가 전혀 직관적이지 않다는 것이었다.

첫 피드백 받을 당시의 화면


앨범 정리 탭은 사진도 보이지 않고 형편없다는 것, 업로드는 각 앨범에 들어가서 바로 할 수 있어야 하고 이 순서 자체가 사용성에 아주 중요하다는 것, 사진을 눌렀을 때 확대까지 탭이 너무 많다는 것. 이 피드백을 받고 전역 업로드·정리 탭을 아예 없애고 각 앨범 페이지 안에서 바로 업로드하도록 바꿨다. 그리드 썸네일도 별도 상세페이지를 거치지 않고 곧바로 확대되도록 줄였고, 다중 선택은 안드로이드 갤러리 앱과 같은 롱프레스 패턴으로, 사진과 앨범 관리는 세로 점 3개 버튼으로 통일했다.

카카오톡으로 받은 피드백


또 하나는 홈 화면 구조에 대한 것이었다. 맨 위에 "새 앨범 만들기", "앨범 없이 업로드", "미분류 사진 안내" 버튼 세 개를 나란히 띄워뒀는데, 메인화면 맨 위에서 선택지를 주지 말라는 것이었다. 버튼 설명이 길어서 혼란스럽고, 서비스가 가진 복잡성을 미리 던지지 말고 정해진 한 길을 따라가면서 필요한 것을 마주치게 하라는 이야기였고, 전적으로 공감이 되는 지적이었다. 목록 안에 조용히 흡수시키는 안, 역할별로 다르게 보여주는 안을 놓고 고민한 끝에 버튼 자체는 그대로 두고 위치만 앨범 목록 아래로 옮겼다. 홈에 도착하면 앨범들만 먼저 보이고, 무언가 만들거나 올리고 싶을 때만 그 행동을 마주치는 구조가 됐다.

두 지적 모두 동작이 잘못된 것이 아니라 쓰기 불편하다는 종류였다. 내가 혼자 잡아낸 것들이 대체로 "잘못 동작하는 것"이었다면, 여기서 온 것은 "동작은 하는데 순서가 틀린 것"이었고 그것이 구조를 바꿨다.

조회 성능 트러블슈팅

기능이 자리를 잡고 나니 다음으로 걸린 것은 속도였다. 앨범을 열고 사진이 보이기까지 2~3초가 걸렸는데, 사진이 몇 장 없어도 그만큼 걸렸다. 뒷부분이 결정적인 단서였다. 사진이 많아서 느리면 데이터가 많으니 그렇겠거니 하고 넘어갔을 텐데, 두 장짜리 앨범도 똑같이 느리다면 사진 수와 무관한 고정 비용이 있다는 뜻이다. 그래서 코드를 고치기 전에 먼저 쟀다. 앨범 페이지의 HTML만 받는 데 1.4초에서 3.9초가 걸렸는데, 사진 이미지는 아직 한 장도 받지 않은 시점이고 그 HTML은 14KB밖에 되지 않는다. 참고로 사진 한 장은 4.76MB, 약 1.9초였다.

그렇다면 14KB짜리 텍스트를 받는 데 몇 초가 걸리는 이유는 무엇일까? 네트워크 문제는 아닐 것이다. 즉, 서버가 그 텍스트를 만드는 동안 무언가를 하고 있다는 뜻이다.

첫 번째 원인은 매번 사진 파일을 열어보고 있었다는 것이다. 앨범 페이지를 그릴 때마다 사진 하나하나에 대해 저장소에서 파일 앞부분 256KB를 읽어와 헤더를 뜯어서 크기를 계산하고 있었다. 사진을 확대했을 때 비율이 깨지지 않으려면 그리기 전에 가로세로를 알아야 해서 넣은 코드인데, 문제는 이것을 페이지를 열 때마다 다시 하고 있었다는 것이다. 한 번 계산하면 바뀌지 않는 값인데도 그랬다. 업로드할 때 한 번만 재서 DB에 저장하고, 조회할 땐 읽기만 하도록 바꿨다.

두 번째 원인은 150픽셀 자리에 4000픽셀짜리를 통째로 내려받고 있었다는 것이다. 그리드의 썸네일은 화면에서 150픽셀 남짓인데 브라우저는 원본을 그대로 받아 축소해 그리고 있었다. 그래서 썸네일을 만들어야 했는데, 선택지가 셋이었다. Cloudflare의 이미지 변환 서비스는 코드가 제일 적게 드는 대신 유료 플랜과 도메인 설정이 필요했고, 지금 쓰는 workers.dev 주소에서는 동작하지 않을 가능성이 높았다. 서버에서 리사이즈하는 것은 Worker의 번들 용량 제한 때문에 위험했는데, 이미지 처리 라이브러리가 수백 KB에서 1MB에 달하기 때문이다. 결국 브라우저에서 만들기로 했다. 브라우저는 어차피 이미지를 화면에 그리려고 디코딩하므로, 업로드 직전에 작은 버전을 하나 더 만들어 원본과 같이 올리면 비용도 0이고 새 라이브러리도 필요 없다. 덤으로 브라우저는 EXIF 회전이 반영된 실제 표시 크기를 알 수 있어서, 썸네일을 만드는 김에 크기도 같이 보내면 서버가 파일을 열어볼 이유가 완전히 사라진다. 첫 번째 문제까지 함께 해결됐고, 그리드가 받는 이미지는 4.76MB에서 46KB로 줄었다.

세 번째 원인은 사진 여덟 장을 지우면 페이지를 여덟 번 다시 그린다는 것이었다. 이건 "삭제 반영이 느리다"는 별개 증상으로 보였는데 파보니 같은 뿌리였다. 삭제 API가 완료 후 앨범 페이지로 돌아가라는 응답(302 리다이렉트)을 주고 있었고, 브라우저의 fetch는 이 리다이렉트를 자동으로 따라간다. 그래서 여덟 장을 동시에 지우면 삭제 여덟 번에 더해 앨범 페이지 전체 렌더링이 여덟 번 일어났다. 화면에는 아무것도 쓰이지 않는 결과물인데도 그랬다. 여러 장을 한 번에 처리하는 전용 API를 만들고, 리다이렉트 대신 결과만 돌려주게 바꿨다.

네 번째 원인은 이미지마다 DB에 물어보고 있었다는 것이다. 여기까지 하고도 0.6초쯤 남아 있었다. 커넥션을 재사용해 다시 재보니 썸네일 한 장당 0.57초였는데, 20KB짜리 파일에 이 시간이 나올 이유가 없다. 경로를 보니 이미지 요청 하나하나가 인증 확인 → DB에 저장소 키 물어보기 → 저장소에서 파일 꺼내기 순서로 돌고 있었다. 이미지마다 DB 왕복이 한 번씩 끼어 있었고 그것이 0.5초의 대부분이었다. 썸네일 파일 이름을 사진 ID에서 바로 계산되게 규칙화해서, DB에 물어볼 필요 자체를 없앴다.

그리고 내가 만든 회귀가 하나 있었다. 개선 결과를 표로 정리해 놓고 보니 캐시 미적중일 때가 오히려 이전보다 느렸다. 엣지 캐시를 붙이면서 파일을 꺼낸 뒤 캐시에 저장이 끝날 때까지 기다렸다가 응답을 보내도록 짜놨던 것이다. 캐시는 다음 사람을 위한 것인데 지금 기다리는 사람이 그 값을 치르는 구조였다. 응답을 먼저 보내고 저장은 뒤에서 하도록 고쳤고, 같은 조건에서 다시 재니 0.57초에서 0.40초로 줄었다. 여기서 더는 내려가지 않는데, 그 0.4초를 분해하면 네트워크 왕복이 약 0.2초고 나머지는 모든 요청이 Next.js를 통과하는 비용이다. 파일 하나 꺼내는 API인데도 프레임워크 라우팅을 거쳐야 하기 때문이다.

측정 자체도 한 번 의심해야 했다. 나중에 구간을 쪼개 보니 처음 잡았던 1.4~3.9초 중 0.6초쯤은 curl이 매 요청마다 새로 TLS 핸드셰이크를 맺느라 든 시간이었다. 실제 브라우저는 연결을 유지하므로 화면을 옮길 때마다 이 값을 내지 않는다. 연결을 재사용해 재보니 같은 페이지가 0.157초에 나왔다. 원인 자체가 틀렸던 것은 아니지만, 문제의 크기를 실제보다 크게 보고 있었던 셈이다.

두 번째 피드백: 기기 알림

가족에게서 온 다음 피드백은 알림이었다. 요즘 폰 갤러리 앱들이 "작년 이맘때의 추억을 되새겨 보세요" 같은 알림을 주는 것에서부터 시작해서, 더 대화를 나눠 보고 우리 서비스에 맞게끔 앨범별로 알림을 전송하는 기능이 있으면 좋을 것이라는 결론까지 나왔다. 사진을 정리해 두는 것만으로는 "다시 찾아보게 만든다"는 원래 목적이 절반만 달성되고, 찾아볼 이유를 먼저 건네줘야 한다는 점에서 이 아카이브에 꼭 필요한 기능이기도 했다.

아빠 : 왜 요즘 핸드폰에 사진 보라고 뭐 오잖냐
나 : ...!
아빠 : 가족앨범에서 그렇게 옛날 사진을 상기시켜주면 더 자주 들어가볼거같어


조건은 서비스에 접속해 있을 때만 뜨는 인앱 알림이 아니라 기기 알림이어야 한다는 것이었다. 웹에서 이것을 하려면 서비스워커와 웹 푸시를 써야 한다. 서비스워커는 구독할 때 기기에 한 번 설치되고, 그다음부터는 브라우저 탭이나 창과 무관하게 상주한다. 서버가 푸시를 보내면 구글 푸시 서버가 폰을 깨우고 → 폰이 브라우저를 → 브라우저가 서비스워커를 깨워서 알림을 띄운다. 이 과정 어디에도 열려 있는 탭은 필요 없다.

여기서 두 가지를 의도적으로 덜 만들었다. 하나는 푸시에 본문을 싣지 않은 것이다. "경주여행, 작년 오늘" 같은 개별 문구를 실으려면 별도 암호화 규격을 직접 구현해야 하는데, 실기기 없이 맞추기 까다로운 부분이라 1차에서는 알맹이 없는 신호만 보내고 문구는 서비스워커에 고정해 뒀다. 배관이 확실히 도는지 먼저 확인하는 것이 순서인 것 같았다. 다른 하나는 스케줄러를 본체에서 떼어낸 것이다. Cloudflare 크론을 쓰려면 Worker에 스케줄 핸들러가 있어야 하는데, 본체 Worker는 OpenNext가 매 빌드마다 새로 생성한다. 그 파일을 감싸는 진입점을 만들 수도 있었지만 이미 빌드가 자주 실패하는 상황이라 빌드 경로에 결합을 더하는 것이 위험하다고 봤다. 그래서 판정 로직은 본체 API에 두고, 스케줄러는 그 URL을 하루 한 번 호출만 하는 최소 Worker로 분리했다. 같은 계정의 Worker끼리 서로를 호출하면 루프 방지에 걸려 error 1042가 나는 함정이 하나 있었는데, 이건 플래그 하나로 해결됐다.

기념일 판정은 앨범의 첫 날짜(사진 중 가장 이른 촬영일자, 없으면 업로드 시각)의 월·일이 오늘과 같고 연도가 과거인지로 한다. 그래서 1년 뒤 한 번이 아니라 매년 다시 걸린다. 같은 앨범을 하루에 두 번 알리지 않도록 발송 이력에 유니크 제약을 걸었고, 앱을 지웠거나 구독을 해지한 기기는 푸시 서버가 404나 410을 주면 그 자리에서 정리한다. 강제 날짜로 판정 로직을 검증하고, 마지막엔 실제 폰으로 알림이 도착하는 것까지 확인했다. 다만 아이폰은 홈 화면에 추가해야만 웹 푸시를 받는다는 제약이 남아 있어서, 가족 중 아이폰 사용자에게는 별도 안내가 필요하다.

인사이트

이 과정에서 배운 것 중 가장 큰 하나는, 생각보다 나 자신이 이 서비스를 객관적으로 평가하지 못했다는 것이다. 만들면서 나는 계속 판단을 내렸다. 이 톤이 맞다, 이 정도면 보기 괜찮다, 이제 쓸 만해졌다. 그런데 그 판단은 대체로 오래가지 못했다. 웜 브라운이 맞다고 정한 뒤 배포된 화면을 보고 파스텔로 뒤집었고, 파스텔로 고정한 뒤 다시 네 가지 선택으로 바꿨다. 앨범 목록을 손보고 "이제 좀 볼 만하다"고 생각한 직후에 화면 잘림 버그가 재발했다. 성능도 마찬가지였다. 느리다고 느끼기 전까지는 문제가 있다는 생각조차 하지 않았고, 막상 재보니 내가 짐작하던 곳이 아니라 매 요청마다 파일을 다시 열어보던 코드가 원인이었다.

더 결정적인 것은 내가 잡아낸 것들과 가족이 짚어준 것들의 성격이 달랐다는 점이다. 내가 혼자 찾은 것은 거의 다 잘못 동작하는 것들이었다. 버튼이 보이지 않고, 사진이 눌리고, 화면이 밀리고, 느렸다. 반대로 가족이 말해준 두 가지는 전부 동작에는 문제가 없는 것들이었다. 업로드를 어디서 하느냐, 홈 화면에서 무엇을 먼저 보여주느냐. 그것은 내가 아무리 오래 들여다봐도 나오지 않았을 종류였다. 나는 이 서비스가 어떻게 동작하는지 이미 알고 있어서, 처음 열어본 사람이 어디서 막히는지를 느낄 수가 없었기 때문이다.

결국 버그는 만든 사람이 잡을 수 있지만 구조는 그렇지 않았다. 그리고 내가 "완성됐다"고 느끼는 감각도 별로 믿을 것이 못 된다는 것을, 이번에 몇 번이나 확인했다.

가족 아카이브의 지금과 미래

처음 그렸던 MVP는 다 채워졌다. 사진을 올리고, 자동으로 앨범이 묶이고, 가족이 링크로 들어와 보고, 기념일에 알림이 간다. 성공 기준으로 적어뒀던 두 가지, 즉 흩어져 있던 사진을 정리할 수 있을 것과 가족이 비공개 링크로 들어와 앨범 단위로 볼 수 있을 것도 충족됐다.

지금의 가족 아카이브


디자인은 아직 손대는 중이다. 앨범 목록을 실제 책장에 책이 꽂힌 모습으로 바꾸고, 앨범을 열면 표지가 책등을 축으로 회전하며 펼쳐지도록 만들었고, 배경은 계절에 따라 바뀌게 했다. 대신 비용도 늘었다. 애니메이션 라이브러리를 들이면서 클라이언트 JS가 730KB에서 853KB가 됐고, 앨범을 열 때 800ms짜리 연출이 붙으면서 방금 0.4초까지 줄여 놓은 진입 속도 위에 그만큼이 다시 얹혔다. 성능을 그렇게 파놓고 스스로 되돌린 셈인데, 이건 알고 한 맞바꿈이다. 부모님 세대가 "이게 사진첩이구나"를 한눈에 알아보는 쪽이 0.8초보다 값어치가 있다고 봤기 때문이다. 물론 이 판단이 맞았는지는 아직 모르겠다. 앞 문단에 적었듯, 내가 맞다고 느끼는 감각은 이 프로젝트에서 여러 번 틀렸다.

사용 피드백은 받았지만, 아직 가족들의 사진 기록이 온전하게 저장돼 있지 않고, 가족들도 온전히 받아들이지는 않았다. 아마 가족사진을 보기 위해 갤러리 대신 여길 찾기까지는 시간이 필요하거나, 일어나지 않을 일일지도 모른다. 그렇지만 나는 꿋꿋히 이 아카이브를 개선하고 공유하고 사용을 장려할 것이다. 유저 피드백을 받아보고 개선 루프를 경험해 본 사이드 프로젝트로서가 아니라, 이 아카이브가 해결하려 했던 문제를 진짜 해결할 때까지 말이다.

'Study' 카테고리의 다른 글

무엇을 단위로 볼 것인가?  (0) 2026.08.11
DDD 는 silverbullet일까?  (0) 2026.08.03
코드리뷰를 왜 할까?  (0) 2026.07.29
자소서 퍼블리셔 스킬 만들기  (0) 2026.06.07
피드백서비스 개선기  (0) 2026.01.22