안정된 것처럼 보였던 순간 — 그러나 질문은 끝나지 않았다

세션 토글을 정리하고 나서야 처음으로 구조가 “정돈되었다”는 감각이 생겼다. 이전까지는 어디서 무엇이 잘못되고 있는지도 명확하지 않았고, 문제를 고치면 다른 곳에서 또 다른 문제가 튀어나오는 상황이 반복되었다. 하지만 UseCase 안으로 정책을 모으고, 상태와 계산을 분리하고 나니, 적어도 시스템이 어디에서 움직이고 있는지는 보이기 시작했다. 각 요소가 어떤 역할을 가지고 있는지도 명확해졌다. 이전처럼 모든 것이 뒤섞인 상태는 아니었다. 그래서 이 시점에서는 자연스럽게 이런 생각이 들었다. 이제 큰 문제는 지나간 것 아닐까라는 생각이다.

하지만 이 안정감은 오래 가지 않았다. 구조가 정리되자마자, 이전에는 보이지 않던 질문이 하나 떠오르기 시작했기 때문이다. “이 모든 변화는 어디에 남는가?”라는 질문이었다. 세션이 시작되면 시작 이벤트가 생긴다. 세션이 종료되면 종료 이벤트가 생긴다. 진행률이 100%가 되면 완독 이벤트가 생긴다. 이 모든 것은 단순히 상태 변화가 아니라, 시간 위에 남는 기록이다. 그런데 이 기록을 어떻게 다룰 것인지에 대해서는, 아직 아무것도 정의되지 않은 상태였다.

이 질문은 단순히 로그를 남길 것인가, 데이터베이스에 저장할 것인가의 문제가 아니었다. 그보다 더 근본적인 문제였다. 이 기록이 시스템에서 어떤 역할을 가지는가, 그리고 이 기록을 어떤 형태로 정의할 것인가라는 문제였다. 세션이라는 구조는 상태를 다루기 위한 것이었지만, 이벤트는 상태와는 다른 성질을 가진다. 상태는 현재를 설명하지만, 이벤트는 과거를 남긴다. 이 차이를 어떻게 모델링할 것인가가 다음 단계의 핵심이 되기 시작했다.

결국 이 시점에서 우리는 또 하나의 경계에 서게 된다. 세션 구조를 정리하면서 “정책”을 코드로 옮겼다면, 이제는 “기록”을 어떻게 정의할 것인가를 결정해야 한다. 그리고 이 결정은 단순한 저장 방식의 문제가 아니라, 도메인 모델 자체를 다시 한 번 흔들 수 있는 문제였다. 안정된 것처럼 보였던 구조 위에서, 또 하나의 질문이 설계를 다시 밀어내기 시작하고 있었다.

Timeline을 처음 바라본 방식 — 상태를 저장하려는 유혹

이 질문에 대한 첫 번째 반응은 매우 자연스러운 방향으로 흘러갔다. 기록을 남겨야 한다면, 그 기록을 바로 사용할 수 있는 형태로 저장하면 되지 않을까라는 생각이었다. 특히 Timeline이라는 UI를 떠올리면, 이 유혹은 더 강해진다. 사용자가 보는 것은 결국 카드 형태의 기록이고, 그 카드에는 책 제목, 진행률, 세션 길이, 그리고 다양한 정보들이 함께 표시된다. 그렇다면 이 모든 데이터를 미리 계산해서 저장해두면, UI는 훨씬 단순해질 것처럼 보인다.

이 접근 방식은 개발자의 입장에서 매우 매력적이다. 데이터를 읽어오면 바로 화면에 그릴 수 있고, 추가적인 계산이나 조회가 필요 없다. 성능 측면에서도 이점이 있을 것처럼 보인다. 그래서 처음에는 TimelineEvent를 단순한 이벤트가 아니라, UI에 최적화된 데이터 구조로 만들려는 시도가 자연스럽게 이어졌다. 기록을 남기는 동시에, 그 기록을 바로 보여줄 수 있도록 만드는 것이다.

TimelineEvent
- bookTitle
- progressPct
- sessionDuration
- computedStats

