Skip to main content
← 블로그

정렬의 언어

VauDium ·

Task Record List에 우선순위·난이도·만족도 정렬을 추가했습니다. 정렬만 더한 게 아니라 각 축에 맞는 시각 언어를 입혔고, 그러면서 그룹 접기 기능도 자연스럽게 따라왔어요.

정렬의 언어

생성일 하나로는 부족해진다

Fecit의 Tasks 탭은 오랫동안 생성일 역순 하나로만 정렬됐습니다. 최근 만든 게 위, 오래된 건 아래. 간단하고 이해하기 쉬운 기본값이에요.

그런데 태스크가 쌓이면 이 정렬만으로는 충분하지 않아지는 순간이 옵니다. 가장 흔한 질문 두 가지.

  • “지금 가장 중요한 걸 먼저 보고 싶다”
  • “짧게 끝낼 수 있는 걸 먼저 처리하고 싶다”

앞의 것은 우선순위(Priority) 기준, 뒤의 것은 난이도(Difficulty) 기준으로 보자는 말입니다. Fecit의 태스크 모델엔 두 필드가 이미 있었어요. 단지 리스트 정렬과 연결돼 있지 않았을 뿐이었습니다.

하나 더 생각해볼 축이 있었습니다. 만족도(Satisfaction). 이건 회고 단계의 필드라 “계획용”이 아니라 “돌아보기용”인데, 오히려 그래서 유용합니다. “지난 달 내가 가장 만족했던 태스크들은 뭐였지?”를 꺼내볼 수 있어요.

그래서 세 축을 추가했습니다 — 우선순위 · 난이도 · 만족도. 기존의 생성일까지 포함하면 네 가지 정렬축이에요. 일단은 각 축을 내림차순(DESC) 하나로만 두기로 했습니다. 오름차순이 필요해지는 케이스가 나타나면 그때 추가하기로.

축마다 다른 시각 언어

정렬 축을 더하고 나서 바로 부딪힌 문제가 있었습니다. 생성일 정렬일 땐 리스트 중간중간 날짜 구분선이 들어가서 “여기서부터 어제”, “여기서부터 그제” 하고 흐름을 보여줬어요. 그런데 우선순위로 정렬하면 이 구분선이 의미를 잃습니다. 같은 날 만들어진 태스크가 우선순위별로 흩어져 있으니까요.

그래서 정렬축마다 고유한 Separator(구분선) 를 새로 만들었습니다.

  • Priority Separator — 각 우선순위 레벨에 맞는 아이콘(priority-high.svg, priority-lowest.svg 등)과 “높음”, “낮음” 같은 라벨
  • Difficulty Separator — 별 개수로 난이도 시각화. Trivial은 별 1개, Extreme은 별 5개
  • Satisfaction Separator — 기존의 SatisfactionIcon을 그대로 재사용. 얼굴 표정과 배경색이 1~5단계별로 다르게 들어가요

같은 “그룹 헤더”지만 축마다 다른 시각 언어를 썼습니다. 숫자 5를 “별 다섯 개”로도, “HIGHEST 라벨”로도, “웃는 얼굴”로도 표현할 수 있어요. 같은 디자인 컴포넌트로 통일하는 것보다 각 축이 가진 의미에 맞는 시각이 훨씬 설명력이 좋습니다.

그룹이 생기니 접고 싶어진다

Separator로 리스트가 섹션화되니 자연스럽게 다음 욕구가 생겼습니다. “이 그룹은 일단 접어두고 아래로 가고 싶다.”

날짜 정렬일 땐 오래된 그룹이 아래로 흘러가면 그만이에요. 다시 돌아볼 일이 많지 않습니다. 그런데 우선순위·난이도·만족도는 다릅니다. 내림차순으로 배치하면 위쪽이 가장 높은 값, 아래로 갈수록 낮은 값인데 아래쪽 그룹에도 자기 역할이 있어요. 낮은 우선순위 태스크는 여유가 생겼을 때 꺼낼 것이고, 쉬운 태스크는 짧은 시간이 났을 때 먼저 훑고 싶은 것이고, 만족도가 낮았던 태스크는 다음엔 뭘 다르게 할지 복기할 거리고요.

그 아래쪽 그룹에 빨리 도달하려면, 위쪽에서 이미 훑은 그룹들을 접어서 스크롤 거리를 줄이는 게 유용합니다.

그래서 모든 Separator에 접기/펼치기를 붙였습니다. 탭하면 그 그룹의 태스크들이 숨어요. 오른쪽에 chevron 아이콘이 있어서 상태를 보여줍니다 — 펼친 상태는 아래 방향(∨), 접으면 오른쪽(>)으로 회전.

