JavaScript Date에서 UTC 시점 저장과 KST 표시는 어떻게 구분하나요?
시점은 UTC ISO 문자열로 저장하고 서울 표시는 Intl.DateTimeFormat의 Asia/Seoul로 만듭니다. 표시 시간대를 바꿔도 원본 시점은 같습니다. 시간대 없는 입력은 실행 환경부터 확인합니다.
적용 조건·범위: 실행 검증: Node.js에서 위 출력과 일치했습니다. datetime-local 입력의 해석은 실행 환경의 로컬 시간대에 따르므로 실제 폼 입력과 브라우저 표시는 미확인입니다.
공식 문서 참고: MDN: Date 시점·시간대·UTC ISO 문자열 · MDN: Intl.DateTimeFormat의 표시 시간대 지정
혼자 실행해 보는 UTC 저장과 KST 표시
JavaScript Date UTC KST 차이: 시간대 변환 기준 잡기 · 독립 실습
같은 시각의 ISO 저장값과 한국 시간 표시를 비교하세요.
예제 크게 보기 (새 탭)독자 질문: 하나의 UTC 시점을 서울 시간으로 보여줄 때 원본 시점은 변하는가?
js-10-example.mjs로 저장하고 node js-10-example.mjs을 실행하세요. 외부 패키지는 필요하지 않습니다.
const stored = '2026-09-20T00:30:00.000Z';
const date = new Date(stored);
const formatter = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit',
day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
});
console.log('저장:', date.toISOString());
console.log('서울:', formatter.format(date));
console.log('유효:', !Number.isNaN(date.getTime()));
console.log('오류:', Number.isNaN(new Date('invalid').getTime()));
예상 출력:
저장: 2026-09-20T00:30:00.000Z
서울: 2026-09-20, 09:30
유효: true
오류: true
실행 검증: Node.js에서 위 출력과 일치했습니다. datetime-local 입력의 해석은 실행 환경의 로컬 시간대에 따르므로 실제 폼 입력과 브라우저 표시는 미확인입니다.
직접 풀어볼 문제
stored를 2026-09-20T18:30:00.000Z로 바꾼 후 서울 표시 날짜가 21일로 넘어가는지 확인하세요.
Date 객체는 특정 시점을 표현합니다
const date = new Date('2026-09-20T00:00:00Z');
console.log(date.getTime());
내부적으로는 특정 시점을 나타내는 숫자 값으로 다룹니다. 화면에 어떤 시간대로 표시할지는 별도 문제입니다.
toISOString()은 UTC 기준 문자열을 반환합니다
const date = new Date();
console.log(date.toISOString());
예:
2026-09-20T04:30:00.000Z
끝의 Z는 UTC를 의미합니다.
같은 시점을 KST로 표시하기
const formatter = new Intl.DateTimeFormat('ko-KR', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'Asia/Seoul',
});
console.log(formatter.format(new Date('2026-09-20T04:30:00.000Z')));
저장값을 KST 문자열로 억지로 바꾸기보다 시점은 ISO로 저장하고 표시 단계에서 원하는 시간대를 적용하면 역할이 분명해집니다.

