구조에 기억을 더한다는 것
사람들이 더 쉽게 성취하게 하려면 뭘 해야 할까? 며칠을 돌고 돌아 결국 'Fecit이 갖고 있는 구조에 시간을 붙이는 일'이라는 답에 도달했습니다.
구조에 기억을 더한다는 것
오늘은 코드보다 _질문_으로 시작한 하루였습니다.
“사람들이 조금 더 쉽게 성취하게 하려면 뭘 해야 할까?”
이 질문 하나로 두 시간이 흘렀고, 결국 도달한 답은 “Fecit이 이미 갖고 있는 구조에 시간을 붙여보자” 였습니다.
첫 번째 답은 틀렸다
처음엔 익숙한 답을 던졌습니다.
오늘의 한 걸음 — 매일 backlog에서 1개를 picking해서 첫 화면 anchor로 보여주고, 한 탭으로 시작.
그럴듯해 보였지만 두 번째에 사용자가 정확하게 짚었습니다.
그게 확장성이 있는지 잘 모르겠네.
맞는 말이었습니다. _“오늘의 한 걸음”_은 day 1과 day 1000이 똑같은 feature예요. 사용자가 사용을 거듭해도 더 좋아지지 않는 정적 surface. 그건 시간을 무시한 디자인이고, 시간이 만드는 가치를 못 누립니다.
Loop closing — 데이터가 돌아오는 길
진짜 확장성 있는 방향은 loop closing — Fecit이 이미 모으고 있는 데이터(완료 / 만족도 / 회고)가 다시 사용자에게 돌아오는 것이었습니다.
day 1엔 아무 데이터 없으니 hint도 없지만, day 100엔 “이 종류 task는 아침에 만족도 4↑, 30분 이하일 때 완료율 높아요” 같은 개인화된 nudge가 자연스럽게 생깁니다. 사용할수록 더 정확해지고 — 다른 앱이 베끼기 어려운 (사용자별 누적 데이터가 필요한) moat도 됩니다.
두 번째 답도 틀렸다
이 다음에 또 잘못된 길로 빠졌습니다.
새 sort option (만족도 순, 완료율 순) 추가해서 보여주자
UI를 덧붙이는 방향이었습니다. 사용자가 즉시 짚었습니다.
UI가 어떻게 해야 할지 모르겠네? 이미 충분히 복잡한데.
맞아요. 통계를 숫자로 보여주는 순간 그건 또 하나의 정보 표면이 되고, “이미 충분히 복잡한” 화면이 더 복잡해집니다.
진짜 답은 “loop을 보이지 않게” 돌리는 거였습니다. stat은 숫자로 보여주지 않고, 이미 있는 UX 한 군데의 입력값으로만 쓰는 것. Spotify Discover Weekly가 점수를 안 보여주는 것처럼.
그래서 어디에 두지?
여기서 마지막 회전이 있었습니다.
Fecit의 진짜 차별점은 _“미리 디자인된 구조화된 문서”_입니다. Notion이 자유롭지만 매번 설계해야 하고 Todoist가 _가볍지만 깊이가 없다_면, Fecit은 그 사이를 메우죠 — target, expectation, obstacle, result, retrospect 같은 슬롯이 미리 만들어져 있고 사용자는 채우기만 하면 됩니다.
그 슬롯에 시간을 붙이면 어떻게 될까?
[목표 textarea]
↓ 라벨 옆에 시계 아이콘
[시계 탭] → bottom sheet
─── 지난 목표 ─────
😊 3월 15일 "5km 달리기" [채우기]
😐 3월 10일 "30분 걷기" [채우기]
🙂 3월 5일 "헬스 가기" [채우기]
각 항목엔 만족도 face icon (만들어둔 stats를 활용 — 숫자가 아닌 표정으로). 날짜. 내용. “채우기” 버튼.
slot이 _빈 칸_이 아니라 시간 위의 실타래가 됩니다.
인프라는 이미 있었다
운 좋게도 어제 만든 denormalized stats가 정확히 여기에 맞아떨어졌습니다.
# TaskTemplate (이전에 만들어둠)
completed_count: int
satisfaction_sum: int
satisfaction_count: int
satisfaction_average: float # sort/index용
duration_sum: int
duration_count: int
duration_average: float
추가로 필요했던 건 slot 단위 history 조회 endpoint 하나:
@router.get("/{task_template_id}/slot-history/get")
async def get_task_template_slot_history(
task_template_id: str,
slot: Literal["description", "target", "expectation",
"obstacle", "result", "retrospect"],
skip: int = 0,
limit: int = 10,
):
# origin.id == template_id, origin.method == GENERATE인 record들 중
# 해당 slot에 내용이 있는 것만 최신순으로 페이지네이션
origin.id는 record가 어느 template으로부터 만들어졌는지 가리키는 link — 이미 모든 record에 박혀 있어서 스키마 추가 없이 바로 활용했습니다.
이미 모으고 있던 데이터, 이미 있던 link, 어제 만든 통계 — 셋이 합쳐지자 새 feature가 거의 _드러나는 것_처럼 만들어졌습니다. 인프라가 의미를 갖는 시점은 항상 그 다음에 오는 표면 디자인에서.
”보이지 않게” 한다는 게 그래도 어딘가는 보여야 한다
loop을 _보이지 않게_라고 했지만, 사용자가 그 기능에 닿을 길은 있어야 합니다. 그래서 시계 아이콘 — 그것도 얇게:
- 라벨 영역 우측에, 핀 바로 왼쪽에
- 16×16, NEUTRAL300 (눈에 띄지 않는 무채색)
- 데이터 있는 record (origin이 template GENERATE) 에서만 노출
빈 화면을 보면 그냥 작은 시계 아이콘. 누르면 그 자리에 _과거의 자기 자신_이 떠오름.
작은 디테일 두 가지
1. 핀과 시계의 영역 충돌
핀 컴포넌트엔 paddingLeft: 8 + 좌측 hitSlop 8이 박혀 있어서, 시계를 옆에 두니 시계 아이콘 영역이 핀의 탭 영역에 잠식됐습니다. 시계를 누르면 핀이 토글됐죠.
// PinToggleButton: 좌측 16px의 탭 영역이 시계를 침범
hitSlop={{left: 8}}, paddingLeft: 8
// SlotHistoryButton: 우측 8px의 탭 영역
hitSlop={{right: 8}}
핀의 paddingLeft를 제거하고 hitSlop을 4로 줄였습니다. 두 버튼 사이에 8px 간격. 두 hitSlop이 중간에서 정확히 만나서 visible icon은 침범하지 않게.
2. 빈 상태 메시지의 무게
처음엔 그냥 _“No results”_라고 적었는데, 사용자가 정확하게 짚었습니다.
“No results found 말고 여기서 과거 데이터를 볼 수 있습니다 같은 형태로”
빈 상태는 _없음_을 알리는 자리가 아니라, _자리의 의미_를 가르치는 자리입니다. 처음 시계를 눌러본 사용자는 자기 데이터가 없는 게 아니라 — 이 자리에 자기 데이터가 쌓일 거라는 것을 배워야 합니다.
"여기서 지난 기록을 볼 수 있어요"
같은 한 줄이지만 _가르치는 줄_과 _없음을 알리는 줄_은 톤이 완전히 다릅니다.
정리
오늘 한 일은 코드량으로 보면 작아요 — endpoint 하나, sheet 하나, 라벨 6개에 prop 하나씩.
하지만 사고 전환은 컸습니다:
- Anchor를 새로 만든다 → 잘못. 시간을 무시한 정적 feature.
- Sort option을 추가한다 → 잘못. UI를 덧붙이는 방향.
- 구조에 기억을 더한다 → 정답. 이미 있는 슬롯에 시간을 붙임.
세 번 굽이친 끝에 도달한 답은 _가장 작은 변경_이었고 _가장 큰 의미_였습니다. 새 화면도, 새 사용자 행동도, 새 인지 부담도 없이 — 그저 라벨 옆에 시계 아이콘 하나.
minimal-to-maximal의 진짜 모습 — 기본은 안 변하고, 데이터가 쌓일수록 깊이가 자라는 표면.
다음 단계는 자연스럽게 보여요. 이 같은 패턴이 comment, freeboard 답글, calendar event 같은 다른 시간 위에 누적되는 곳에도 똑같이 적용될 수 있습니다. 슬롯이 기억을 가질 때 비로소 시작되는 새 카테고리의 UX가 있고, Fecit이 그 첫 발을 디뎠습니다.