구현 자체는 간단합니다. Set<string>에 접힌 그룹의 키를 넣어두고, 리스트 아이템을 생성할 때 해당 그룹에 속한 태스크는 건너뛰는 식이에요. 그룹 키는 축마다 다르게 계산됩니다.

date-2026-04-22T...
priority-4
difficulty-2
satisfaction-5

애니메이션을 고르는 여정

이 기능의 진짜 고민은 접기/펼치기의 체감이었어요. 리스트의 여러 아이템이 한꺼번에 사라지거나 나타나면, 기본 layout animation이 각 셀마다 개별로 계산되면서 “어떤 셀은 빨리, 어떤 셀은 약간 늦게” 움직이는 불일치가 눈에 띕니다.

여러 접근을 시도했습니다.

  • Fade in/out을 붙여볼까 — 태스크 셀이 등장/소멸할 때 개별 애니메이션이 보여서, 접기보다는 “아이템이 생성됐다/사라졌다”는 인상이 강해졌어요. 폐기.
  • 접는 순간만 layout animation을 끌까 — 즉시 자리가 잡혀서 확실히 동기화되지만 너무 딱딱한 느낌.
  • duration을 줄여볼까 — 짧은 애니메이션 자체는 부드럽지만, 그 시간을 태스크 생성/수정 같은 다른 상황과 공유하기 때문에 부작용이 컸습니다.

결국 “접기 순간에만 layout animation을 일시적으로 비활성화”하는 방식으로 자리 잡았습니다. collapsingRef 플래그를 세우고 두 프레임 뒤에 복구합니다. 그 사이에 일어나는 layout 변경은 애니메이션 없이 즉시 반영돼요. 생성·수정·포커스 진입 같은 다른 상황의 부드러운 200ms 전환은 그대로 유지됩니다.

완벽히 만족스러운 결과는 아니에요. 이상적인 체감은 “리스트 전체가 한 덩어리로 스크롤되며 위로 올라오는 느낌”인데, FlatList + 셀별 Reanimated layout animation 조합에서는 구조적으로 달성하기 어려운 영역입니다. 그 효과를 얻으려면 FlatList를 ScrollView로 바꿔야 하는데, virtualize를 포기하고 페이지네이션·sticky header·focused slot 같은 로직을 재설계해야 해요. 비용이 너무 커서 지금은 거기까지 가지 않았습니다.

남긴 것

  • 네 개의 정렬축 — 생성일, 우선순위, 난이도, 만족도
  • 축마다 다른 시각 언어를 가진 그룹 헤더
  • 접기/펼치기로 리스트 길이를 스스로 조절할 수 있는 구조

정렬이라는 기능은 표면적으로는 단순한 리스트 순서 바꾸기에 불과해요. 그런데 잘 만들면, 태스크 관리에 대한 각자의 시각을 열어줍니다. 어떤 사람은 우선순위로, 어떤 사람은 난이도로, 어떤 사람은 만족도로 자신의 하루를 돌아봅니다. 정렬은 그 시각을 담는 그릇이에요.

서버도, 웹도

이 정렬 기능은 모바일만 바뀐 게 아닙니다. 서버 API의 TaskSortType enum에 세 값을 더했고, gather 엔드포인트에 해당 정렬 분기를 추가했어요. 데스크톱 웹의 필터 모달에도 같은 옵션이 들어갔습니다. 세 곳 모두 같은 enum 값을 쓰니, 한 곳에서 정렬하면 다른 곳에서도 같은 결과가 나오고요.

로컬 SQLite 캐시에는 priority, difficulty, satisfaction 컬럼과 인덱스를 추가하는 마이그레이션이 붙었습니다. 스키마 버전을 올릴 때마다 해당 컬럼을 ALTER TABLE ADD COLUMN으로 추가하고, 기존 row의 JSON에서 값을 복구합니다. 처음 마이그레이션을 짤 때 조건을 잘못 걸어서 중간 버전이 어긋나는 깨진 상태가 잠깐 있었는데, 그 과정에서 “마이그레이션 블록은 한 번 커밋된 시점의 스키마를 만드는 역할로 고정하고 절대 나중에 수정하지 않는다”는 원칙을 다시 새겼습니다.

정렬 기능 하나를 얹는데 생각보다 많은 레이어가 건드려졌어요. 그만큼 Fecit의 태스크 모델이 여러 곳에 퍼져 있다는 뜻이기도 하고, 한 기능이 사용자에게 도달하기까지 거치는 길이 긴 구조라는 뜻이기도 합니다.