Guider/Frontend/FrontendDevGuide0005
Frontend#05

FrontendDevGuide0005

JavaScript ์‹ฌํ™” (DOM & API)

๐Ÿงฉ JavaScript ์‹ฌํ™” — DOM & API ์™„์ „ ์ •๋ณต

๐Ÿ“‹ 5๋‹จ๊ณ„ ํ•™์Šต ๊ฐœ์š” | DOM ์กฐ์ž‘ + fetch API | ์˜ˆ์ƒ ํ•™์Šต ๊ธฐ๊ฐ„: 3~4์ฃผ

์ด์ „ ๋‹จ๊ณ„์—์„œ JavaScript ๋ฌธ๋ฒ•์„ ์ตํ˜”๋‹ค๋ฉด, ์ด๋ฒˆ์—” ๊ทธ ์ง€์‹์„ ์‹ค์ œ ์›น ํŽ˜์ด์ง€๋ฅผ ์›€์ง์ด๋Š” ํž˜์œผ๋กœ ๋ฐ”๊ฟ”๋ด…๋‹ˆ๋‹ค.
DOM์œผ๋กœ HTML์„ ์ž์œ ๋กญ๊ฒŒ ์กฐ์ž‘ํ•˜๊ณ , fetch API๋กœ ์„œ๋ฒ„์™€ ํ†ต์‹ ํ•˜๋Š” ๋ฒ•์„ ๋ฐฐ์›๋‹ˆ๋‹ค.
์ด ๋‹จ๊ณ„๋ฅผ ๋งˆ์น˜๋ฉด ํ™”๋ฉด์ด ์‚ด์•„ ์›€์ง์ด๋Š” ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์›น์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค!

๐ŸŒณ 1. DOM์ด๋ž€? — HTML ๋ฌธ์„œ๋ฅผ JS๋กœ ๋‹ค๋ฃจ๋Š” ์—ด์‡ 

DOM(Document Object Model)์€ HTML ๋ฌธ์„œ๋ฅผ ๋‚˜๋ฌด(Tree) ๊ตฌ์กฐ๋กœ ํ‘œํ˜„ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.
๋ธŒ๋ผ์šฐ์ €๊ฐ€ HTML ํŒŒ์ผ์„ ์ฝ์„ ๋•Œ, ๋ชจ๋“  ํƒœ๊ทธ๋ฅผ '๋…ธ๋“œ(Node)' ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค์–ด์š”.
JavaScript๋Š” ์ด ํŠธ๋ฆฌ๋ฅผ ํƒ์ƒ‰·์ˆ˜์ •·์‚ญ์ œ·์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” API๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“Œ ๋น„์œ : DOM์€ HTML์˜ ๊ฐ€๊ณ„๋„(์กฑ๋ณด)์ž…๋‹ˆ๋‹ค. ๋ถ€๋ชจ-์ž์‹ ๊ด€๊ณ„๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ , JS๋Š” ๊ทธ ๊ฐ€๊ณ„๋„๋ฅผ ๋งˆ์Œ๋Œ€๋กœ ์ˆ˜์ •ํ•˜๋Š” ํŽธ์ง‘์ž ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

DOM ํŠธ๋ฆฌ ๊ตฌ์กฐ ์‹œ๊ฐํ™”


๐Ÿ” 2. DOM ์š”์†Œ ์„ ํƒ — ์›ํ•˜๋Š” ์š”์†Œ ์ฐพ์•„๋‚ด๊ธฐ

JavaScript๋กœ HTML ์š”์†Œ๋ฅผ ์กฐ์ž‘ํ•˜๋ ค๋ฉด ๋จผ์ € ํ•ด๋‹น ์š”์†Œ๋ฅผ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. querySelector๊ฐ€ ํ˜„์—…์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

DOM ์„ ํƒ ๋ฉ”์„œ๋“œ ๋น„๊ตํ‘œ

๐Ÿ”‘ ํ˜„์—… ์„ ํƒ ๊ฐ€์ด๋“œ:
querySelector / querySelectorAll → ๋Œ€๋ถ€๋ถ„์˜ ์ƒํ™ฉ์—์„œ ์‚ฌ์šฉ (CSS ์„ ํƒ์ž ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ)
getElementById → ์„ฑ๋Šฅ์ด ์ค‘์š”ํ•  ๋•Œ (๊ฐ€์žฅ ๋น ๋ฆ„)
closest() → ์ด๋ฒคํŠธ ์œ„์ž„(delegation)์—์„œ ๋งค์šฐ ์œ ์šฉ

๐Ÿ’ป ์‹ค์ „ ์ฝ”๋“œ ์˜ˆ์‹œ:

