
타임라인 뷰 구현기
라이브러리 도입 고민
타임라인 뷰를 개발하기에 앞서, 먼저 라이브러리 도입 여부를 고민했어요. 한정된 시간 동안 뷰 구현을 완료해야 하는 상황이었기 때문에, 안정적인 기능 구현이 가능하다면 라이브러리를 도입하는 것도 좋은 방법이라고 생각했기 때문이에요.
라이브러리를 검토할 때는 두 가지 기준을 세워 고민했어요.
- 사용자 수
- 최신 업데이트 여부
이 기준에 따라 여러 오픈소스 타임라인 라이브러리를 조사해 봤지만, 기대에 부합하는 라이브러리는 찾기 어려웠어요. 사용자 수가 적거나, 어느 정도 사용자 수가 있어도 업데이트가 몇 년째 멈춰 있는 경우가 대부분이었거든요.
또한, 제공하는 뷰와 저희가 구현하고자 하는 뷰 사이에 차이가 컸어요. 그래서 라이브러리를 억지로 커스터마이징하는 데 드는 리소스가 오히려 직접 만드는 것보다 더 많이 들 수 있다고 판단했어요.
결국, 라이브러리 도입은 포기하고 직접 타임라인 뷰를 개발하기로 결정했어요.
타임라인 뷰 기본 구조
초기에는 다음과 같은 기능을 완성했어요.
- 생성된 블록을 기간에 맞게 배치해요.
- 달 헤더에서 선택한 월과 연도에 해당하는 블록만 렌더링해요.
- 선택한 블록의 정보를 사이드바(Drawer)에 표시해요.

