Skip to Content
WebJavaScriptECMAScript12. Date와 Intl로 마감일·통계 표시

이번 편의 결과물: list에 마감일이 “D-4”처럼 표시되고, todo stats가 전체 완료율을 백분율로 출력합니다. · 다루는 개념: Date 생성·비교, Intl.DateTimeFormat, Intl.NumberFormat/RelativeTimeFormat

이 편에서 만드는 파일

todo-cli/ └── src/ ├── utils/ │ └── date.js (+) └── commands/ ├── list.js (~) └── stats.js (+)

src/index.js에도 stats 명령 분기가 하나 추가됩니다.

개념 정리

Date 생성과 비교

new Date(문자열)은 ISO 형식('2026-09-20') 문자열을 날짜 객체로 만듭니다. 두 Date 객체는 - 연산으로 밀리초 차이를 구할 수 있습니다.

표현결과
new Date('2026-09-20')2026년 9월 20일 자정(UTC) 기준 Date 객체
dateA - dateB두 날짜의 차이(밀리초, number)
date.getFullYear(), getMonth(), getDate()로컬 기준 연/월(0부터 시작)/일

밀리초 차이를 일 단위로 바꾸려면 1000(밀리초) × 60(초) × 60(분) × 24(시간)으로 나눕니다. 시(時)·분·초까지 있는 두 시각을 “며칠 차이”로 비교하려면, 비교 전에 두 날짜 모두 자정 시각으로 맞춰야 날짜 경계에서 오차가 생기지 않습니다.

Intl.DateTimeFormat — 로케일 기반 날짜 서식

new Intl.DateTimeFormat('ko-KR', { dateStyle: 'medium' }).format(new Date('2026-09-20')); // '2026. 9. 20.'

직접 문자열을 조립하지 않고 Intl.DateTimeFormat에 로케일과 서식 옵션을 넘기면, 해당 로케일 표기 규칙에 맞는 날짜 문자열을 얻습니다.

Intl.RelativeTimeFormat — 상대 시간 표현

new Intl.RelativeTimeFormat('ko', { numeric: 'auto' }).format(4, 'day'); // '4일 후'

format(값, 단위)에서 값이 양수면 미래, 음수면 과거를 뜻합니다. numeric: 'auto'는 값이 -1, 0, 1처럼 흔한 경우 “어제”, “오늘”, “내일” 같은 자연스러운 표현을 우선 시도합니다. 실행 환경(Node.js 버전, 로케일 데이터)에 따라 정확한 문구는 조금 다를 수 있습니다.

Intl.NumberFormat — 퍼센트 서식

new Intl.NumberFormat('ko-KR', { style: 'percent', maximumFractionDigits: 0 }).format(0.4); // '40%'

style: 'percent'를 지정하면 0.4 같은 비율 값을 직접 100을 곱하지 않아도 “40%“로 표시합니다.

실습

1. src/utils/date.js 파일 만들기

날짜 계산과 서식을 한 곳에 모읍니다.

2. 코드 작성

// src/utils/date.js export function formatDueBadge(dueString, now = new Date()) { if (!dueString) return '마감 없음'; const due = startOfDay(new Date(dueString)); const today = startOfDay(now); const diffDays = Math.round((due - today) / (1000 * 60 * 60 * 24)); const relative = new Intl.RelativeTimeFormat('ko', { numeric: 'auto' }).format(diffDays, 'day'); if (diffDays === 0) return '오늘 마감'; if (diffDays > 0) return `D-${diffDays} (${relative})`; return `D+${Math.abs(diffDays)} (${relative})`; } export function formatDate(dateString) { return new Intl.DateTimeFormat('ko-KR', { dateStyle: 'medium' }).format(new Date(dateString)); } function startOfDay(date) { return new Date(date.getFullYear(), date.getMonth(), date.getDate()); }

startOfDay로 시·분·초를 지운 뒤 빼야 “오늘 밤 11시”와 “내일 새벽 1시”처럼 실제로는 하루 차이인데 몇 시간밖에 차이 나지 않는 두 시각이 잘못 비교되지 않습니다. now 매개변수에 기본값 new Date()를 줘서, 테스트할 때는 원하는 기준일을 직접 넘길 수 있게 열어둡니다.

src/commands/list.js에서 마감일 표시를 formatDueBadge로 바꿉니다.

// src/commands/list.js import { formatDueBadge } from '../utils/date.js'; const PAGE_SIZE = 2; export function runList(store, flags) { const result = store.list(flags); if (flags.page === undefined) { printList(result); return; } const pageNumber = Number(flags.page); let index = 1; for (const page of paginate(result, PAGE_SIZE)) { if (index === pageNumber) { console.log(`--- ${pageNumber}페이지 ---`); printList(page); return; } index += 1; } console.log(`${pageNumber}페이지는 존재하지 않습니다. 전체 ${index - 1}페이지입니다.`); } function* paginate(items, pageSize) { for (let i = 0; i < items.length; i += pageSize) { yield items.slice(i, i + pageSize); } } function printList(list) { list.forEach(({ id, title, done, tags, due }) => { const mark = done ? '[x]' : '[ ]'; const tagText = tags.length > 0 ? tags.join(', ') : '없음'; console.log(`${mark} #${id} ${title} (태그: ${tagText}, 마감: ${formatDueBadge(due)})`); }); }