// CSS ์„ ํƒ์ž๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
const title = document.querySelector('h1');                   // ์ฒซ ๋ฒˆ์งธ h1
const allCards = document.querySelectorAll('.card');          // ๋ชจ๋“  .card
const btn = document.querySelector('#submit-btn');            // id๋กœ
const activeItem = document.querySelector('ul li.active');    // ๋ณตํ•ฉ ์„ ํƒ์ž

// NodeList ์ˆœํšŒ
allCards.forEach(card => {
  card.style.border = '1px solid #eee';
});

// ์š”์†Œ๊ฐ€ ์—†์„ ์ˆ˜๋„ ์žˆ์œผ๋ฏ€๋กœ ์ฒดํฌ
const maybeNull = document.querySelector('.not-exist');
if (maybeNull) {
  maybeNull.textContent = '์ฐพ์•˜๋‹ค!';
}

โš™๏ธ 3. DOM ์กฐ์ž‘ — ์š”์†Œ๋ฅผ ๋ณ€๊ฒฝ·์ถ”๊ฐ€·์‚ญ์ œํ•˜๊ธฐ

์š”์†Œ๋ฅผ ์„ ํƒํ–ˆ์œผ๋ฉด ์ด์ œ ์ž์œ ๋กญ๊ฒŒ ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ…์ŠคํŠธ ๋ณ€๊ฒฝ, ์Šคํƒ€์ผ ๋ณ€๊ฒฝ, ํด๋ž˜์Šค ์ œ์–ด, ์ƒˆ ์š”์†Œ ์ƒ์„ฑ ๋“ฑ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“Œ textContent vs innerHTML
textContent — ์ˆœ์ˆ˜ ํ…์ŠคํŠธ๋งŒ ๋ณ€๊ฒฝ (XSS ์•ˆ์ „ โœ…)
innerHTML — HTML ํƒœ๊ทธ ํฌํ•จ ๋ณ€๊ฒฝ (์‚ฌ์šฉ์ž ์ž…๋ ฅ๊ฐ’์— ์‚ฌ์šฉ ์‹œ XSS ์œ„ํ—˜ โš ๏ธ)
ํ˜„์—…์—์„œ๋Š” ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•  ๋•Œ ๋ฐ˜๋“œ์‹œ textContent ์‚ฌ์šฉ ๊ถŒ์žฅ!
const title = document.querySelector('h1');

// โ‘  ํ…์ŠคํŠธ / HTML ๋ณ€๊ฒฝ
title.textContent = '์ƒˆ๋กœ์šด ์ œ๋ชฉ';      // ์•ˆ์ „ํ•œ ๋ฐฉ๋ฒ•
title.innerHTML = '<strong>๊ตต๊ฒŒ</strong> ์ œ๋ชฉ';  // HTML ์‚ฝ์ž… ์‹œ

// โ‘ก ์Šคํƒ€์ผ ์ง์ ‘ ๋ณ€๊ฒฝ (์ธ๋ผ์ธ ์Šคํƒ€์ผ)
title.style.color = '#e74c3c';
title.style.fontSize = '2rem';
title.style.display = 'none';          // ์ˆจ๊ธฐ๊ธฐ

// โ‘ข ํด๋ž˜์Šค ์กฐ์ž‘ (๊ถŒ์žฅ ๋ฐฉ์‹ — CSS์—์„œ ์Šคํƒ€์ผ ๊ด€๋ฆฌ)
title.classList.add('highlight');      // ์ถ”๊ฐ€
title.classList.remove('highlight');   // ์ œ๊ฑฐ
title.classList.toggle('active');      // ์—†์œผ๋ฉด ์ถ”๊ฐ€, ์žˆ์œผ๋ฉด ์ œ๊ฑฐ
title.classList.contains('active');    // ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ → true/false
title.classList.replace('old', 'new'); // ๊ต์ฒด

// โ‘ฃ ์†์„ฑ ์กฐ์ž‘
const img = document.querySelector('img');
img.setAttribute('src', 'new.jpg');
img.getAttribute('alt');
img.removeAttribute('disabled');

// โ‘ค ์ƒˆ ์š”์†Œ ์ƒ์„ฑ ๋ฐ ์ถ”๊ฐ€
const newCard = document.createElement('div');
newCard.classList.add('card');
newCard.textContent = '์ƒˆ ์นด๋“œ!';
document.body.appendChild(newCard);         // ๋งจ ๋์— ์ถ”๊ฐ€

const list = document.querySelector('ul');
const newItem = document.createElement('li');
newItem.textContent = '์ƒˆ ํ•ญ๋ชฉ';
list.insertBefore(newItem, list.firstChild); // ๋งจ ์•ž์— ์ถ”๊ฐ€