이 구조는 직관적이다. Timeline 카드에 필요한 모든 정보가 하나의 레코드에 들어 있고, 별도의 조합 없이 그대로 사용할 수 있다. 특히 모바일 환경에서는 네트워크나 계산 비용을 줄이는 것이 중요하기 때문에, 이런 방식은 더욱 설득력 있게 느껴진다. “어차피 보여줄 데이터라면, 미리 만들어두자”라는 생각은 매우 합리적으로 보인다.

하지만 이 구조를 조금 더 오래 들여다보면, 묘하게 불편한 감각이 생기기 시작한다. 이 데이터는 과연 “기록”인가? 아니면 “결과”인가? TimelineEvent라는 이름을 가지고 있지만, 실제로는 이벤트의 원인이 아니라, 이벤트의 결과를 저장하고 있는 것처럼 보인다. 그리고 이 차이는 단순한 표현의 문제가 아니라, 설계 방향 자체를 바꿔버릴 수 있는 중요한 차이였다.

어긋남의 시작 — Timeline이 ‘기록’이 아니라 ‘결과’가 되는 순간

이 구조의 문제는 실제 사용 시나리오를 떠올리는 순간 명확해진다. 예를 들어, TimelineEvent에 책 제목을 함께 저장했다고 가정해보자. 사용자가 나중에 책 정보를 수정하면 어떻게 될까? 과거의 Timeline 이벤트는 그대로 남아 있어야 하는가, 아니면 최신 정보로 업데이트되어야 하는가? 어느 쪽을 선택하더라도 문제가 발생한다. 과거를 유지하면 UI가 일관되지 않고, 업데이트하면 기록의 의미가 사라진다.

이 문제는 단순히 책 제목에만 국한되지 않는다. 진행률 계산 방식이 바뀌거나, 세션 길이 계산 정책이 변경되거나, 심지어 UI 표현 방식이 바뀌는 경우에도 동일한 문제가 반복된다. 이미 저장된 TimelineEvent는 과거의 계산 결과를 담고 있기 때문에, 새로운 정책과 맞지 않게 된다. 결국 데이터는 점점 “현재 시스템과 어긋난 과거의 결과”가 되어버린다. 그리고 이 어긋남은 시간이 지날수록 더 커진다.

이 시점에서 하나의 명확한 사실이 드러난다. 파생 데이터는 반드시 언젠가 깨진다는 점이다. 계산된 값은 계산 로직이 바뀌는 순간 의미를 잃는다. 그리고 그 계산 결과를 그대로 저장해두면, 시스템은 점점 더 많은 “죽은 데이터”를 가지게 된다. 이 데이터는 삭제할 수도 없고, 수정하기도 어렵다. 왜냐하면 그것은 과거의 기록처럼 보이지만, 실제로는 과거의 결과이기 때문이다.

결국 Timeline은 더 이상 “기록”이 아니라, “캐시된 결과의 집합”으로 변하기 시작한다. 그리고 이 상태에서는 도메인 모델이 더 이상 신뢰할 수 없는 상태가 된다. 이벤트는 과거의 사실을 나타내야 하는데, 여기서는 과거의 해석이 저장되어 있기 때문이다. 이 차이는 작아 보이지만, 시스템 전체의 방향을 바꿔버리는 수준의 문제다.

이 어긋남을 인식하는 순간, 우리는 다시 처음 질문으로 돌아가게 된다. 우리는 무엇을 남기려고 했는가. 그리고 그 질문은 결국 다음 결정을 강제한다. Timeline은 결과를 저장하는 곳이 아니라, 사실을 기록하는 곳이어야 한다는 방향으로.

질문의 전환 — 우리는 무엇을 남기려고 했는가

Timeline을 결과 중심으로 설계하려던 시도가 어긋나기 시작하면서, 자연스럽게 처음으로 돌아가게 된다. 왜 이 구조를 만들려고 했는가라는 질문이다. 처음에는 단순했다. 사용자가 남긴 기록을 시간 순서대로 보여주기 위해서였다. 하지만 이 목적을 조금 더 깊게 들여다보면, 단순히 “보여주기 위한 데이터”가 아니라 “남겨야 할 데이터”라는 점이 드러난다. 그리고 이 두 가지는 비슷해 보이지만 전혀 다른 방향을 가진다.