offset 없는 문자열은 주의합니다
new Date('2026-09-20T13:00:00');
이 문자열에는 Z나 +09:00 같은 offset이 없습니다. 이런 값은 실행 환경의 로컬 시간 기준으로 해석됩니다.
datetime-local 입력값도 시간대 정보가 없는 문자열이므로 사용자의 로컬 시각으로 Date를 만든 뒤 ISO로 변환하는 흐름을 명시해야 합니다.
Invalid Date 확인
const date = new Date('not-a-date');
console.log(Number.isNaN(date.getTime()));
날짜 문자열을 받는 함수에서는 실제 Date로 변환 가능한지 확인합니다.
날짜만 필요한 값과 정확한 시점 구분
생일처럼 ‘2026-09-20’ 자체가 의미인 데이터와 로그 생성 시각처럼 정확한 순간이 중요한 데이터는 성격이 다릅니다.
이번 TaskFlow의 createdAt, dueAt은 정확한 시점으로 처리합니다.
자주 발생하는 오류
UTC 저장값에 직접 +9를 더합니다
서머타임이나 다른 시간대 확장까지 고려하면 직접 시간 계산보다 Intl.DateTimeFormat의 timeZone 옵션을 사용하는 편이 안전합니다.
잘못된 날짜를 그대로 표시합니다
Invalid Date 여부를 먼저 확인하고 대체 문구를 제공합니다.
표시 문자열을 다시 데이터 원본으로 사용합니다
사용자용 포맷 문자열과 저장용 ISO 문자열을 분리합니다.
학습 완료 체크리스트
Date가 시점을 다룬다는 의미를 설명할 수 있습니다.- ISO UTC 문자열을 만들 수 있습니다.
Intl.DateTimeFormat으로 KST 표시를 만들 수 있습니다.- offset 없는 입력의 의미를 구분할 수 있습니다.
- Invalid Date를 확인할 수 있습니다.
독립 실습은 여기서 마칠 수 있습니다. 아래 선택 실습은 TaskFlow 프로젝트 파일로 진행합니다.
선택 실습: 할 일 앱에 적용하기
TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 JavaScript로 화면에 동작을 붙입니다.
작업 form에 마감 시각 필드를 추가하고 아래 날짜 모듈과 store/view 변경을 함께 적용합니다.
선택: 연재 프로젝트와 전체 자료
연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.
TaskFlow 프로젝트 적용
JavaScript Date UTC KST 차이: 시간대 변환 기준 잡기 · 단계별 TaskFlow
작업 추가·완료 전환·삭제와 현재 단계의 필터/출력을 확인하세요. 검색 조건을 바꿔 가상 URL을 확인하세요.
예제 크게 보기 (새 탭)변경 설명과 부분 예제를 읽은 뒤, 시작·완료 파일과 적용 순서에서 필요한 선언·import·화면 연결을 포함한 전체 파일을 확인하세요. 이번 단계의 변경 이유와 적용 위치도 함께 확인합니다.
시작본과 이번 변경
JavaScript URLSearchParams 사용법: URL 파라미터 읽고 수정하기의 완료 체크포인트를 시작본으로 사용합니다.
js/date.js를 추가하고 task-store.js의 addTask 및 초기 데이터에 createdAt·dueAt을 반영합니다. task-view.js는 toIsoFromLocal과 formatKstDateTime을 ./date.js에서 import하고 bindTaskForm의 제출 콜백에서 마감 입력을 읽습니다. app.js의 onAdd 인수를 객체로 맞추고 마감 없음·유효 날짜·잘못된 날짜를 확인하세요.
연속 프로젝트 선수 조건
- TaskFlow 작업 객체 구조
- URL 필터/검색 상태
javascript-step-09-url-state체크포인트
이전 체크포인트
javascript-step-09-url-state
form에 마감 시각 추가
작업 이름 입력 아래에 선택 필드를 추가합니다.
<div class="form-field">
<label for="task-due-at">마감 시각</label>
<input
id="task-due-at"
name="dueAt"
type="datetime-local"
/>
</div>
이번 회차 변경 코드와 전체본 연결
작업 form에 마감 시각 필드를 추가하고 아래 날짜 모듈과 store/view 변경을 함께 적용합니다.
전체 파일·코드 보기
js/date.js
const kstFormatter = new Intl.DateTimeFormat('ko-KR', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'Asia/Seoul',
});
export function toIsoFromLocal(value) {
if (!value) return null;
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return null;
}
return date.toISOString();
}
export function formatKstDateTime(value) {
if (!value) return '없음';
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '잘못된 날짜';
}
return kstFormatter.format(date);
}
task-store.js의 작업 생성 규칙
export function addTask(title, dueAt = null) {
const task = {
id: String(Date.now()),
title,
completed: false,
createdAt: new Date().toISOString(),
dueAt,
};
tasks = [...tasks, task];
return task;
}
초기 tasks에도 같은 필드를 맞춥니다.
{
id: '1',
title: 'JavaScript 과정 정리',
completed: false,
createdAt: '2026-09-20T00:00:00.000Z',
dueAt: null,
}
form binding 변경
const dueAtInput = document.querySelector('#task-due-at');
const dueAt = toIsoFromLocal(dueAtInput.value);
onAdd({ title, dueAt });
app.js:
bindTaskForm(({ title, dueAt }) => {
addTask(title, dueAt);
render();
});
렌더링에서 날짜 표시
const meta = document.createElement('small');
meta.textContent =
`생성 ${formatKstDateTime(task.createdAt)} · 마감 ${formatKstDateTime(task.dueAt)}`;
task-view.js에서 formatKstDateTime을 import해 사용합니다.
검증
- 마감 시각 없이 작업 추가
- 마감 시각을 입력해 작업 추가
- store의
createdAt,dueAt이 ISO 문자열인지 확인 - 화면에서는 KST 형식으로 표시되는지 확인
- 잘못된 날짜 입력을 함수 단위로 확인
중간부터 시작하는 전체 파일과 적용 순서
시작 ZIP은 직전 프로젝트 단계 javascript-step-09-url-state의 실제 완료 파일입니다. 프로젝트 순서를 이어가는 기준이며 사이트의 기술 읽기 순서와는 구분합니다.
- 시작 ZIP을 새 폴더에 풉니다. 이 폴더가 작업 루트이며 기존 체크포인트 파일을 별도로 찾을 필요가 없습니다.
- 마감 입력, date 모듈, store 날짜 필드, view의 import/콜백/날짜 요소, app의 객체 인수를 함께 적용합니다.
- 아래 변경 파일을 같은 상대 경로에 추가하거나 전체 교체합니다. 표시하지 않은 파일과 images는 시작본 그대로 유지합니다. package-lock.json은 완료 ZIP에 있는 파일을 함께 적용합니다.
- 작업 루트에서 명령을 실행하고 예상 결과를 확인합니다. 완료 ZIP은 직접 적용한 결과를 비교하는 기준입니다.
추가·교체 파일
index.html— 전체 교체CHANGES.md— 전체 교체README.md— 전체 교체js/app.js— 전체 교체js/task-store.js— 전체 교체js/task-view.js— 전체 교체js/date.js— 추가
실행과 확인
npm start
npm test
개발 서버는 실행 상태로 두고 다른 터미널에서 검사 명령을 실행하세요. Node.js 24.18.0에서 검증했습니다. JavaScript는 http://127.0.0.1:4173, TypeScript는 Vite 실행 출력의 주소로 접속합니다.
예상 결과: 마감 없음은 없음, 유효 날짜는 ISO 저장/KST 표시, 잘못된 날짜는 잘못된 날짜로 표시합니다.
실제 검증: JavaScript 모듈·DOM·경계값 실행 검사 통과. 시작 ZIP에 이 글의 변경 파일만 적용한 결과와 완료 ZIP의 모든 파일 내용이 일치했습니다. 브라우저 화면 배치·실제 기기·외부 CDN 가용성은 이 검사에 포함하지 않았습니다.
이 절의 전체 파일은 ZIP과 일치합니다. 위쪽 원문의 짧은 예제나 생략 표시가 있는 부분 코드는 개념·변경 위치 설명용입니다. 서버 저장과 인증은 제공하지 않으며 JSON fixture는 로컬 읽기 실습용입니다.
복사할 변경 파일 전체 코드
index.html — 전체 파일
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>
TaskFlow - 간단한 작업 정리 서비스
</title>
<link href="./style.css" rel="stylesheet"/>
<meta content="TaskFlow의 주요 기능과 요금제, 문의 방법을 확인할 수 있는 작업 관리 서비스 소개 페이지입니다." name="description"/>
</head>
<body>
<a class="skip-link" href="#main">
본문으로 바로가기
</a>
<header class="site-header">
<div class="page site-header__inner">
<a class="brand" href="#home">
TaskFlow
</a>
<nav aria-label="주요 메뉴" class="nav">
<a href="#features">
기능
</a>
<a href="#pricing">
요금제
</a>
<a href="#contact">
문의
</a>
<a href="#faq">
FAQ
</a>
</nav>
</div>
</header>
<main class="page" id="main" tabindex="-1">
<div class="service-layout">
<div>
<section class="hero" id="home">
<div>
<h1>
해야 할 일을 한눈에 정리하세요
</h1>
<p>
TaskFlow는 오늘의 작업을 빠르게 정리하는 서비스입니다.
</p>
<a class="button-link" href="#contact">
시작 문의하기
</a>
</div>
<img alt="TaskFlow 서비스 소개 페이지의 기능·문의 화면" class="hero__image" height="800" src="./images/hero.webp" width="1200"/>
</section>
<section id="features">
<h2>
주요 기능
</h2>
<div class="feature-grid">
<article class="card feature-card">
<span class="feature-card__badge">
NEW
</span>
<h3>
빠른 작업 추가
</h3>
<p>
해야 할 일을 바로 기록할 수 있습니다.
</p>
<img alt="TaskFlow 기능 카드 세 개" class="feature-card__image" height="450" loading="lazy" src="./images/feature-board.webp" width="800"/>
</article>
<article class="card">
<h3>
간단한 분류
</h3>
<p>
해야 할 일을 구분합니다.
</p>
</article>
<article class="card">
<h3>
진행 확인
</h3>
<p>
완료 여부를 확인합니다.
</p>
</article>
<article class="card task-preview">
<div class="task-preview__content">
<h3>
아주 긴 작업 제목과 URL
</h3>
<p>
https://example.com/very-long-task-url-without-normal-break-points-123456789
</p>
</div>
<a href="#contact">
문의
</a>
</article>
</div>
</section>
<section class="faq" id="faq">
<h2>
자주 묻는 질문
</h2>
<details>
<summary>
TaskFlow는 어떤 서비스인가요?
</summary>
<p>
해야 할 일을 빠르게 정리하는 작업 관리 서비스 예제입니다.
</p>
</details>
<details>
<summary>
모바일에서도 볼 수 있나요?
</summary>
<p>
작은 화면부터 설계한 반응형 레이아웃을 사용합니다.
</p>
</details>
<details>
<summary>
문의는 어디서 하나요?
</summary>
<p>
아래 문의 폼을 이용할 수 있습니다.
</p>
</details>
</section>
<section id="pricing">
<h2>
요금제
</h2>
<div class="pricing-grid">
<article class="card">
<h3>
Basic
</h3>
<p>
개인 학습용
</p>
</article>
<article class="card">
<h3>
Pro
</h3>
<p>
더 많은 프로젝트
</p>
</article>
</div>
<div aria-label="요금제 비교표 가로 스크롤" class="table-scroll" role="region" tabindex="0">
<table class="pricing-table">
<caption>
TaskFlow 요금제 기능 비교
</caption>
<thead>
<tr>
<th scope="col">
항목
</th>
<th scope="col">
Basic
</th>
<th scope="col">
Pro
</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">
프로젝트
</th>
<td>
3개
</td>
<td>
무제한
</td>
</tr>
<tr>
<th scope="row">
작업 보관
</th>
<td>
30일
</td>
<td>
무제한
</td>
</tr>
<tr>
<th scope="row">
팀 공유
</th>
<td>
없음
</td>
<td>
지원
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section id="contact">
<h2>
문의하기
</h2>
<form>
<div class="form-field">
<label for="name">
이름
</label>
<input autocomplete="name" id="name" name="name" type="text"/>
</div>
<div class="form-field">
<label for="email">
이메일
</label>
<input autocomplete="email" id="email" name="email" type="email"/>
</div>
<div class="form-field">
<label for="message">
문의 내용
</label>
<textarea id="message" name="message" rows="5"></textarea>
</div>
<button type="submit">
문의 보내기
</button>
</form>
</section>
<div class="feature-slot">
<article class="card feature-card">
<img alt="TaskFlow 기능 카드 세 개" class="feature-card__image" height="450" loading="lazy" src="./images/feature-board.webp" width="800"/>
<div>
<h3>
작업 카드
</h3>
<p>
넓은 공간에서는 이미지와 설명을 나란히 배치합니다.
</p>
</div>
</article>
</div>
</div>
<aside aria-label="기능 미리보기">
<div class="feature-slot">
<article class="card feature-card">
<img alt="TaskFlow 기능 카드 세 개" class="feature-card__image" height="450" loading="lazy" src="./images/feature-board.webp" width="800"/>
<div>
<h3>
작업 카드
</h3>
<p>
넓은 공간에서는 이미지와 설명을 나란히 배치합니다.
</p>
</div>
</article>
</div>
</aside>
</div>
<section
class="task-demo"
id="task-demo"
aria-labelledby="task-demo-title"
>
<h2 id="task-demo-title">작업 관리 데모</h2>
<p>JavaScript 과정에서 이 영역을 단계적으로 동작하게 만듭니다.</p>
<form class="task-form" id="task-form" novalidate>
<div class="form-field">
<label for="task-title">작업 이름</label>
<input
id="task-title"
name="title"
type="text"
maxlength="80"
required
aria-describedby="task-message"
/>
</div>
<button type="submit">작업 추가</button>
<div class="form-field">
<label for="task-due-at">마감 시각</label>
<input
id="task-due-at"
name="dueAt"
type="datetime-local"
/>
</div>
</form><div class="task-controls">
<label for="task-status">상태</label>
<select id="task-status">
<option value="all">전체</option>
<option value="pending">미완료</option>
<option value="completed">완료</option>
</select>
<label for="task-search">검색</label>
<input id="task-search" type="search" />
</div>
<p id="task-message" aria-live="polite"></p>
<div id="task-summary" aria-live="polite"></div>
<ul class="task-list" id="task-list"></ul>
</section>
</main>
<footer class="site-footer">
<div class="page">
<p>
© TaskFlow
</p>
</div>
</footer>
<a class="help-button" href="#contact">
문의
</a>
<script type="module" src="./js/app.js"></script></body>
</html>
js/app.js — 전체 파일
import {
addTask,
deleteTask,
filterTasks,
getTaskSummary,
toggleTask,
} from './task-store.js';
import {
bindTaskControls,
bindTaskForm,
bindTaskList,
getTaskControls,
renderTasks,
setTaskControls,
} from './task-view.js';
import { readUrlState, writeUrlState } from './url-state.js';
function render() {
const controls = getTaskControls();
const tasks = filterTasks(controls.status, controls.query);
renderTasks(tasks, getTaskSummary());
}
const initialUrlState = readUrlState();
setTaskControls(initialUrlState);
bindTaskForm(({ title, dueAt }) => {
addTask(title, dueAt);
render();
});
bindTaskControls(() => {
const controls = getTaskControls();
writeUrlState(controls);
render();
});
bindTaskList(({ id, action }) => {
if (action === 'toggle') toggleTask(id);
if (action === 'delete') deleteTask(id);
render();
});
render();
js/task-store.js — 전체 파일
let tasks = [
{ id: '1', title: 'JavaScript 과정 정리', completed: false, createdAt: '2026-09-20T00:00:00.000Z', dueAt: null },
{ id: '2', title: 'TaskFlow 화면 확인', completed: true, createdAt: '2026-09-20T00:00:00.000Z', dueAt: null },
{ id: '3', title: '다음 글 읽기', completed: false, createdAt: '2026-09-20T00:00:00.000Z', dueAt: null },
];
export function getTasks() {
return [...tasks];
}
export function addTask(title, dueAt = null) {
const task = {
id: crypto.randomUUID(),
title,
completed: false,
createdAt: new Date().toISOString(),
dueAt,
};
tasks = [...tasks, task];
return task;
}
export function toggleTask(id) {
let updatedTask = null;
tasks = tasks.map((task) => {
if (task.id !== id) return task;
updatedTask = {
...task,
completed: !task.completed,
};
return updatedTask;
});
return updatedTask;
}
export function deleteTask(id) {
tasks = tasks.filter((task) => task.id !== id);
}
export function filterTasks(status = 'all', query = '') {
const normalizedQuery = query.trim().toLowerCase();
return tasks.filter((task) => {
const matchesStatus =
status === 'all' ||
(status === 'completed' && task.completed) ||
(status === 'pending' && !task.completed);
const matchesQuery =
!normalizedQuery ||
task.title.toLowerCase().includes(normalizedQuery);
return matchesStatus && matchesQuery;
});
}
export function getTaskSummary() {
return tasks.reduce(
(summary, task) => {
summary.total += 1;
if (task.completed) {
summary.completed += 1;
} else {
summary.pending += 1;
}
return summary;
},
{ total: 0, completed: 0, pending: 0 },
);
}
js/task-view.js — 전체 파일
import { toIsoFromLocal, formatKstDateTime } from './date.js';
export function bindTaskForm(onAdd) {
const form = document.querySelector('#task-form');
const titleInput = document.querySelector('#task-title');
const message = document.querySelector('#task-message');
if (!form || !titleInput || !message) {
throw new Error('TaskFlow 작업 폼 요소를 찾을 수 없습니다.');
}
form.addEventListener('submit', (event) => {
event.preventDefault();
const title = titleInput.value.trim();
if (!title) {
titleInput.setAttribute('aria-invalid', 'true');
message.textContent = '작업 이름을 입력해 주세요.';
titleInput.focus();
return;
}
const dueAtInput = document.querySelector('#task-due-at');
onAdd({ title, dueAt: toIsoFromLocal(dueAtInput.value) });
dueAtInput.value = '';
titleInput.value = '';
titleInput.setAttribute('aria-invalid', 'false');
message.textContent = '작업을 추가했습니다.';
titleInput.focus();
});
}
export function bindTaskControls(onChange) {
const status = document.querySelector('#task-status');
const search = document.querySelector('#task-search');
if (!status || !search) {
throw new Error('TaskFlow 필터 요소를 찾을 수 없습니다.');
}
status.addEventListener('change', onChange);
search.addEventListener('input', onChange);
}
export function getTaskControls() {
return {
status: document.querySelector('#task-status').value,
query: document.querySelector('#task-search').value,
};
}
export function bindTaskList(onAction) {
const list = document.querySelector('#task-list');
if (!list) {
throw new Error('task-list를 찾을 수 없습니다.');
}
list.addEventListener('click', (event) => {
const button = event.target.closest('button[data-action]');
const item = button?.closest('[data-task-id]');
if (!button || !item) return;
onAction({
id: item.dataset.taskId,
action: button.dataset.action,
});
});
}
export function renderTasks(tasks, summary) {
const list = document.querySelector('#task-list');
const summaryElement = document.querySelector('#task-summary');
list.replaceChildren();
summaryElement.textContent =
`전체 ${summary.total} · 완료 ${summary.completed} · 미완료 ${summary.pending}`;
if (tasks.length === 0) {
const empty = document.createElement('li');
empty.textContent = '조건에 맞는 작업이 없습니다.';
list.append(empty);
return;
}
tasks.forEach((task) => {
const item = document.createElement('li');
item.dataset.taskId = String(task.id);
const title = document.createElement('span');
title.textContent = task.title;
const toggleButton = document.createElement('button');
toggleButton.type = 'button';
toggleButton.dataset.action = 'toggle';
toggleButton.textContent = task.completed ? '미완료로 변경' : '완료';
const deleteButton = document.createElement('button');
deleteButton.type = 'button';
deleteButton.dataset.action = 'delete';
deleteButton.textContent = '삭제';
const meta = document.createElement('small');
meta.textContent =
`생성 ${formatKstDateTime(task.createdAt)} · 마감 ${formatKstDateTime(task.dueAt)}`;
item.append(title, meta, toggleButton, deleteButton);
list.append(item);
});
}
export function setTaskControls({ status, query }) {
document.querySelector('#task-status').value = status;
document.querySelector('#task-search').value = query;
}
js/date.js — 전체 파일
const kstFormatter = new Intl.DateTimeFormat('ko-KR', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'Asia/Seoul',
});
export function toIsoFromLocal(value) {
if (!value) return null;
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return null;
}
return date.toISOString();
}
export function formatKstDateTime(value) {
if (!value) return '없음';
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '잘못된 날짜';
}
return kstFormatter.format(date);
}
완료 체크포인트
javascript-step-10-date-time
다음 학습
다음 글에서는 지금까지 전역 모듈 변수로 관리하던 tasks를 클로저 안으로 옮기고 store의 공개 API를 확정합니다.
다음 글: JavaScript 실행 컨텍스트 기준: 스코프 호이스팅 클로저 연결하기
글 소개·학습 준비·전체 목차

JavaScript · 10 / 15 · 필수 학습 · Date UTC KST
정확한 시점은 toISOString()의 UTC ISO 문자열로 저장하고, KST 표시는 Intl.DateTimeFormat의 timeZone: Asia/Seoul로 만듭니다. 표시 시간대를 바꾸어도 원본 시점은 같으며, datetime-local처럼 offset 없는 날짜·시간 입력은 실행 환경의 로컬 시간대부터 확인합니다.
이번 글에서는 JavaScript Date가 다루는 시점, ISO 문자열, UTC와 KST 표시 차이를 정리하고 TaskFlow 작업 데이터에 생성 시각과 마감 시각을 추가합니다.
이번 글에서 배우는 내용
Date와 시점- ISO 8601 문자열
toISOString()- UTC 저장과 로컬 표시
Intl.DateTimeFormatInvalid Datedatetime-local값 처리
먼저 알아야 할 내용
문자열, 객체, 함수 호출을 알면 됩니다. 날짜·시간대 예제는 TaskFlow 없이 Node.js에서 실행합니다.
목차
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 15개 · 필수 14개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.