// โ‘ฅ ์š”์†Œ ์‚ญ์ œ
const oldEl = document.querySelector('.remove-me');
oldEl.remove();                              // ํ˜„๋Œ€์  ๋ฐฉ์‹

โšก 4. ์ด๋ฒคํŠธ — ์‚ฌ์šฉ์ž ๋™์ž‘์— ๋ฐ˜์‘ํ•˜๊ธฐ

์ด๋ฒคํŠธ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๊ฑฐ๋‚˜, ํ‚ค๋ณด๋“œ๋ฅผ ๋ˆ„๋ฅด๊ฑฐ๋‚˜, ๋งˆ์šฐ์Šค๋ฅผ ์›€์ง์ด๋Š” ๋“ฑ์˜ ๋ชจ๋“  ๋™์ž‘์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. addEventListener๋กœ ์ด๋ฒคํŠธ๋ฅผ '๊ฐ์‹œ'ํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฒคํŠธ ์ข…๋ฅ˜ ๋ฐ ์‚ฌ์šฉ ํŒจํ„ด

// ๊ธฐ๋ณธ ํŒจํ„ด
const btn = document.querySelector('#myBtn');

btn.addEventListener('click', function(event) {
  console.log('ํด๋ฆญ๋จ!');
  console.log(event.target);       // ํด๋ฆญ๋œ ์‹ค์ œ ์š”์†Œ
  console.log(event.currentTarget); // ๋ฆฌ์Šค๋„ˆ๊ฐ€ ๋‹ฌ๋ฆฐ ์š”์†Œ
});

// ํ™”์‚ดํ‘œ ํ•จ์ˆ˜ ์‚ฌ์šฉ
btn.addEventListener('click', (e) => {
  e.preventDefault();  // ๊ธฐ๋ณธ ๋™์ž‘ ๋ฐฉ์ง€ (๋งํฌ ์ด๋™, ํผ ์ œ์ถœ ๋“ฑ)
  e.stopPropagation(); // ์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง ์ค‘๋‹จ
});

// ํ‚ค๋ณด๋“œ ์ด๋ฒคํŠธ
document.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') console.log('์—”ํ„ฐ ๋ˆŒ๋ฆผ!');
  if (e.key === 'Escape') console.log('ESC ๋ˆŒ๋ฆผ!');
  if (e.ctrlKey && e.key === 's') {
    e.preventDefault(); // Ctrl+S ๊ธฐ๋ณธ๋™์ž‘ ๋ง‰๊ธฐ
    saveWork();
  }
});

// ํผ ์ œ์ถœ ์ฒ˜๋ฆฌ (ํ˜„์—… ํ•ต์‹ฌ ํŒจํ„ด)
const form = document.querySelector('#myForm');
form.addEventListener('submit', async (e) => {
  e.preventDefault(); // ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ ๋ฐฉ์ง€!
  const formData = new FormData(form);
  const data = Object.fromEntries(formData);
  console.log(data); // ํผ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ์ฒด๋กœ
  // await fetch๋กœ ์„œ๋ฒ„์— ์ „์†ก...
});

๐Ÿซง ์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง & ์ด๋ฒคํŠธ ์œ„์ž„

์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง๊ณผ ์บก์ฒ˜๋ง ํ๋ฆ„๋„

๐Ÿš€ ์ด๋ฒคํŠธ ์œ„์ž„(Event Delegation) — ํ˜„์—… ํ•„์ˆ˜ ํŒจํ„ด!
๋™์ ์œผ๋กœ ์ถ”๊ฐ€๋˜๋Š” ์š”์†Œ๋“ค์— ์ด๋ฒคํŠธ๋ฅผ ์ผ์ผ์ด ๋ถ™์ด์ง€ ์•Š๊ณ , ๋ถ€๋ชจ ์š”์†Œ ํ•˜๋‚˜์—๋งŒ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋‹ฌ๊ณ  ์ž์‹ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
์˜ˆ: ๋ฆฌ์ŠคํŠธ ํ•ญ๋ชฉ 100๊ฐœ → ํ•ญ๋ชฉ๋งˆ๋‹ค ๋ฆฌ์Šค๋„ˆ โŒ → ๋ถ€๋ชจ ul์— ํ•˜๋‚˜๋งŒ โœ…
// โŒ ๋น„ํšจ์œจ์ ์ธ ๋ฐฉ๋ฒ• (๊ฐ ๋ฒ„ํŠผ๋งˆ๋‹ค ๋ฆฌ์Šค๋„ˆ)
document.querySelectorAll('.delete-btn').forEach(btn => {
  btn.addEventListener('click', () => { /* ์‚ญ์ œ ์ฒ˜๋ฆฌ */ });
});