보여주기 위한 데이터는 현재의 요구사항에 맞춰 최적화된다. UI가 바뀌면 데이터 구조도 바뀌고, 표현 방식이 달라지면 저장 방식도 함께 수정된다. 반면 남겨야 할 데이터는 UI와 독립적이어야 한다. 그것은 시간이 지나도 의미가 유지되어야 하고, 새로운 해석이 가능해야 하며, 무엇보다 변하지 않아야 한다. 이 차이를 인식하는 순간, Timeline을 바라보는 관점이 완전히 달라진다. 더 이상 이것은 화면을 구성하기 위한 데이터 구조가 아니라, 시스템이 현실을 기록하는 방식이 된다.

이 시점에서 다시 등장하는 개념이 있다. 상태와 이벤트의 차이다. 상태는 언제든지 변경될 수 있고, 현재를 설명하기 위한 값이다. 하지만 이벤트는 과거에 실제로 발생한 일이며, 그 자체로 의미를 가진다. 우리는 Timeline을 상태의 집합으로 만들려고 했고, 그 결과 파생 데이터 문제에 부딪혔다. 하지만 Timeline이 이벤트의 집합이라면, 이야기는 달라진다. 우리는 더 이상 “지금 무엇을 보여줄 것인가”를 고민하는 것이 아니라, “무슨 일이 일어났는가”를 기록하는 쪽으로 이동하게 된다.

이 변화는 단순한 모델 수정이 아니다. 도메인 자체를 다시 정의하는 과정이다. Timeline은 더 이상 UI의 하위 개념이 아니라, 시스템의 핵심 레이어로 올라온다. 그리고 이 순간, 설계는 다시 한 번 방향을 바꾸게 된다. 우리는 Timeline을 어떻게 구성할 것인가가 아니라, 무엇을 사실로 간주할 것인가를 결정해야 하는 단계로 들어간다.

결정 — Timeline은 ‘사실’만 기록한다

이 질문의 끝에서 하나의 결론이 나온다. TimelineEvent는 상태가 아니라 사실이어야 한다. 이 문장은 단순해 보이지만, 실제로는 많은 것을 포기해야 가능한 선택이다. 왜냐하면 사실만 기록한다는 것은, 대부분의 편의성을 버린다는 의미이기 때문이다. 계산된 값도, UI에 필요한 정보도, 심지어 일부 상태 정보조차도 저장하지 않겠다는 결정이기 때문이다. 하지만 이 결정을 내리지 않으면, Timeline은 계속해서 파생 데이터의 집합으로 남게 된다.

그래서 구조를 완전히 다시 정의한다. TimelineEvent는 가능한 한 단순하게 만든다. 어떤 일이 발생했는지, 언제 발생했는지, 그리고 그 이벤트가 어떤 엔티티와 연결되는지만 남긴다. 나머지는 모두 제거한다. 이 구조는 처음 보면 지나치게 빈약해 보인다. 하지만 이 단순함이야말로, 이벤트를 이벤트로 남기기 위한 최소 조건이다.

TimelineEvent
- id
- recordId
- eventType
- payload
- createdAtUtc

여기서 핵심은 payload다. payload는 이벤트의 의미를 설명하기 위한 최소한의 정보만 담는다. 예를 들어 세션이 종료되었다면, 그 시점의 진행률 정도만 저장한다. 세션 길이나 통계 값은 저장하지 않는다. 그것들은 나중에 계산할 수 있기 때문이다. 이 원칙은 명확하다. 계산 가능한 값은 저장하지 않는다. 상태는 저장하지 않는다. 원인만 남긴다.

이 구조를 받아들이는 순간, Timeline은 완전히 다른 성격을 가지게 된다. 더 이상 “편리한 데이터 저장소”가 아니라, 시스템의 진실을 기록하는 레이어가 된다. 그리고 이 레이어는 다른 어떤 것보다도 변경되지 않아야 하는 영역이 된다.

