
타임라인 UI를 만들 때 가장 어렵다고 생각하는 게 뭔지 물어보면, 대부분 "애니메이션"이나 "디자인"을 꼽습니다. 그런데 제가 직접 써봤는데, 진짜 발목을 잡는 건 따로 있었습니다. 수직선 하나 제대로 그리는 것, 그리고 그 선과 카드를 맞물리게 하는 position 속성의 이해였습니다. 블로그에 "개발자가 된 과정"을 연대기로 정리하면서 처음 타임라인을 도입했는데, 줄글로 쓸 때는 독자가 연도별 흐름을 거의 파악 못 한다는 걸 그때 처음 실감했습니다.
수직선 하나에 이렇게 많은 게 걸려 있을 줄 몰랐습니다
CSS 타임라인 구현에서 핵심이 되는 건 position 속성입니다. 여기서 position이란 HTML 요소가 문서 흐름 안에서 어디에 자리를 잡을지, 그리고 그 기준점을 무엇으로 삼을지를 제어하는 속성입니다. position: relative로 부모 컨테이너를 기준점으로 지정하고, 그 안에 position: absolute를 쓴 자식 요소를 배치하는 방식이 타임라인 구조의 뼈대입니다(출처: MDN Web Docs).
수직 중앙선은 CSS 가상 요소(pseudo-element)로 그립니다. 가상 요소란 HTML에 별도의 태그를 추가하지 않고도 CSS만으로 시각적인 요소를 생성할 수 있는 기능으로, ::before와 ::after가 대표적입니다. .timeline::before에 position: absolute, left: 50%, transform: translateX(-50%)를 조합하면 부모 너비가 바뀌어도 선이 항상 정중앙을 유지합니다. 처음엔 left: 50%만 쓰다가 선이 미묘하게 왼쪽으로 쏠리는 걸 발견했는데, translateX(-50%)를 더하는 순간 해결됐습니다. 별거 아닌 것 같아도 이걸 모르면 픽셀 단위로 계속 보정하게 됩니다.
각 이벤트 카드는 좌우 교대로 배치하는 패턴이 공간 효율 면에서 유리하다는 의견도 있고, 오히려 시선이 좌우로 분산돼 읽기 불편하다는 시각도 있습니다. 저는 이벤트 수가 6~15개 사이일 때는 좌우 교대 배치가 스크롤 길이를 줄여줘서 실제로 효과적이었습니다. 단, 카드 너비가 50%이기 때문에 .timeline-item.right에는 반드시 margin-left: 50%를 적용해야 오른쪽 영역에 정확히 들어갑니다.
- position: relative → 부모 요소를 자식의 좌표 기준으로 설정
- position: absolute → 기준 부모로부터의 오프셋(offset)으로 위치를 지정. 오프셋이란 기준점에서 얼마나 떨어졌는지를 나타내는 수치입니다
- transform: translateX(-50%) → 요소 자신의 너비 절반만큼 왼쪽으로 이동, 중앙 정렬 보정
- ::before 가상 요소 → 별도 HTML 없이 수직선을 CSS만으로 렌더링
구조 설계를 잘못 잡으면 나중에 손 못 댑니다
타임라인 구조를 div 덩어리로만 쌓으면 처음엔 잘 돌아가는 것처럼 보입니다. 그런데 이벤트가 10개를 넘어가는 순간 유지보수가 급격히 어려워집니다. 날짜를 하나 수정하려면 HTML 전체를 뒤져야 하고, 순서를 바꾸려면 div 블록을 통째로 이동시켜야 합니다. 솔직히 이건 예상 밖이었습니다. 처음 만들 때는 "이 정도면 충분하지" 했는데, 6개월 뒤에 내용을 업데이트하려다가 구조 파악 자체가 힘들었습니다.
이벤트가 10개 이상이라면 JavaScript로 데이터 배열을 렌더링하는 방식으로 전환하는 편이 훨씬 현실적입니다. 이벤트 정보를 객체 배열로 관리하면, 날짜나 텍스트를 고칠 때 데이터 파일만 수정하면 됩니다. 반대로 이벤트가 5개 이하인 경우엔 타임라인보다 간단한 목록이 더 읽기 쉽다는 의견에 저도 동의합니다. 구조를 위한 구조는 오히려 독자를 피로하게 만듭니다.
반응형 처리도 설계 단계에서 함께 잡아야 합니다. 뷰포트(viewport) 너비가 640px 이하일 때는 좌우 교대 배치를 단일 컬럼으로 전환하고, ::before로 그린 수직선도 left: 50%에서 left: 20px로 옮겨줍니다. 뷰포트란 사용자의 화면에서 실제로 보이는 웹 페이지의 영역을 가리킵니다. 이 작업을 미디어 쿼리(media query)로 처리하는데, 미디어 쿼리란 화면 크기나 해상도 조건에 따라 다른 CSS를 적용하는 기능입니다. 모바일에서도 타임라인 구조 자체는 그대로 살아있되, 좌우 분리 대신 왼쪽 정렬 단일 흐름으로 자연스럽게 바뀝니다.
- 이벤트 5개 이하 → 타임라인보다 단순 목록 추천. 과도한 시각화는 오히려 역효과
- 이벤트 6~15개 → CSS 타임라인 적합 구간. 좌우 교대 배치로 스크롤 길이 최적화
- 이벤트 20개 초과 → 스크롤 피로감 급증. JavaScript 렌더링 방식으로 전환 검토
접근성을 생략하면 타임라인이 절반짜리가 됩니다
CSS 타임라인을 만들 때 접근성 처리는 뒷전이 되기 쉽다는 분들도 있는데, 저는 그게 가장 위험한 생각이라고 봅니다. 타임라인은 시각적으로는 순서가 명확해 보이지만, 스크린 리더(screen reader)는 HTML 소스 순서대로 콘텐츠를 읽습니다. 스크린 리더란 화면을 볼 수 없는 사용자를 위해 화면의 텍스트와 구조를 음성으로 변환해주는 보조 기술입니다.
좌우 교대 배치를 쓰면 시각적 순서와 소스 순서가 어긋날 수 있습니다. 이때 ol 태그로 타임라인 전체를 감싸고 각 이벤트를 li로 처리하면, 스크린 리더가 순서를 올바르게 인식합니다. W3C의 웹 콘텐츠 접근성 지침(WCAG 2.1)에서는 정보의 순서와 의미가 시각에 의존하지 않아야 한다는 원칙을 명시하고 있습니다(출처: W3C WCAG 2.1). 제 경험상 이 원칙을 지키는 것 자체가 HTML 구조를 더 단단하게 만들어주는 부수 효과도 있었습니다.
타임라인의 원형 아이콘(dot)이나 수직선은 순수 장식 요소이므로, 스크린 리더가 읽지 않도록 aria-hidden="true"를 적용하는 게 맞습니다. aria-hidden이란 해당 요소를 보조 기술에서 완전히 숨기는 WAI-ARIA 속성입니다. 이 처리 없이 배포하면 스크린 리더 사용자가 의미 없는 가상 요소를 반복해서 듣게 됩니다. CSS만 잘 짜는 것보다 이런 디테일 하나가 실제 사용자 경험을 훨씬 크게 좌우합니다.
- ol + li 구조 사용 → 스크린 리더가 이벤트 순서를 올바르게 파악
- 장식용 가상 요소에 aria-hidden="true" 적용 → 불필요한 음성 출력 차단
- 날짜 정보에 time 태그 활용 → 기계가 날짜를 의미 있는 데이터로 인식
타임라인 UI는 "정보의 흐름을 시각화한다"는 목적이 분명할 때 빛을 발합니다. 제가 직접 써봤는데, 이걸 가장 실감한 순간은 독자가 댓글로 "연도별로 한눈에 보여서 좋았다"고 했을 때였습니다. 그 전까지 줄글로 쓴 포스팅에는 그런 반응이 거의 없었습니다.
처음 도전한다면 position: relative와 absolute 조합부터 손으로 직접 짜보시길 권합니다. 완성된 템플릿을 복붙하는 것보다, 수직선 하나를 직접 그리고 위치를 잡아보는 과정에서 CSS 레이아웃 전반에 대한 감각이 한꺼번에 붙습니다. 접근성 처리는 처음부터 함께 잡는 것이 나중에 다시 손보는 것보다 훨씬 쉽습니다.
참고: MDN Web Docs — CSS position / CSS Tricks — 타임라인 구현 예제 / W3C WCAG 2.1 접근성 가이드라인 / Smashing Magazine — UI 패턴 / MDN Web Docs — CSS pseudo-elements