// โœ… ์ด๋ฒคํŠธ ์œ„์ž„ (๋ถ€๋ชจ์— ํ•˜๋‚˜๋งŒ)
document.querySelector('#list').addEventListener('click', (e) => {
  if (e.target.matches('.delete-btn')) {
    e.target.closest('.item').remove();
  }
  if (e.target.matches('.edit-btn')) {
    // ์ˆ˜์ • ์ฒ˜๋ฆฌ
  }
});

๐Ÿ“š 5. ๋ฐฐ์—ด & ๊ฐ์ฒด ๊ณ ๊ธ‰ ๋ฉ”์„œ๋“œ — ๋ฐ์ดํ„ฐ ๊ฐ€๊ณต์˜ ํ•ต์‹ฌ

ํ˜„์—…์—์„œ๋Š” ์„œ๋ฒ„์—์„œ ๋ฐ›์€ ๋ฐฐ์—ด ๋ฐ์ดํ„ฐ๋ฅผ map → filter → reduce ์กฐํ•ฉ์œผ๋กœ ๊ฐ€๊ณตํ•˜๋Š” ํŒจํ„ด์„ ๋งค์šฐ ์ž์ฃผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

๋ฐฐ์—ด ๊ณ ๊ธ‰ ๋ฉ”์„œ๋“œ ์™„์ „ ์ •๋ฆฌํ‘œ

// ์‹ค์ „ ์˜ˆ์‹œ: ์ƒํ’ˆ ๋ชฉ๋ก ๊ฐ€๊ณต
const products = [
  { id: 1, name: '๋…ธํŠธ๋ถ', price: 1200000, category: 'electronics', inStock: true },
  { id: 2, name: 'ํ‚ค๋ณด๋“œ', price: 80000, category: 'electronics', inStock: false },
  { id: 3, name: '๋งˆ์šฐ์Šค', price: 30000, category: 'electronics', inStock: true },
  { id: 4, name: '์ฑ…์ƒ', price: 300000, category: 'furniture', inStock: true },
];

// map — ์ด๋ฆ„๋งŒ ์ถ”์ถœ
const names = products.map(p => p.name);
// ['๋…ธํŠธ๋ถ', 'ํ‚ค๋ณด๋“œ', '๋งˆ์šฐ์Šค', '์ฑ…์ƒ']

// filter — ์žฌ๊ณ  ์žˆ๋Š” ๊ฒƒ๋งŒ
const available = products.filter(p => p.inStock);

// map + filter ์ฒด์ด๋‹ (ํ˜„์—… ํ•ต์‹ฌ!)
const affordableNames = products
  .filter(p => p.inStock && p.price < 200000)
  .map(p => p.name);
// ['๋งˆ์šฐ์Šค', '์ฑ…์ƒ']

// reduce — ์ด ๊ธˆ์•ก ๊ณ„์‚ฐ
const totalPrice = products.reduce((sum, p) => sum + p.price, 0);
// 1610000

// find — ํŠน์ • ์ƒํ’ˆ ์ฐพ๊ธฐ
const laptop = products.find(p => p.name === '๋…ธํŠธ๋ถ');

// sort — ๊ฐ€๊ฒฉ์ˆœ ์ •๋ ฌ (์›๋ณธ ๋ณ€๊ฒฝ ์ฃผ์˜!)
const sorted = [...products].sort((a, b) => a.price - b.price);

// ๊ฐ์ฒด ๊ณ ๊ธ‰ ํ™œ์šฉ
const { name, price, ...rest } = products[0]; // ๊ตฌ์กฐ ๋ถ„ํ•ด
const merged = { ...products[0], price: 999000 }; // ์Šคํ”„๋ ˆ๋“œ๋กœ ์—…๋ฐ์ดํŠธ
const keys = Object.keys(products[0]);   // ['id', 'name', 'price', ...]
const entries = Object.entries(products[0]); // [['id', 1], ['name', '๋…ธํŠธ๋ถ'], ...]

๐ŸŒ 6. fetch API — ์„œ๋ฒ„์™€ ํ†ต์‹ ํ•˜๊ธฐ (AJAX)

fetch API ์š”์ฒญ ํ๋ฆ„๋„