이벤트를 얇게 만든다는 것 — 저장하지 않는 설계

이벤트를 얇게 만든다는 것은 단순히 필드를 줄이는 문제가 아니다. 그것은 의도적으로 “저장하지 않기로 결정하는 것”이다. 대부분의 설계에서는 가능한 많은 정보를 저장하려고 한다. 나중에 필요할 수도 있고, 계산 비용을 줄일 수도 있으며, 예외 상황을 대비할 수도 있기 때문이다. 하지만 이 접근 방식은 시스템을 점점 더 무겁게 만든다. 그리고 무엇보다, 데이터의 의미를 흐리게 만든다.

얇은 이벤트 구조에서는 정반대의 선택을 한다. 필요한 것만 남기고, 나머지는 모두 버린다. 예를 들어 세션 길이는 매우 중요한 정보처럼 보이지만, 실제로는 시작 시점과 종료 시점만 있으면 언제든지 계산할 수 있다. 그렇다면 굳이 저장할 필요가 없다. 오히려 저장해버리면, 나중에 계산 방식이 바뀌었을 때 문제가 발생한다. 이처럼 “저장하지 않음”은 단순한 생략이 아니라, 미래의 변경 가능성을 열어두는 설계다.

이 방식은 처음에는 불안하다. 모든 정보를 가지고 있지 않다는 느낌이 들기 때문이다. 마치 중요한 것을 놓치고 있는 것처럼 보인다. 하지만 시간이 지나면서 이 구조의 장점이 드러난다. 데이터는 더 이상 서로 충돌하지 않고, 과거의 이벤트는 언제나 동일한 의미를 유지한다. 새로운 정책이 추가되더라도, 기존 데이터를 수정할 필요가 없다. 모든 변화는 “해석 방식”에서 일어난다.

결국 이벤트를 얇게 만든다는 것은, 시스템의 복잡도를 저장이 아니라 계산으로 이동시키는 선택이다. 그리고 이 선택은 구조를 가볍게 만든다. 무거운 것은 나중에 계산할 수 있지만, 잘못 저장된 데이터는 되돌릴 수 없다. 이 단순한 사실이, Timeline을 다시 정의하는 과정에서 가장 중요한 기준이 된다.

payload는 어디까지 허용되는가 — 자유와 통제 사이

이벤트를 얇게 만든다는 원칙을 받아들인 이후에도, 하나의 애매한 영역이 남는다. 바로 payload다. 구조 자체는 단순하다. id, eventType, createdAtUtc 같은 필드는 거의 고정되어 있고, 설계의 자유도는 대부분 payload 안에 들어간다. 이 때문에 payload는 동시에 가장 유연한 영역이면서, 가장 위험한 영역이 된다. 조금만 방심하면, 이 안에 모든 것이 들어가기 시작한다. 그리고 그 순간, 우리는 다시 처음의 “두꺼운 이벤트”로 돌아가게 된다.

처음에는 payload를 JSON으로 두는 것 자체가 매우 편리하게 느껴진다. 새로운 이벤트 타입이 추가되어도 스키마를 바꿀 필요가 없고, 필요한 정보를 유연하게 담을 수 있다. 개발 속도도 빠르고, 실험하기에도 좋다. 하지만 이 편의성은 명확한 기준이 없으면 곧 통제되지 않는 방향으로 흐른다. progressPct를 넣고, sessionDuration을 넣고, 나중에는 bookTitleSnapshot 같은 값도 넣고 싶어진다. 그렇게 하나씩 추가하다 보면, 어느 순간 payload는 하나의 “숨겨진 테이블”처럼 변한다.

이 지점에서 중요한 것은 “무엇을 넣을 수 있는가”가 아니라, “무엇을 넣지 않을 것인가”를 정의하는 것이다. payload는 모든 것을 담기 위한 공간이 아니라, 이벤트를 다시 구성하기 위한 최소 정보만을 담는 공간이어야 한다. 예를 들어 SESSION_ENDED 이벤트라면, 그 시점의 진행률 정도만 있으면 충분하다. 세션 길이는 계산할 수 있고, 책 정보는 recordId를 통해 조회할 수 있다. 즉, payload는 독립적인 데이터가 아니라, 다른 데이터와 결합될 때 의미를 가지는 최소 단위여야 한다.

