이번 편의 결과물: notes-app을 실행하면 초기 메모 4개가 카드 목록으로 화면에 렌더링됩니다. · 다루는 개념: Vite vanilla 템플릿 정리, document.createElement/textContent, 배열→DOM 렌더 함수, data-* 속성
이 편에서 만드는 파일
notes-app/
├── index.html (~)
├── package.json (+)
└── src/
├── main.js (~)
├── render.js (+)
├── styles.css (+)
└── data/
└── notes.seed.json (+)개념 정리
document.createElement로 요소 만들기
HTML 문자열을 통째로 조립하는 대신, document.createElement로 요소를 하나씩 만들고 textContent로 글자를 채우는 방식을 씁니다. innerHTML에 사용자 데이터를 그대로 넣으면 문자열 안의 <, >가 태그로 해석되어 스크립트가 삽입될 수 있습니다(XSS). textContent는 문자열을 항상 순수 텍스트로만 넣습니다.
| 방법 | 동작 |
|---|---|
document.createElement('div') | 빈 div 요소 생성(아직 문서에 붙지 않음) |
element.textContent = text | 요소 안의 글자를 text로 설정(HTML로 해석 안 됨) |
parent.appendChild(child) | child를 parent의 마지막 자식으로 붙임 |
parent.replaceChildren(...nodes) | parent의 기존 자식을 모두 지우고 nodes로 교체 |
배열을 DOM으로 바꾸는 렌더 함수
메모 배열이 있을 때, 배열의 각 항목을 카드 요소로 바꾸고 그 카드들을 목록 컨테이너에 넣는 함수 하나로 화면을 그립니다. 이 함수는 이후 편에서 메모가 추가·삭제될 때마다 다시 호출하는 공통 진입점이 됩니다.
renderList(notes 배열, 담을 요소)
1. notes 배열의 각 note마다 카드 요소 생성
2. 카드 안에 제목·본문·태그를 textContent로 채움
3. 카드에 note.id를 data-*로 기록(05편에서 클릭 대상 찾을 때 사용)
4. 완성된 카드들을 담을 요소에 한 번에 반영data-* 속성
data-id="n1"처럼 data-로 시작하는 속성은 HTML/DOM 표준이 보장하는 사용자 정의 속성입니다. 자바스크립트에서는 element.dataset.id로 읽고 씁니다(data-id → dataset.id). 05편에서 카드 클릭 시 어떤 메모인지 찾는 데 이 속성을 그대로 씁니다.
| HTML 속성 | dataset 접근 |
|---|---|
data-id | element.dataset.id |
data-pinned | element.dataset.pinned |
실습
1. Vite vanilla 프로젝트 생성
npm create vite@latest notes-app -- --template vanilla
cd notes-app
npm install2. 기본 템플릿 파일 정리
Vite가 만든 counter.js, javascript.svg, public/vite.svg 파일을 삭제합니다. src/style.css도 삭제하고 이번 편에서 만들 src/styles.css로 대체합니다.
rm src/counter.js src/javascript.svg src/style.css public/vite.svg3. 메모 시드 데이터 받기
메모 4개가 담긴 시드 파일을 src/data/notes.seed.json으로 받습니다.
mkdir -p src/data
curl -o src/data/notes.seed.json https://zeno.it.kr/practice/notes-app/notes.seed.json터미널 대신 브라우저를 쓴다면 위 URL을 열어 Ctrl+S(또는 Cmd+S)로 src/data/notes.seed.json 경로에 저장합니다. 받은 파일은 아래와 같은 배열입니다.
// src/data/notes.seed.json (일부)
[
{
"id": "n1",
"title": "Vite 프로젝트 만들기",
"body": "npm create vite@latest notes-app -- --template vanilla 로 시작한다.",
"tags": ["setup"],
"pinned": true,
"updatedAt": "2026-09-15T09:00:00+09:00"
}
]4. index.html 정리
<!-- index.html -->
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>notes-app</title>
</head>
<body>
<div id="app">
<header class="app-header">
<h1>메모</h1>
</header>
<ul id="note-list" class="note-list"></ul>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>ul#note-list가 05편부터 목록 컨테이너로 계속 쓰입니다. 지금은 빈 상태로 두고 자바스크립트가 채웁니다.
5. 렌더 함수 작성
// src/render.js
export function renderList(notes, listEl) {
const cards = notes.map((note) => createNoteCard(note));
listEl.replaceChildren(...cards);
}
function createNoteCard(note) {
const li = document.createElement('li');
li.className = 'note-card';
li.dataset.id = note.id;
const title = document.createElement('h2');
title.className = 'note-title';
title.textContent = note.title;
const body = document.createElement('p');
body.className = 'note-body';
body.textContent = note.body;
const tagList = document.createElement('div');
tagList.className = 'note-tags';
for (const tag of note.tags) {
const tagEl = document.createElement('span');
tagEl.className = 'note-tag';
tagEl.textContent = tag;
tagList.appendChild(tagEl);
}
li.append(title, body, tagList);
return li;
}replaceChildren은 기존 자식을 한 번에 지우고 새 카드로 교체합니다. 카드마다 appendChild를 반복하는 대신 이 방식을 쓰면 목록을 다시 그릴 때 이전 카드가 남아 중복되는 실수를 피할 수 있습니다.
6. 진입점 작성
// src/main.js
import notesSeed from './data/notes.seed.json';
import { renderList } from './render.js';
import './styles.css';
const listEl = document.querySelector('#note-list');
renderList(notesSeed, listEl);Vite는 .json 파일을 import하면 자동으로 자바스크립트 객체(배열)로 파싱해 줍니다. 별도의 fetch나 JSON.parse가 필요 없습니다.
7. 최소 스타일 작성
/* src/styles.css */
body {
font-family: system-ui, sans-serif;
background: #f4f4f5;
margin: 0;
padding: 24px;
}
.app-header h1 {
font-size: 20px;
margin: 0 0 16px;
}
.note-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 12px;
}
.note-card {
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.note-title {
margin: 0 0 8px;
font-size: 16px;
}
.note-body {
margin: 0 0 8px;
color: #52525b;
font-size: 14px;
}
.note-tags {
display: flex;
gap: 6px;
}
.note-tag {
font-size: 12px;
background: #e4e4e7;
border-radius: 999px;
padding: 2px 8px;
}8. 실행
npm run dev확인
- 터미널에 나온
http://localhost:5173주소로 접속하면 “메모” 제목 아래 카드 4개가 보입니다. - 각 카드에 제목·본문·태그(회색 알약 모양)가 표시됩니다.
- 개발자 도구
Elements탭에서li.note-card에data-id="n1"처럼 속성이 붙어 있는 것을 확인합니다. - 콘솔에
document.querySelector('.note-card').dataset.id를 입력하면"n1"이 출력됩니다.
직접 해보기
notes.seed.json에 다섯 번째 메모 객체를 하나 추가하고 새로고침했을 때 카드가 5개로 늘어나는지 확인해봅니다.note.pinned가true인 카드에만note-card--pinned클래스를 추가로 붙여, CSS로 왼쪽 테두리 색을 다르게 표시해봅니다.
정답 보기
// src/render.js의 createNoteCard 함수 중 li 생성 부분만 교체
function createNoteCard(note) {
const li = document.createElement('li');
li.className = note.pinned ? 'note-card note-card--pinned' : 'note-card';
li.dataset.id = note.id;
// ...기존 코드 유지(title, body, tagList 생성과 append)
return li;
}/* src/styles.css에 추가 */
.note-card--pinned {
border-left: 4px solid #f59e0b;
}자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
| 화면에 아무것도 안 뜸 | index.html의 #note-list와 render.js의 셀렉터 이름이 다름 | 두 파일에서 같은 id(note-list)를 쓰는지 확인 |
Failed to resolve import "./data/notes.seed.json" | 파일을 받지 않았거나 경로가 다름 | src/data/notes.seed.json 파일이 실제로 있는지 확인 |
| 카드가 계속 쌓여 중복됨 | appendChild를 다시 그릴 때마다 반복 호출 | replaceChildren으로 기존 자식을 먼저 비우고 교체 |