AJAX๋ž€? — ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ ์—†์ด ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ฑฐ๋‚˜ ๋ณด๋‚ด๋Š” ๊ธฐ์ˆ 
์˜ˆ์ „์—๋Š” XMLHttpRequest๋ฅผ ์‚ฌ์šฉํ–ˆ์ง€๋งŒ, ํ˜„์žฌ๋Š” fetch API(๋˜๋Š” axios ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ)๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
// โ‘  ๊ธฐ๋ณธ GET ์š”์ฒญ (๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ)
async function fetchUsers() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    
    // ์‘๋‹ต ์ƒํƒœ ํ™•์ธ
    if (!response.ok) {
      throw new Error('HTTP Error: ' + response.status);
    }
    
    const users = await response.json(); // JSON → JS ๊ฐ์ฒด ๋ณ€ํ™˜
    console.log(users);
  } catch (error) {
    console.error('์š”์ฒญ ์‹คํŒจ:', error.message);
  }
}

// โ‘ก POST ์š”์ฒญ (๋ฐ์ดํ„ฐ ๋ณด๋‚ด๊ธฐ)
async function createPost(postData) {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',    // JSON ์ „์†ก ๋ช…์‹œ
      'Authorization': 'Bearer ' + token,   // ์ธ์ฆ ํ† ํฐ
    },
    body: JSON.stringify(postData),           // JS ๊ฐ์ฒด → JSON ๋ฌธ์ž์—ด
  });
  
  const result = await response.json();
  return result;
}

// โ‘ข ์—ฌ๋Ÿฌ ์š”์ฒญ ๋™์‹œ์— (Promise.all — ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋กœ ์†๋„ ์ตœ์ ํ™”)
async function fetchAll() {
  const [users, posts] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
  ]);
  // ๋‘ ์š”์ฒญ์ด ๋™์‹œ์— ๋‚ ์•„๊ฐ€๊ณ , ๋‘˜ ๋‹ค ์™„๋ฃŒ๋˜๋ฉด ์‹คํ–‰๋จ
}

// โ‘ฃ AbortController — ์š”์ฒญ ์ทจ์†Œ (๊ฒ€์ƒ‰ ์ž๋™์™„์„ฑ ๋“ฑ)
let controller;
async function search(query) {
  if (controller) controller.abort(); // ์ด์ „ ์š”์ฒญ ์ทจ์†Œ
  controller = new AbortController();
  
  const res = await fetch('/api/search?q=' + query, {
    signal: controller.signal
  });
  return res.json();
}

๐Ÿ”„ HTTP ๋ฉ”์„œ๋“œ & REST API ๊ธฐ์ดˆ

// RESTful API ํŒจํ„ด (ํ˜„์—… ํ‘œ์ค€)
GET    /api/users        → ๋ชฉ๋ก ์กฐํšŒ
GET    /api/users/1      → ๋‹จ๊ฑด ์กฐํšŒ
POST   /api/users        → ์ƒ์„ฑ
PUT    /api/users/1      → ์ „์ฒด ์ˆ˜์ •
PATCH  /api/users/1      → ์ผ๋ถ€ ์ˆ˜์ •
DELETE /api/users/1      → ์‚ญ์ œ

// HTTP ์ƒํƒœ ์ฝ”๋“œ
200 OK             → ์„ฑ๊ณต
201 Created        → ์ƒ์„ฑ ์„ฑ๊ณต
400 Bad Request    → ์š”์ฒญ ํ˜•์‹ ์˜ค๋ฅ˜
401 Unauthorized   → ์ธ์ฆ ํ•„์š”
403 Forbidden      → ๊ถŒํ•œ ์—†์Œ
404 Not Found      → ๋ฆฌ์†Œ์Šค ์—†์Œ
500 Server Error   → ์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜

๐Ÿ—„๏ธ 7. ๋ธŒ๋ผ์šฐ์ € ์Šคํ† ๋ฆฌ์ง€ — ๋ฐ์ดํ„ฐ ์ €์žฅํ•˜๊ธฐ

๋ธŒ๋ผ์šฐ์ € ์Šคํ† ๋ฆฌ์ง€ ๋น„๊ตํ‘œ

// ===== localStorage ์‹ค์ „ ํŒจํ„ด =====

// ๋‹คํฌ๋ชจ๋“œ ์„ค์ • ์ €์žฅ
function setTheme(theme) {
  localStorage.setItem('theme', theme);
  document.body.dataset.theme = theme;
}
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);

// ๊ฐ์ฒด/๋ฐฐ์—ด ์ €์žฅ (JSON ์ง๋ ฌํ™” ํ•„์ˆ˜)
const cartItems = [
  { id: 1, name: '์ƒํ’ˆA', qty: 2 },
  { id: 2, name: '์ƒํ’ˆB', qty: 1 },
];
localStorage.setItem('cart', JSON.stringify(cartItems));
const cart = JSON.parse(localStorage.getItem('cart')) || [];