이 기준을 명확히 하지 않으면, payload는 점점 상태를 침범하게 된다. 그리고 상태가 payload 안으로 들어오는 순간, 이벤트는 더 이상 이벤트가 아니다. 그것은 또 다른 형태의 Record가 된다. 그래서 payload는 자유롭게 설계할 수 있는 영역이 아니라, 오히려 가장 엄격하게 제한해야 하는 영역이다. 자유를 유지하기 위해서는, 오히려 더 강한 제약이 필요하다는 역설이 여기서 드러난다.

불변성 — Timeline이 신뢰를 갖는 조건

이벤트를 사실로 정의했다면, 그 다음에 따라오는 조건은 자연스럽게 결정된다. 그 사실은 변하지 않아야 한다는 점이다. 만약 TimelineEvent가 수정될 수 있다면, 그것은 더 이상 “기록”이 아니다. 그것은 언제든지 변경될 수 있는 데이터일 뿐이다. 그리고 이 순간, Timeline이 가지는 가장 중요한 속성인 “신뢰성”이 무너진다. 그래서 이 구조에서는 이벤트를 생성하는 것만 허용하고, 이후에는 수정하지 않는다는 원칙을 명확히 세운다.

이 원칙은 생각보다 많은 것을 바꾼다. 예를 들어, 사용자가 실수로 잘못된 기록을 남겼다면 어떻게 할 것인가? 직관적으로는 그 이벤트를 수정하거나 삭제하고 싶어진다. 하지만 그렇게 하면 Timeline의 의미가 깨진다. 대신 다른 방식이 필요해진다. 새로운 이벤트를 추가하거나, 논리적으로 무효화하는 방법을 사용해야 한다. 즉, 과거를 바꾸는 것이 아니라, 새로운 사실을 추가하는 방식으로 문제를 해결해야 한다.

이 구조는 처음에는 불편하게 느껴진다. 수정이 불가능하다는 것은 개발자 입장에서 제약처럼 보이기 때문이다. 하지만 이 제약이 바로 시스템을 단단하게 만든다. 모든 이벤트는 시간 순서대로 쌓이고, 그 자체로 의미를 가진다. 그리고 이 이벤트들의 집합이 곧 시스템의 진실이 된다. 어떤 상태든, 이 이벤트들을 기반으로 재구성할 수 있다. 이때 중요한 것은, 이 재구성이 항상 동일한 결과를 만들어낸다는 점이다.

결국 불변성은 선택이 아니라 필수 조건이 된다. Timeline이 기록으로 남기 위해서는, 그 기록이 변하지 않아야 한다. 그리고 이 불변성 덕분에, 우리는 데이터를 신뢰할 수 있게 된다. 시스템이 복잡해질수록, 이 신뢰성은 점점 더 중요한 역할을 하게 된다.

UI는 어떻게 만들어지는가 — 기록과 표현의 분리

이벤트를 얇게 만들고, payload를 제한하고, 불변성을 유지하는 구조를 받아들이면 자연스럽게 하나의 질문이 따라온다. 그렇다면 UI는 어떻게 구성되는가라는 질문이다. 이전 구조에서는 TimelineEvent 자체가 UI에 필요한 데이터를 모두 가지고 있었기 때문에, 그대로 렌더링하면 되었다. 하지만 이제는 상황이 완전히 달라졌다. TimelineEvent는 더 이상 UI를 위한 데이터가 아니라, 단순한 사실의 기록일 뿐이다.

이 변화는 처음에는 불편하게 느껴진다. UI를 구성하기 위해 추가적인 계산과 조회가 필요해지기 때문이다. 예를 들어 SESSION_ENDED 이벤트를 표시하려면, recordId를 통해 책 정보를 가져와야 하고, 세션 길이는 시작 이벤트와 종료 이벤트를 조합해서 계산해야 한다. 즉, 하나의 카드가 여러 데이터 소스를 결합한 결과로 만들어진다. 이 과정은 이전보다 복잡해 보인다.

