Skip to Content
WebJavaScriptWeb APIs04. 프로젝트 시작 — DOM 선택·생성으로 메모 목록 렌더

이번 편의 결과물: 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)childparent의 마지막 자식으로 붙임
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-iddataset.id). 05편에서 카드 클릭 시 어떤 메모인지 찾는 데 이 속성을 그대로 씁니다.

HTML 속성dataset 접근
data-idelement.dataset.id
data-pinnedelement.dataset.pinned

실습

1. Vite vanilla 프로젝트 생성

npm create vite@latest notes-app -- --template vanilla cd notes-app npm install

2. 기본 템플릿 파일 정리

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.svg

3. 메모 시드 데이터 받기

메모 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하면 자동으로 자바스크립트 객체(배열)로 파싱해 줍니다. 별도의 fetchJSON.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-carddata-id="n1"처럼 속성이 붙어 있는 것을 확인합니다.
  • 콘솔에 document.querySelector('.note-card').dataset.id를 입력하면 "n1"이 출력됩니다.

직접 해보기

  1. notes.seed.json에 다섯 번째 메모 객체를 하나 추가하고 새로고침했을 때 카드가 5개로 늘어나는지 확인해봅니다.
  2. note.pinnedtrue인 카드에만 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-listrender.js의 셀렉터 이름이 다름두 파일에서 같은 id(note-list)를 쓰는지 확인
Failed to resolve import "./data/notes.seed.json"파일을 받지 않았거나 경로가 다름src/data/notes.seed.json 파일이 실제로 있는지 확인
카드가 계속 쌓여 중복됨appendChild를 다시 그릴 때마다 반복 호출replaceChildren으로 기존 자식을 먼저 비우고 교체

확인 문제

문제 14지선다
사용자 데이터를 화면에 표시할 때 innerHTML 대신 textContent를 권장하는 이유는?
문제 24지선다
HTML 속성 data-id를 자바스크립트에서 읽는 방법으로 옳은 것은?
문제 34지선다
listEl.replaceChildren(...cards)를 사용하는 이유로 가장 알맞은 것은?
문제 44지선다
Vite 프로젝트에서 import notesSeed from ./data/notes.seed.json 처럼 JSON 파일을 import했을 때 결과는?

참고 자료

Last updated on