// ===== Cookie ์‚ฌ์šฉ (๊ฐ„๋‹จํ•œ ๋ฐฉ์‹) =====
document.cookie = 'token=abc123; expires=Fri, 31 Dec 2026 23:59:59 GMT; path=/; Secure; HttpOnly';

// ===== IndexedDB (๋Œ€์šฉ๋Ÿ‰ ๊ตฌ์กฐํ™” ๋ฐ์ดํ„ฐ) =====
// ๋น„๋™๊ธฐ Key-Value ์ €์žฅ์†Œ — ์ˆ˜์‹ญ MB ๊ฐ€๋Šฅ
// ํ˜„์—…์—์„œ๋Š” idb ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋˜๋Š” Dexie.js ์‚ฌ์šฉ ์ถ”์ฒœ

๐ŸŽฏ 8. ์‹ค์ „ ํ”„๋กœ์ ํŠธ — ๋‚ ์”จ ์•ฑ ๋งŒ๋“ค๊ธฐ

์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด DOM ์กฐ์ž‘ + fetch API๋ฅผ ๋ชจ๋‘ ํ™œ์šฉํ•œ ์‹ค์ „ ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค.
open-meteo.com (๋ฌด๋ฃŒ, API ํ‚ค ๋ถˆํ•„์š”) ๋‚ ์”จ API๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
// index.html
// <button id="getWeather">๋‚ ์”จ ํ™•์ธ</button>
// <div id="weatherResult"></div>

const btn = document.querySelector('#getWeather');
const resultDiv = document.querySelector('#weatherResult');

btn.addEventListener('click', async () => {
  btn.textContent = '๋กœ๋”ฉ ์ค‘...';
  btn.disabled = true;
  resultDiv.innerHTML = '';

  try {
    // 1. ๋‚ ์”จ API ์š”์ฒญ (์„œ์šธ ๊ธฐ์ค€)
    const url = 'https://api.open-meteo.com/v1/forecast'
      + '?latitude=37.56&longitude=126.97'
      + '&current_weather=true'
      + '&hourly=temperature_2m,precipitation_probability';
    
    const response = await fetch(url);
    if (!response.ok) throw new Error('๋‚ ์”จ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.');
    
    const data = await response.json();
    const weather = data.current_weather;
    
    // 2. DOM ์—…๋ฐ์ดํŠธ
    const card = document.createElement('div');
    card.classList.add('weather-card');
    card.innerHTML = [
      '<h2>๐ŸŒค๏ธ ์„œ์šธ ํ˜„์žฌ ๋‚ ์”จ</h2>',
      '<p>๐ŸŒก๏ธ ์˜จ๋„: ' + weather.temperature + '°C</p>',
      '<p>๐Ÿ’จ ํ’์†: ' + weather.windspeed + ' km/h</p>',
      '<p>๐Ÿ• ์‹œ๊ฐ: ' + new Date(weather.time).toLocaleString('ko-KR') + '</p>',
    ].join('');
    
    resultDiv.appendChild(card);
    
    // 3. ์˜ค๋Š˜ ์‹œ๊ฐ„๋ณ„ ๊ธฐ์˜จ (์ฒ˜์Œ 8๊ฐœ = 24์‹œ๊ฐ„)
    const hourlyTemps = data.hourly.temperature_2m.slice(0, 8);
    const hourlyList = document.createElement('ul');
    hourlyTemps.forEach((temp, i) => {
      const li = document.createElement('li');
      li.textContent = (i * 3) + '์‹œ: ' + temp + '°C';
      hourlyList.appendChild(li);
    });
    resultDiv.appendChild(hourlyList);
    
  } catch (error) {
    resultDiv.textContent = '์˜ค๋ฅ˜: ' + error.message;
  } finally {
    btn.textContent = '๋‚ ์”จ ํ™•์ธ';
    btn.disabled = false;
  }
});

๐Ÿ”ฅ 9. ํ˜„์—… ํ•„์ˆ˜ ๊ณ ๊ธ‰ ๊ธฐ๋ฒ•๋“ค

โ‘  Intersection Observer — ๋ฌดํ•œ ์Šคํฌ๋กค & ์Šคํฌ๋กค ์• ๋‹ˆ๋ฉ”์ด์…˜

// ์š”์†Œ๊ฐ€ ๋ทฐํฌํŠธ์— ๋“ค์–ด์˜ฌ ๋•Œ ๊ฐ์ง€ (์Šคํฌ๋กค ์ด๋ฒคํŠธ๋ณด๋‹ค ์„ฑ๋Šฅ ์šฐ์ˆ˜!)
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('visible'); // ์• ๋‹ˆ๋ฉ”์ด์…˜ ํŠธ๋ฆฌ๊ฑฐ
      observer.unobserve(entry.target);      // ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰ ์‹œ
    }
  });
}, { threshold: 0.2 }); // 20% ๋ณด์ผ ๋•Œ ์‹คํ–‰