하지만 이 구조는 중요한 분리를 만들어낸다. 기록과 표현이 완전히 분리된다. TimelineEvent는 과거의 사실을 그대로 유지하고, UI는 현재의 정책과 상태를 반영해서 그 사실을 해석한다. 이 덕분에 UI가 바뀌더라도 데이터 구조를 수정할 필요가 없다. 새로운 표시 방식이 필요하면, ViewModel에서 조합 방식을 바꾸면 된다. 과거의 이벤트는 그대로 두고, 해석만 바꾸면 된다.

이 구조는 시간이 지날수록 더 큰 장점을 가진다. 기능이 추가되고, UI가 바뀌고, 정책이 수정되더라도, Timeline 자체는 영향을 받지 않는다. 변화는 항상 바깥 레이어에서 일어나고, 기록 레이어는 그대로 유지된다. 이 안정성은 단순한 구현 편의성을 넘어서, 시스템 전체를 유지 가능한 상태로 만든다. 그리고 이 순간, 우리는 하나의 명확한 결론에 도달한다. Timeline은 UI를 위한 구조가 아니라, 그 위에서 UI가 만들어지는 기반이라는 사실이다.

두 번째 붕괴 이후 — 구조가 아니라 사고가 바뀌었다

Timeline을 이벤트 중심으로 재정의하고, payload를 제한하고, 불변성을 도입하는 일련의 과정은 단순히 하나의 모델을 바꾼 경험이 아니었다. 오히려 더 중요한 변화는, 설계를 바라보는 방식 자체가 달라졌다는 점이다. 이전까지는 가능한 많은 정보를 저장하고, 나중에 그것을 활용하는 방향으로 생각해왔다. 데이터는 많을수록 안전하고, 모든 것을 가지고 있으면 유연하게 대응할 수 있을 것처럼 보였다. 하지만 실제로는 그 반대였다. 더 많이 저장할수록, 더 많은 모순이 생기고, 더 많은 수정이 필요해졌다.

이벤트를 얇게 만든 이후에는, 이 사고가 완전히 뒤집힌다. 더 이상 “무엇을 더 저장할 수 있는가”를 고민하지 않는다. 대신 “무엇을 저장하지 않아야 하는가”를 고민하게 된다. 이 변화는 단순히 데이터 구조의 변경이 아니라, 설계의 기준이 바뀐 것이다. 이전에는 불안함 때문에 모든 것을 붙잡으려고 했다면, 이제는 신뢰를 위해 최소한만 남기려는 방향으로 이동한다. 이 차이는 작아 보이지만, 시스템 전체의 복잡도를 결정짓는 핵심적인 요소다.

이 과정에서 하나의 중요한 감각이 생긴다. 구조가 단단해졌다는 느낌이다. 이 단단함은 코드가 깔끔해졌기 때문이 아니다. 오히려 그 반대에 가깝다. 일부 영역은 더 복잡해졌고, 계산해야 할 것도 늘어났다. 하지만 데이터 자체는 더 이상 흔들리지 않는다. 과거의 기록은 그대로 남아 있고, 어떤 정책이 바뀌더라도 그 기록은 변하지 않는다. 이 안정성이 바로 설계의 중심을 잡아준다.

결국 이 변화는 “어떻게 저장할 것인가”의 문제가 아니라, “무엇을 사실로 간주할 것인가”의 문제로 이어진다. Timeline을 재정의하는 과정은, 데이터 모델을 바꾸는 작업이 아니라, 시스템이 현실을 어떻게 이해하는지를 바꾸는 과정이었다.

Timeline은 UI가 아니다 — 기록 레이어라는 선언