타임라인에 표시할 블록들의 위치와 너비는 서버에서 전달받은 startDate와 endDate를 기준으로 계산했어요.
const blockWidth = (new Date(endDate).getDate() - new Date(startDate).getDate() + 1) * 6;
const startPosition = (new Date(startDate).getDate() - 1) * 6;
- 블록의 너비는 기간의 길이에 따라 결정돼요.
- 블록의 시작 위치는 시작일을 기준으로 계산해요.
6rem 단위로 칸이 나누어진 배경 위에, 블록을 정확히 배치할 수 있도록 했어요.
블록 쌓기 로직
타임라인을 구현하면서 특히 신경 쓴 부분은 **"기간이 겹치는 블록을 어떻게 자연스럽게 쌓을까"**였어요.
여기서 고려했던 핵심은 다음 두 가지예요.
- 블록 기간이 겹치는 경우, 나중에 생성된 블록이 기존 블록 아래에 겹치지 않게 쌓여야 한다.
- 달 헤더를 클릭할 때마다 서버로부터 해당 월과 연도의 데이터만 요청하여 렌더링 최적화
블록을 쌓는 로직은 별도의 유틸 함수로 분리하고, map을 통해 블록을 렌더링했어요.
기본 구조는 다음과 같아요.
const timeTable: boolean[][] = Array.from({ length: endDay.getDate() + 1 }, () => Array(data.length).fill(false));
const floors: Floors = {};
const clickedMonth = parseInt(selectedMonth.split('월')[0]);
timeTable: 일(day) × 블록 개수 형태의 2차원 배열로, 특정 날짜에 블록이 있는지를 관리해요.floors: 각 블록이 위치할 층(깊이) 정보를 저장해요.clickedMonth: 선택된 월을 숫자로 변환해 저장해요.
블록을 배치하는 과정은 다음과 같아요.
data.forEach((block) => {
if (blockMonth === clickedMonth && blockStartDate.getUTCFullYear() === currentYear) {
const days = getDays(blockStartDate, blockEndDate);
let floor = 0;
for (let depth = 1; depth <= data.length; depth++) {
if (days.every((day) => !timeTable[day][depth])) {
floor = depth;
break;
}
}
if (floor !== 0) {
days.forEach((day) => {
timeTable[day][floor] = true;
});
floors[block.timeBlockId] = floor;
}
}
});
간단하게 말하자면,
- 선택된 월에 해당하는 블록만 대상으로 하고,
- 겹치지 않는 가장 낮은 층을 찾아 블록을 배치하고,
timeTable에 해당 층이 사용되었음을 표시하는 방식이에요.
Trouble Shooting : 서버 개발자와의 소통
타임라인 개발 중 가장 크게 겪었던 문제는 **"서버와 클라이언트 데이터 처리 방식 불일치"**였어요.
처음에는 더미 데이터를 사용해, 클라이언트 쪽에서 월과 연도에 맞게 데이터를 필터링해 직접 렌더링하는 방식을 사용했어요. 하지만 실제 API 연동 이후에도 이 방식을 그대로 유지하면서, 미래 기간 블록이 제대로 렌더링되지 않는 문제가 발생했어요.
문제를 분석해 보니, 서버에서는 선택한 달(예: 2024-07)을 클릭할 때 모든 데이터를 한 번에 내려주고 있었어요. 반면, 클라이언트는 매번 선택한 달과 연도에 맞춰 데이터를 요청하고 필터링하는 구조였죠.
이로 인해, 월별 데이터를 정확히 뿌리려던 의도와 실제 API 응답이 일치하지 않아 문제가 생겼어요.
결국 서버 데이터 설계와 클라이언트 처리 방식을 다시 점검하고, 데이터를 어떤 방식으로 불러와야 할지에 대해 고민해야 했어요. 이 과정을 통해, 평소에도 중요하다고 생각해왔던 디버깅과 문제 분석의 중요성을 다시 한 번 절실히 느낄 수 있었어요. 또한, 서버 개발자 분들과 즉각적으로 상황을 공유하고 소통하는 습관이 얼마나 중요한지도 몸소 깨닫게 되었어요.
또한 이런 문제가 재발되지 않기 위해선 MSW와 같은 라이브러리를 활용하여 미리 모킹하는 것도 꽤 중요한 과정임을 다시 깨닫게 되었어요. (당시는 시간이 부족해서 미리 모킹할 시간을 없었겠지만 ...)
서버와의 소통이 어긋나면서 예상치 못한 문제가 발생했을 때, 적극적으로 상황을 공유하고 조율하는 것의 중요성을 크게 체감했어요. 또한 개발한 것에 대한 확신을 갖지 못한 탓에 이유를 계속 나에게만 찾고 있던 저를 발견했어요.. 또 이런 일이 있지 않기 위해선 코드를 작성하면서 계속 저에게 '왜 이렇게 작성했어?'라고 묻는 게 중요할 것 같다고 생각했어요.
또한 이번 작업을 통해, GUI를 기준으로 라이브러리 도입 여부를 명확히 판단할 수 있었어요. 이번 경험으로 단순히 "남들이 쓰니까"를 기준으로 판단하는 것이 아니라, 사용자 수, 업데이트 여부, 커스터마이징 리소스까지 종합적으로 고려해, 결국 '직접 개발하는 것이 더 낫다'는 결론을 내릴 수 있었어요.
타임라인 UI 수정
1차 스프린트 이후, 전반적인 디자인 수정이 이루어지면서 타임라인 UI도 크게 변경되었어요.
1차 스프린트 버전의 타임라인은 DaySection을 가로 스크롤로만 확인할 수 있었고, 전체 일정을 한눈에 볼 수 없었어요. 월 변경은 버튼으로만 가능했으며, 홀수 요일에만 배경색이 들어가 있었고, Drawer(오른쪽 패널)가 열릴 때 타임라인 일부가 잘리는 문제가 있었어요