document.querySelectorAll('.fade-in').forEach(el => observer.observe(el));

// ๋ฌดํ•œ ์Šคํฌ๋กค ํŒจํ„ด
const loadMoreObserver = new IntersectionObserver(async (entries) => {
  if (entries[0].isIntersecting) {
    const newItems = await fetchMoreItems();
    renderItems(newItems);
  }
});
loadMoreObserver.observe(document.querySelector('#load-trigger'));

โ‘ก MutationObserver — DOM ๋ณ€ํ™” ๊ฐ์ง€

const observer = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    if (mutation.type === 'childList') {
      console.log('์ž์‹ ์š”์†Œ ์ถ”๊ฐ€/์‚ญ์ œ:', mutation.addedNodes);
    }
    if (mutation.type === 'attributes') {
      console.log('์†์„ฑ ๋ณ€๊ฒฝ:', mutation.attributeName);
    }
  });
});

observer.observe(document.body, {
  childList: true,   // ์ž์‹ ์ถ”๊ฐ€/์‚ญ์ œ ๊ฐ์ง€
  subtree: true,     // ํ•˜์œ„ ๋ชจ๋“  ๋…ธ๋“œ ๊ฐ์ง€
  attributes: true,  // ์†์„ฑ ๋ณ€๊ฒฝ ๊ฐ์ง€
});

โ‘ข debounce & throttle — ์ด๋ฒคํŠธ ์„ฑ๋Šฅ ์ตœ์ ํ™”

// debounce: ๋งˆ์ง€๋ง‰ ํ˜ธ์ถœ ํ›„ N ms ๋’ค์— ์‹คํ–‰ (๊ฒ€์ƒ‰ ์ž๋™์™„์„ฑ)
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

const searchInput = document.querySelector('#search');
searchInput.addEventListener('input', debounce(async (e) => {
  const results = await fetch('/api/search?q=' + e.target.value);
  // ...
}, 300)); // 300ms ๋™์•ˆ ์ž…๋ ฅ์ด ์—†์„ ๋•Œ ์‹คํ–‰