이 시점에서 하나의 선언이 필요해진다. Timeline은 더 이상 UI를 위한 구조가 아니라는 선언이다. 이 문장은 단순한 표현처럼 보이지만, 실제로는 시스템의 레이어를 완전히 분리하는 결정이다. 이전에는 Timeline이 화면에 무엇을 보여줄 것인가를 기준으로 설계되었다면, 이제는 그 반대다. Timeline은 오직 기록만을 담당하고, 화면은 그 기록을 해석하는 별도의 레이어에서 만들어진다.

이 선언이 중요한 이유는, 책임이 명확하게 나뉘기 때문이다. Timeline은 과거를 담당하고, UI는 현재를 담당한다. 이 둘은 서로 영향을 주지 않는다. UI 정책이 바뀌더라도 Timeline은 변하지 않고, Timeline 구조가 단단하게 유지되기 때문에 UI는 언제든지 새로운 방식으로 재구성할 수 있다. 이 분리는 단순한 구조적 이점이 아니라, 시스템 전체의 유연성을 만들어낸다.

또 하나 중요한 변화는, Timeline이 더 이상 “보여주기 위한 데이터”가 아니라 “의미를 보존하기 위한 데이터”가 된다는 점이다. 이벤트 하나하나는 작고 단순하지만, 그 집합은 강력한 의미를 가진다. 그리고 이 의미는 시간이 지나도 변하지 않는다. 새로운 기능이 추가되더라도, 기존 이벤트는 그대로 유지되고, 그 위에 새로운 해석이 쌓인다. 이 구조는 시스템을 점점 더 확장 가능하게 만든다.

이 선언은 이후의 모든 설계에 영향을 미친다. 특히 데이터 저장 위치나 동기화 방식 같은 문제에서, 이 구조는 중요한 기준이 된다. 기록은 변하지 않아야 하고, 해석은 언제든지 바뀔 수 있어야 한다. 이 단순한 원칙이, 이후 더 큰 구조를 결정짓는 기반이 된다.

다음 문제로 이어지는 지점 — 기록은 어디에 있어야 하는가

Timeline을 기록 레이어로 정의하는 순간, 또 하나의 질문이 자연스럽게 따라온다. 이 기록은 어디에 있어야 하는가라는 질문이다. 이전까지는 이 질문이 크게 중요하지 않았다. 서버에 저장하든, 로컬에 저장하든, 어차피 데이터는 동일하게 다뤄진다고 생각했기 때문이다. 하지만 이제는 상황이 달라진다. Timeline은 단순한 데이터가 아니라, 시스템의 “진실”이기 때문이다.

이 진실을 어디에 둘 것인가는 단순한 저장 위치의 문제가 아니다. 그것은 책임의 문제다. 이 데이터를 누가 책임지고, 어디에서 생성되고, 어디에서 보존되는가를 결정하는 문제다. 이벤트는 사용자의 행동에서 발생한다. 그렇다면 이 데이터는 가장 먼저 어디에 있어야 하는가? 이 질문은 자연스럽게 로컬로 향하게 된다. 사용자의 행동은 디바이스에서 일어나고, 그 기록 역시 그 디바이스에서 가장 먼저 생성된다.

하지만 여기서 또 하나의 현실적인 문제가 등장한다. 이 기록을 그대로 로컬에만 둘 수 있는가라는 문제다. 백업, 동기화, 그리고 여러 디바이스 간의 일관성 같은 문제들이 동시에 떠오른다. 즉, Timeline을 로컬에 두는 것은 자연스러운 선택이지만, 동시에 새로운 복잡성을 만들어낸다. 이 지점에서 우리는 또 다른 설계의 갈림길에 서게 된다.

이제 질문은 더 명확해진다. 기록은 로컬에 있어야 하는가, 아니면 서버에 있어야 하는가. 혹은 그 둘 사이의 어딘가에 있어야 하는가. 이 질문은 단순한 기술 선택이 아니라, 시스템의 방향을 결정짓는 문제다. 그리고 이 질문에 대한 답을 찾는 과정에서, Local-first라는 개념이 본격적으로 등장하게 된다. 다음 글에서는 이 선택이 어떻게 이루어졌는지, 그리고 왜 동기화를 나중으로 미룰 수밖에 없었는지를 다루게 된다.