2차 스프린트에서는 여러 부분이 개선되었어요.
- 화살표 버튼을 이용해 월 단위로 이동할 수 있게 되었어요.
- 한 화면에 요일 전체를 한눈에 볼 수 있도록 구성했어요.
- Drawer가 열릴 때 타임라인 전체가 자연스럽게 밀리면서, TimeBlock(타임블록) 너비도 함께 조정되었어요.
- '오늘' 버튼을 추가해 현재 연도와 월로 빠르게 이동할 수 있게 되었어요.

타임 블록 너비 조정 문제
대부분의 디자인 변경사항은 무리 없이 적용했지만, Drawer가 열릴 때 타임블록 너비가 일정하게 줄어들지 않는 문제가 발생했어요.
1차 스프린트 때 작성한 타임블록 배치 방식은 position: absolute를 사용했기 때문에, DaySection과 TimeBlock이 서로 연동되지 않은 상태였어요.
타임블록은 날짜 범위를 기준으로 width를 계산했지만, Drawer가 열릴 때마다 레이아웃과 너비가 따로 움직이며 불안정하게 줄어드는 현상이 나타났어요.
당시 타임블록의 스타일은 다음과 같이 작성되어 있었어요.
export const blockStyle = (width: number, startPosition: number, floor: number, color: string, isSelected: boolean) =>
css({
display: 'flex',
position: 'absolute',
top: `${floor * 4.5 - 1}rem`,
left: `${startPosition + 1.5}rem`,
width: `calc(${width})`,
height: '3.6rem',
zIndex: theme.zIndex.overlayBottom,
...생략
});
처음에는 디자이너분께 해상도별로 타임블록 너비가 몇 퍼센트 줄어드는지 값을 요청하려 했지만, DaySection이 min-width만 설정된 유동적인 구조라서, 정확한 퍼센트 값을 제공하기가 어려웠어요.
게다가 absolute 방식 배치 때문에, 브라우저 창 크기를 줄였을 때 타임블록이 잘리는 문제까지 추가로 발견됐어요.

Trouble Shooting : 레이아웃 구조 변경
'뭔가 잘못됐다'는 느낌을 계속 갖고 고민하다가, 결국 absolute 방식 자체가 문제라는 결론에 도달했어요.
DaySection과 TimeBlock을 서로 연관짓는 구조를 만들어야 디자이너가 원하는 UI를 정확히 구현할 수 있을 거라고 판단했어요.
그래서 타임블록의 배치 방식을 grid 레이아웃으로 전면 수정했어요.
DaySection 전체를 display: grid로 구성하고,
TimeBlock은 gridColumn과 gridRow 속성을 사용해 배치했어요.
각 블록이 차지할 날짜 범위와 층 위치를 다음처럼 지정했어요.
gridColumn: `${new Date(startDate).getDate()} / span ${daysLength}`,
gridRow: `${floor}`,
이 방법을 적용한 결과, Drawer가 열릴 때 DaySection과 TimeBlock이 함께 자연스럽게 밀렸고, 브라우저 창을 줄여도 타임블록이 잘리는 문제 없이 자연스럽게 반응형으로 축소되었어요.
Grid 레이아웃을 활용한 덕분에, 디자이너의 요구사항을 안정적으로 만족시킬 수 있었어요.
마무리
2차 스프린트를 통해 타임라인 뷰를 디벨롭 하면서, 구조적 설계의 중요성을 깊이 체감했어요. 레이아웃을 잡을 때 단순히 배치만 고려하는 것이 아니라, 나중에 발전될 것도 고려하면서 개발해야 한다는 걸 알게 되었어요.
또한, 문제를 발견했을 때 단순히 "디자인에 맞추는 방법"만 찾는 것이 아니라, 기본 구조부터 돌아보고 다시 설계하는 과정이 얼마나 중요한지도 배울 수 있었어요. 앞으로 복잡한 UI를 개발할 때, 레이아웃을 구성하는 초반 단계에서부터 좀 더 체계적이고 유연하게 접근하는 개발자가 되어야겠다고 생각하게 됐던 좋은 경험이었어요.