// throttle: N ms์— ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰ (์Šคํฌ๋กค ์ด๋ฒคํŠธ)
function throttle(fn, limit) {
  let inThrottle;
  return function(...args) {
    if (!inThrottle) {
      fn.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

window.addEventListener('scroll', throttle(() => {
  console.log('์Šคํฌ๋กค ์œ„์น˜:', window.scrollY);
}, 100));

โ‘ฃ ํด๋ฆฝ๋ณด๋“œ & ๋‹ค์ด์–ผ๋กœ๊ทธ API

// ํด๋ฆฝ๋ณด๋“œ ๋ณต์‚ฌ (ํ˜„๋Œ€์  ๋ฐฉ๋ฒ•)
async function copyToClipboard(text) {
  try {
    await navigator.clipboard.writeText(text);
    showToast('๋ณต์‚ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!');
  } catch (err) {
    console.error('๋ณต์‚ฌ ์‹คํŒจ:', err);
  }
}

// ๋ธŒ๋ผ์šฐ์ € ๋‹ค์ด์–ผ๋กœ๊ทธ
const ok = confirm('์ •๋ง ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?');
const name = prompt('์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”:', 'ํ™๊ธธ๋™');

// ํ˜„๋Œ€์  ๋Œ€ํ™”์ƒ์ž: <dialog> ์š”์†Œ
const modal = document.querySelector('#myModal');
modal.showModal(); // ๋ชจ๋‹ฌ๋กœ ์—ด๊ธฐ
modal.close();     // ๋‹ซ๊ธฐ

๐Ÿ“– 10. ์ž์ฃผ ์“ฐ๋Š” Web API ๋ชจ์Œ

// โ‘  URL & ์ฟผ๋ฆฌ์ŠคํŠธ๋ง ๋‹ค๋ฃจ๊ธฐ
const url = new URL('https://example.com/search?q=js&page=2');
url.searchParams.get('q');           // 'js'
url.searchParams.set('page', '3');
url.searchParams.append('filter', 'new');

// โ‘ก Geolocation (์œ„์น˜ ์ •๋ณด)
navigator.geolocation.getCurrentPosition(
  (pos) => console.log(pos.coords.latitude, pos.coords.longitude),
  (err) => console.error('์œ„์น˜ ์ ‘๊ทผ ๊ฑฐ๋ถ€')
);

// โ‘ข Notification API (๋ธŒ๋ผ์šฐ์ € ์•Œ๋ฆผ)
async function notify() {
  const permission = await Notification.requestPermission();
  if (permission === 'granted') {
    new Notification('์•Œ๋ฆผ ์ œ๋ชฉ', { body: '์•Œ๋ฆผ ๋‚ด์šฉ', icon: '/logo.png' });
  }
}

// โ‘ฃ history API (SPA ๋ผ์šฐํŒ… ํ•ต์‹ฌ)
history.pushState({ page: 2 }, '', '/page/2'); // URL ๋ณ€๊ฒฝ (์ƒˆ๋กœ๊ณ ์นจ ์—†์ด)
window.addEventListener('popstate', (e) => {
  console.log('๋’ค๋กœ๊ฐ€๊ธฐ/์•ž์œผ๋กœ๊ฐ€๊ธฐ:', e.state);
});

// โ‘ค Web Workers (๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ)
const worker = new Worker('worker.js'); // ๋ฌด๊ฑฐ์šด ๊ณ„์‚ฐ์„ ๋ณ„๋„ ์Šค๋ ˆ๋“œ์—์„œ
worker.postMessage({ data: bigData });
worker.onmessage = (e) => console.log('๊ณ„์‚ฐ ๊ฒฐ๊ณผ:', e.data);

// โ‘ฅ ResizeObserver (์š”์†Œ ํฌ๊ธฐ ๋ณ€ํ™” ๊ฐ์ง€)
const resizeObserver = new ResizeObserver(entries => {
  for (const entry of entries) {
    console.log('ํฌ๊ธฐ ๋ณ€๊ฒฝ:', entry.contentRect.width);
  }
});
resizeObserver.observe(document.querySelector('.resizable'));

โœ… 5๋‹จ๊ณ„ ํ•™์Šต ์ฒดํฌ๋ฆฌ์ŠคํŠธ

โœ… ๊ธฐ์ดˆ (๋ฐ˜๋“œ์‹œ ๋งˆ์Šคํ„ฐ)
โ˜ querySelector / querySelectorAll๋กœ ์š”์†Œ ์„ ํƒ
โ˜ textContent, classList๋กœ DOM ์กฐ์ž‘
โ˜ addEventListener๋กœ ํด๋ฆญ·ํผ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
โ˜ fetch + async/await๋กœ GET ์š”์ฒญ
โ˜ map / filter / reduce ์‚ฌ์šฉ

๐Ÿš€ ์‹ฌํ™” (ํ˜„์—… ์ˆ˜์ค€)
โ˜ ์ด๋ฒคํŠธ ์œ„์ž„ ํŒจํ„ด ๊ตฌํ˜„
โ˜ POST ์š”์ฒญ์œผ๋กœ ๋ฐ์ดํ„ฐ ์ „์†ก
โ˜ Promise.all ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ
โ˜ debounce๋กœ ๊ฒ€์ƒ‰ ์ตœ์ ํ™”
โ˜ localStorage๋กœ ์ƒํƒœ ์œ ์ง€
โ˜ Intersection Observer๋กœ ์Šคํฌ๋กค ์• ๋‹ˆ๋ฉ”์ด์…˜

๐Ÿ† ์‹ค์ „ ๋ฏธ์…˜
โ˜ ๋‚ ์”จ ์•ฑ (fetch + DOM ์กฐ์ž‘)
โ˜ Todo ์•ฑ (์ด๋ฒคํŠธ ์œ„์ž„ + localStorage)
โ˜ ์˜ํ™” ๊ฒ€์ƒ‰ ์•ฑ (TMDB API ํ™œ์šฉ)

๐ŸŽฏ ๋‹ค์Œ ๋‹จ๊ณ„๋Š”?

DOM API๋ฅผ ์ด์ œ ์ž์œ ๋กญ๊ฒŒ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๊ฒŒ ๋๋‹ค๋ฉด, ๋‹ค์Œ ๋‹จ๊ณ„๋Š” React์ž…๋‹ˆ๋‹ค.
React๋Š” ๋ฐ”๋‹๋ผ JS๋กœ ํ•˜๋˜ DOM ์กฐ์ž‘์„ ํ›จ์”ฌ ์ฒด๊ณ„์ ์ด๊ณ  ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•ด์ค๋‹ˆ๋‹ค.
FrontendDevGuide0006 — React ์ž…๋ฌธ์œผ๋กœ ๊ณ„์† ๋„์ „ํ•˜์„ธ์š”! ๐Ÿš€
๋ฐ˜์‘ํ˜•