마지막으로 통계 명령을 추가합니다.

// src/commands/stats.js export function runStats(store) { const all = store.toArray(); const total = all.length; const done = all.filter((todo) => todo.done).length; const rate = total === 0 ? 0 : done / total; const percent = new Intl.NumberFormat('ko-KR', { style: 'percent', maximumFractionDigits: 0 }).format(rate); console.log(`전체 ${total}개 중 완료 ${done}개 (완료율 ${percent})`); }

src/index.jsstats 명령 분기를 추가합니다.

// src/index.js import { parseArgs } from './cli/parseArgs.js'; import { TodoStore } from './models/TodoStore.js'; import { loadTodos, saveTodos } from './storage/fileStore.js'; import { runAdd } from './commands/add.js'; import { runList } from './commands/list.js'; import { runTags } from './commands/tags.js'; import { runSearch } from './commands/search.js'; import { runStats } from './commands/stats.js'; const argv = process.argv.slice(2); const { command, positional, flags } = parseArgs(argv); const rawTodos = await loadTodos(); const store = TodoStore.fromArray(rawTodos); if (command === 'add') { await runAdd(store, positional, flags); await saveTodos(store.toArray().map((todo) => todo.toJSON())); } else if (command === 'list') { runList(store, flags); } else if (command === 'tags') { runTags(store); } else if (command === 'search') { runSearch(store, positional); } else if (command === 'stats') { runStats(store); } else { console.log('알 수 없는 명령입니다:', command); }

3. 실행

node src/index.js list
node src/index.js stats

확인

  • 오늘을 2026-09-16으로 가정하면, seed 데이터의 마감일이 아래처럼 표시됩니다.
[x] #1 ECMAScript 03편 실습 끝내기 (태그: study, 마감: D-2 (2일 후)) [ ] #2 포트폴리오 프로젝트 카드 6개 채우기 (태그: portfolio, html, 마감: D-4 (4일 후)) [ ] #3 todo-cli에 검색 명령 추가 (태그: study, node, 마감: D-6 (6일 후)) [ ] #4 운동 30분 (태그: health, 마감: 마감 없음) [x] #5 독서 기록 앱 별점 기능 (태그: react, 마감: D+2 (2일 전))
  • stats 실행 결과(5개 중 2개 완료):
전체 5개 중 완료 2개 (완료율 40%)
  • 로케일 데이터 버전에 따라 Intl.RelativeTimeFormat의 괄호 안 문구(“2일 후” 등)는 조금 다르게 보일 수 있지만 D-4, D+2 같은 핵심 표시는 항상 동일합니다.

직접 해보기

  1. formatDatestats 결과에 추가해, “오늘(2026. 9. 16. 기준)” 같은 문구를 함께 출력해봅니다.
  2. Intl.NumberFormatmaximumFractionDigits1로 바꿔 완료율이 소수점 첫째 자리까지 나오는지 확인합니다.

정답 보기

// src/commands/stats.js import { formatDate } from '../utils/date.js'; export function runStats(store) { const all = store.toArray(); const total = all.length; const done = all.filter((todo) => todo.done).length; const rate = total === 0 ? 0 : done / total; const percent = new Intl.NumberFormat('ko-KR', { style: 'percent', maximumFractionDigits: 0 }).format(rate); console.log(`오늘(${formatDate(new Date().toISOString())} 기준)`); console.log(`전체 ${total}개 중 완료 ${done}개 (완료율 ${percent})`); }

자주 하는 실수

증상원인고치는 법
D-day가 하루씩 어긋남시·분·초까지 포함한 채로 날짜를 뺌startOfDay로 자정 시각으로 맞춘 뒤 뺀다
완료율이 NaN%로 나옴목록이 비어 있을 때 0으로 나눔total === 0일 때는 rate0으로 고정한다
Intl.DateTimeFormat 결과가 예상과 다른 표기로 나옴로케일 문자열('ko-KR')을 빠뜨리거나 오타로케일 코드와 dateStyle 옵션 철자를 확인한다

확인 문제

문제 14지선다
두 Date 객체를 dateA - dateB로 뺐을 때 반환되는 값의 단위는?
문제 24지선다
formatDueBadge에서 startOfDay로 두 날짜를 자정 시각으로 맞춘 뒤 비교하는 이유는?
문제 34지선다
new Intl.NumberFormat('ko-KR', { style: 'percent' }).format(0.4)의 결과는?
문제 44지선다
완료율 계산에서 total이 0일 때 rate를 0으로 고정하는 이유는?

참고 자료

Last updated on