← DoniKit 웹 앱으로

JS/TS 코드 스니펫 29종

화면 개발에서 반복되는 JavaScript 코드 조각 모음입니다. jQuery DOM·이벤트, fetch 통신, 배열·객체 가공, 날짜 형태 맞추기, 폼 검증까지 — 레거시(jQuery)와 표준 API를 함께 담았습니다.

웹 앱에서는 빈칸을 채워 완성된 코드를 바로 복사할 수 있습니다.

반복문 (for · for-of · forEach/map) + 객체 순회

반복문

const items = ["a", "b", "c"];

// 1) 기본 for (인덱스 필요할 때)
for (let i = 0; i < items.length; i++) {
  console.log(i, items[i]);
}

// 2) for-of — 값만 필요할 때
for (const item of items) {
  console.log(item);
}

// 3) forEach / map
items.forEach((item, i) => console.log(i, item));
const upper = items.map((s) => s.toUpperCase());

// 객체 순회 (key/value)
const scores = { kim: 90, lee: 80 };
for (const [name, score] of Object.entries(scores)) {
  console.log(name, score);
}

객체 배열 생성 · 채우기 (자바 List<Map> 대응)

컬렉션

// 자바의 List<Map<String,Object>> = JS의 "객체 배열"
const rows = [];
rows.push({ id: 1, name: "홍길동", active: true });
rows.push({ id: 2, name: "김철수", active: false });

// 한 번에 초기화
const rows2 = [
  { id: 1, name: "홍길동" },
  { id: 2, name: "김철수" },
];

// 읽기·가공
for (const r of rows) console.log(r.id, r.name);
const names = rows.map((r) => r.name);       // ["홍길동", "김철수"]
const found = rows.find((r) => r.id === 1);  // { id: 1, ... }

형변환 (문자열 ↔ 숫자, JSON ↔ 객체)

형변환

// 문자열 → 숫자
const a = Number("42");         // 42  (실패 시 NaN)
const b = parseInt("42px", 10); // 42  (앞부분만, 진법 10 명시 권장)
const c = parseFloat("3.14");
const bad = Number.isNaN(Number("abc")); // true

// 숫자/불리언 → 문자열
const s = String(42);
const s2 = (42).toString();

// JSON ↔ 객체
const obj = JSON.parse('{"id":1,"name":"홍길동"}');
const text = JSON.stringify(obj);            // 한 줄
const pretty = JSON.stringify(obj, null, 2); // 들여쓰기 2칸

💡 Number("")는 0, Number("abc")는 NaN. 빈 문자열·공백 입력을 따로 걸러야 할 때가 많다.

Map ↔ 객체 ↔ JSON · 깊은 복사

형변환

// 객체 → Map
const obj = { id: 1, name: "홍길동" };
const map = new Map(Object.entries(obj));

// Map → 객체
const back = Object.fromEntries(map);

// Map은 넣은 순서를 지키고 키에 아무 타입이나 쓸 수 있다
map.set(42, "숫자키");
console.log(map.get("name"), map.size); // 홍길동 3

// Map → JSON (객체를 거쳐야 한다)
const json = JSON.stringify(Object.fromEntries(map));
const restored = new Map(Object.entries(JSON.parse(json)));

// 깊은 복사 — 중첩 객체까지 새로 만든다
const copy = structuredClone(obj);

💡 JSON.stringify(map)은 {}만 뱉는다 — Map은 직렬화 전에 Object.fromEntries로 객체를 만들어야 한다. 이때 숫자 키 42는 문자열 "42"가 된다. structuredClone은 함수·Symbol은 복사하지 못한다.

HTTP GET (fetch · async/await)

HTTP 클라 · 표준 fetch

async function getUser(id) {
  const res = await fetch("https://api.example.com/users?id=" + id, {
    headers: { Accept: "application/json" },
  });
  if (!res.ok) throw new Error("HTTP " + res.status);
  return await res.json();
}

getUser(1).then((user) => console.log(user));

💡 fetch는 4xx/5xx에도 reject하지 않는다 → res.ok(또는 res.status)로 직접 확인해야 한다.

HTTP POST (fetch · JSON 전송)

HTTP 클라 · 표준 fetch

async function createUser(data) {
  const res = await fetch("https://api.example.com/users", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  });
  if (!res.ok) throw new Error("HTTP " + res.status);
  return await res.json();
}

createUser({ name: "홍길동", age: 30 }).then(console.log);

HTTP GET (jQuery $.ajax)

HTTP 클라 · jQuery

$.ajax({
  url: "https://api.example.com/users",
  method: "GET",
  data: { id: 1 },        // 쿼리스트링으로 → ?id=1
  dataType: "json",
  success: function (res) {
    console.log(res);
  },
  error: function (xhr, status, err) {
    console.error(status, err);
  },
});

// 축약형
$.get("https://api.example.com/users", { id: 1 }, function (res) {
  console.log(res);
});

💡 jQuery 로드 필요(<script src="jquery.js">). 신규 코드라면 표준 fetch를 우선 고려.

HTTP POST (jQuery $.ajax · JSON 전송)

HTTP 클라 · jQuery

$.ajax({
  url: "https://api.example.com/users",
  method: "POST",
  contentType: "application/json",  // JSON으로 보낼 때 필수
  data: JSON.stringify({ name: "홍길동", age: 30 }),
  dataType: "json",
  success: function (res) {
    console.log(res);
  },
  error: function (xhr) {
    console.error(xhr.status, xhr.responseText);
  },
});

💡 contentType을 안 주면 폼 형식(application/x-www-form-urlencoded)으로 전송된다.

서버로 GET/POST 받기 (Express)

HTTP 서버 · Express

const express = require("express");
const app = express();
app.use(express.json()); // JSON 바디 파싱 (필수)

// GET /api/users/1?keyword=kim
app.get("/api/users/:id", (req, res) => {
  const id = req.params.id;          // 경로 변수
  const keyword = req.query.keyword; // 쿼리스트링
  res.json({ id, keyword });
});

// POST /api/users  (JSON 바디)
app.post("/api/users", (req, res) => {
  const body = req.body;             // { name, age }
  res.status(201).json({ created: body });
});

app.listen(3000, () => console.log("http://localhost:3000"));

💡 express.json() 미들웨어가 없으면 POST의 req.body가 undefined가 된다.

배열 가공 (filter · map · reduce · find)

컬렉션 가공

const users = [
  { name: "홍길동", age: 30 },
  { name: "김철수", age: 18 },
];

// 걸러서 이름만
const names = users.filter((u) => u.age >= 20).map((u) => u.name);

// 합계 (reduce)
const total = users.reduce((sum, u) => sum + u.age, 0);

// 찾기 · 존재 여부
const found = users.find((u) => u.name === "홍길동");
const hasMinor = users.some((u) => u.age < 20);
const allAdult = users.every((u) => u.age >= 20);

배열 정렬 · 그룹핑

컬렉션 가공

// 숫자 정렬 — 비교 함수 필수! (기본 sort는 문자열 비교)
const nums = [10, 2, 33, 4];
nums.sort((a, b) => a - b);   // [2, 4, 10, 33]

// 필드 기준 (나이 → 이름)
users.sort((a, b) => a.age - b.age || a.name.localeCompare(b.name));

// 그룹핑 → { city: [user, ...] }
const byCity = users.reduce((acc, u) => {
  (acc[u.city] ??= []).push(u);
  return acc;
}, {});

💡 sort는 원본 배열을 바꾼다(in-place). 기본 정렬은 원소를 문자열로 비교하므로 숫자엔 반드시 (a,b)=>a-b.

문자열 다루기 (자르기 · 나누기 · 치환)

문자열

const s = "  Hello, World  ";

s.trim();                 // 앞뒤 공백 제거
s.toLowerCase();
s.includes("World");      // true
s.startsWith("  He");     // true
s.replaceAll("l", "L");   // 모두 치환 (replace는 첫 개만)

// 자르기 · 나누기 · 잇기
const parts = "a,b,c".split(",");  // ["a","b","c"]
const joined = parts.join("-");    // "a-b-c"
const sub = "abcdef".slice(1, 4);  // "bcd"

템플릿 리터럴 · 자리 채우기

문자열

const name = "홍길동", age = 30;

// 템플릿 리터럴(백틱) — 변수 삽입
const msg = `${name}님은 ${age}세`;
const multi = `첫 줄
둘째 줄`;                    // 줄바꿈 그대로 유지

// 자리 채우기 · 반복
"7".padStart(3, "0");       // "007"
"9".padEnd(3, "-");         // "9--"
"=".repeat(20);

💡 작은따옴표가 아니라 백틱(`)이다. ${...} 안에는 식을 넣을 수 있다.

숫자·통화 표시 (천단위 콤마 · 원화 · 퍼센트)

문자열

const n = 1234567.891;

// 천단위 콤마
n.toLocaleString();                                      // "1,234,567.891"
n.toLocaleString("ko-KR", { maximumFractionDigits: 0 }); // "1,234,568"

// 통화 (원화는 소수 없음)
const krw = new Intl.NumberFormat("ko-KR", { style: "currency", currency: "KRW" });
krw.format(n); // "₩1,234,568"

// 퍼센트
new Intl.NumberFormat("ko-KR", { style: "percent", minimumFractionDigits: 1 })
  .format(0.1234); // "12.3%"

// 소수 자리 고정 — toFixed는 "문자열"을 돌려준다
(3.14159).toFixed(2); // "3.14"

// 역방향: 콤마 문자열 → 숫자
Number("1,234,568".replaceAll(",", "")); // 1234568

💡 계산은 숫자로 끝내고 표시 직전에만 포맷 — 콤마 붙은 문자열로 계산하면 NaN. toFixed는 이진 오차 탓에 (2.675).toFixed(2)가 "2.67"이 되는 등 반올림이 어긋날 수 있다.

날짜 다루기 (포맷·더하기·차이)

날짜·시간

const now = new Date();

// 포맷
now.toISOString();                             // "2026-07-07T12:30:00.000Z"
new Intl.DateTimeFormat("ko-KR").format(now);  // "2026. 7. 7."

// 더하기 (밀리초 기반)
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);

// 두 날짜 차이(일)
const d1 = new Date("2026-07-07");
const d2 = new Date("2026-07-14");
const days = (d2 - d1) / (1000 * 60 * 60 * 24); // 7

💡 표준 Date는 포맷·연산이 빈약하다. 복잡해지면 date-fns(MIT) 같은 라이브러리를 고려.

날짜 비교 — 기준 형태로 완성해서 비교 (yyyyMMdd · yyyyMMddHHmmss · Date · 일 단위)

날짜·시간

// 쓰는 법: 비교할 기준 형태를 하나 고르고, 그 영역의 변환으로 값을 완성한 뒤 비교한다.
const pad = (n) => String(n).padStart(2, "0");
const now = new Date();   // 예시 값: 2026-08-02 09:30:00 을 담은 Date 객체

// ══ 1. "yyyyMMdd" 문자열로 완성해서 비교 — 완성형: "20260730" ══════
const s1 = "20260730143025".slice(0, 8);                                        // "20260730143025" → "20260730"
const s2 = "" + now.getFullYear() + pad(now.getMonth() + 1) + pad(now.getDate()); // Date → "20260802"

s1 === s2;   // "20260730" === "20260802" → false
s1 > s2;     // 이후인가 (같은 자릿수 문자열은 사전순 = 시간순)

// ══ 2. "yyyyMMddHHmmss" 문자열로 완성해서 비교 — 완성형: "20260730143025" ══
const t1 = "20260730" + "000000";                                          // "20260730" → "20260730000000" (자정 붙이기)
const t2 = s2 + pad(now.getHours()) + pad(now.getMinutes()) + pad(now.getSeconds()); // Date → "20260802093000"

t1 === t2;   // 초까지 같은가
t1 < t2;     // 이전인가

// ══ 3. Date(시각 포함)로 완성해서 비교 — 완성형: Date(2026-07-30 14:30:25) ══
// 압축 문자열은 new Date()가 못 읽는다 — 잘라서 조립 (월은 0부터라 -1)
const toDate = (s) => new Date(
  +s.slice(0, 4), +s.slice(4, 6) - 1, +s.slice(6, 8),
  +s.slice(8, 10) || 0, +s.slice(10, 12) || 0, +s.slice(12, 14) || 0,
);                                              // "20260730143025" → Date(2026-07-30 14:30:25)
toDate("20260730143025").getTime() === now.getTime();  // ==는 객체 비교라 항상 false — 꼭 getTime()
toDate("20260730143025").getTime() > now.getTime();    // getTime() = epoch 밀리초 숫자로 비교

// ══ 4. 일 단위 Date로 완성해서 비교 — 완성형: Date(2026-07-30 00:00:00) ══
const day1 = toDate("20260730");                         // "20260730" → Date(2026-07-30 00:00:00)
const day2 = new Date(now); day2.setHours(0, 0, 0, 0); // Date(…09:30:00) → Date(…00:00:00) 시각 지우기
day1.getTime() === day2.getTime();                     // 같은 날인가

💡 서버(Java·DB)로 보내 비교할 값은 문자열(1·2번) 그대로 보낸다 — Date를 JSON에 실으면 UTC ISO("…T15:00:00.000Z")로 바뀌어 날짜가 하루 밀릴 수 있다. new Date("2026-07-30")처럼 구분자 있는 문자열도 UTC 자정으로 읽히는 함정 — 압축 문자열은 3번처럼 수동 파싱. 비교는 같은 자릿수끼리만.

try-catch · 비동기 에러 · 커스텀 에러

예외처리

// try - catch - finally
try {
  const obj = JSON.parse(input);
} catch (e) {
  console.error("파싱 실패:", e.message);
} finally {
  // 항상 실행
}

// async/await 에러는 try/catch로 감싼다
async function load() {
  try {
    const res = await fetch("/api");
    return await res.json();
  } catch (e) {
    console.error(e);
  }
}

// Promise 체인은 .catch
fetch("/api").then((r) => r.json()).catch((e) => console.error(e));

// 에러 던지기
throw new Error("id는 필수입니다");

Vitest / Jest (describe·expect · 비동기 · 모킹)

테스트 · Vitest / Jest

import { describe, it, expect, vi, beforeEach } from "vitest";
// Jest면 import 없이 전역 describe/it/expect 사용, vi 대신 jest

describe("계산기", () => {
  beforeEach(() => {
    // 각 테스트 전 초기화
  });

  it("두 수를 더한다", () => {
    expect(add(2, 3)).toBe(5);          // 원시값 비교
    expect({ a: 1 }).toEqual({ a: 1 }); // 객체·배열은 toEqual
    expect([1, 2, 3]).toContain(2);
  });

  it("비동기 — await로 끝까지 기다린다", async () => {
    await expect(fetchUser(1)).resolves.toMatchObject({ name: "홍길동" });
    await expect(fetchUser(-1)).rejects.toThrow("없는 사용자");
  });

  it("모킹 — 함수를 가짜로 바꾼다", () => {
    const send = vi.fn().mockReturnValue(true); // jest.fn()도 동일
    notify(send);
    expect(send).toHaveBeenCalledTimes(1);
    expect(send).toHaveBeenCalledWith("완료");
  });
});

💡 객체·배열 비교는 toBe가 아니라 toEqual — toBe는 같은 참조인지만 본다. 비동기 검증에서 await를 빠뜨리면 검증 전에 테스트가 끝나 "항상 통과"하는 가짜 초록불이 된다.

DB 쿼리 (node-postgres · 파라미터 바인딩)

DB 접근 · node-postgres (pg)

const { Pool } = require("pg");
const pool = new Pool();

// 자리표시 $1, $2 + 배열 — 문자열 연결 금지(SQL 인젝션)
const sql = "SELECT id, name FROM users WHERE age >= $1";
const { rows } = await pool.query(sql, [20]);
rows.forEach((r) => console.log(r.id, r.name));

// INSERT + 생성된 id 반환
const insert = "INSERT INTO users(name, age) VALUES($1, $2) RETURNING id";
const res = await pool.query(insert, ["홍길동", 30]);
console.log(res.rows[0].id);

💡 mysql2 등 다른 드라이버는 자리표시가 ? 다. 공통 원칙은 "값은 항상 파라미터로, 문자열로 이어붙이지 않는다".

ORM (Prisma · findMany/create/update)

DB 접근 · Prisma

import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();

// 조회
const users = await prisma.user.findMany({
  where: { age: { gte: 20 } },
  orderBy: { name: "asc" },
});
const one = await prisma.user.findUnique({ where: { id: 1 } });

// 생성 · 수정 · 삭제
await prisma.user.create({ data: { name: "홍길동", age: 30 } });
await prisma.user.update({ where: { id: 1 }, data: { age: 31 } });
await prisma.user.delete({ where: { id: 1 } });

💡 ORM은 바인딩·이스케이프를 알아서 처리한다. 스키마(prisma/schema.prisma) 정의 후 generate가 선행되어야 한다.

요소 조작 (val · text · html · show/hide · class)

DOM·이벤트 · jQuery

// 값 읽기/쓰기 — input은 val(), 일반 요소는 text()/html()
const name = $("#userName").val();  // 읽기
$("#userName").val("홍길동");        // 쓰기
$("#msg").text("저장되었습니다");     // 텍스트 (이스케이프됨 — 안전)
$("#box").html("<b>굵게</b>");       // HTML로 해석 — 주의

// 보이기/숨기기/토글
$("#panel").show();
$("#panel").hide();
$("#panel").toggle();

// 클래스
$("#row1").addClass("active");
$("#row1").removeClass("active");
$("#row1").toggleClass("active");
const on = $("#row1").hasClass("active"); // true/false

// 속성·데이터
$("#link").attr("href", "/next");
const userId = $("#row1").data("userId"); // data-user-id="7" → 7

💡 text()는 이스케이프되지만 html()은 태그가 해석된다 — 사용자 입력을 html()에 넣지 말 것(XSS). data-user-id처럼 하이픈 속성은 data("userId") 카멜로 읽는다.

이벤트 바인딩 · 위임 (동적 요소)

DOM·이벤트 · jQuery

// 문서 준비 후 실행 — 이벤트 바인딩은 이 안에서
$(function () {
  // 직접 바인딩 — "지금 존재하는" 요소에만 걸린다
  $("#saveBtn").on("click", function () {
    console.log("저장 클릭");
  });

  // ★ 위임 — 나중에 추가된 요소에도 걸린다 (ajax로 그린 행/버튼 필수)
  $(document).on("click", ".del-btn", function () {
    $(this).closest("tr").remove();
  });

  // change · keyup
  $("#deptSelect").on("change", function () {
    console.log("선택값:", $(this).val());
  });
  $("#keyword").on("keyup", function (e) {
    if (e.key === "Enter") console.log("검색:", $(this).val());
  });
});

💡 $("셀렉터").on()은 바인딩 시점에 존재하는 요소에만 걸린다 — 동적으로 추가한 버튼이 "클릭이 안 먹는" 원인 1위. 동적 요소는 $(document).on("click", "셀렉터", ...) 위임으로.

테이블 행 동적 추가 · 삭제 (조회 결과 그리기)

DOM·이벤트 · jQuery

// 조회 결과(객체 배열)를 tbody에 그린다
function renderRows(rows) {
  const $tbody = $("#resultTable tbody");
  $tbody.empty(); // 기존 행 비우기

  if (rows.length === 0) {
    $tbody.append('<tr><td colspan="3">결과 없음</td></tr>');
    return;
  }
  $.each(rows, function (i, row) {
    const $tr = $("<tr>");
    $tr.append($("<td>").text(row.id));
    $tr.append($("<td>").text(row.name)); // 값은 text()로 — 안전
    $tr.append($("<td>").html('<button type="button" class="del-btn">삭제</button>'));
    $tbody.append($tr);
  });
}

// 행 삭제 — 동적 행이므로 위임 바인딩
$(document).on("click", ".del-btn", function () {
  $(this).closest("tr").remove();
});

💡 "<td>" + row.name + "</td>" 문자열 조립은 XSS·따옴표 깨짐의 지름길 — 값은 $("<td>").text(값)으로 넣는다.

폼 다루기 (serialize · 체크박스 전체선택 · select 채우기)

DOM·이벤트 · jQuery

// 폼 전체 값 모으기
const qs = $("#searchForm").serialize();       // "name=kim&age=30"
const arr = $("#searchForm").serializeArray(); // [{ name, value }, ...]

// 체크박스 전체선택/해제 + 선택된 값 모으기
$("#checkAll").on("change", function () {
  $(".row-check").prop("checked", $(this).is(":checked"));
});
const checked = $(".row-check:checked").map(function () {
  return $(this).val();
}).get(); // ["1", "3"]

// select 옵션 동적 채우기 + 선택하기
const list = [{ code: "D01", name: "영업" }, { code: "D02", name: "개발" }];
const $sel = $("#deptSelect").empty();
$sel.append($("<option>").val("").text("전체"));
$.each(list, function (i, d) {
  $sel.append($("<option>").val(d.code).text(d.name));
});
$sel.val("D01"); // 값으로 선택

💡 체크 상태는 attr이 아니라 prop("checked", ...) — attr은 초기 HTML 속성이라 두 번째부터 안 먹는다(단골 함정). serialize는 disabled 필드·체크 안 된 체크박스를 제외한다.

DOM 조작 · 이벤트 위임 (jQuery 없이)

DOM·이벤트 · 표준 DOM

// 선택
const input = document.querySelector("#userName");
const rows = document.querySelectorAll("#list tr"); // NodeList

// 값·텍스트·클래스
input.value = "홍길동";
document.querySelector("#msg").textContent = "저장되었습니다";
document.querySelector("#panel").classList.toggle("hidden");

// 이벤트
document.querySelector("#saveBtn").addEventListener("click", () => {
  console.log("저장 클릭");
});

// ★ 위임 — 동적 요소는 부모에서 받아 closest로 판별
document.querySelector("#list").addEventListener("click", (e) => {
  const btn = e.target.closest(".del-btn");
  if (!btn) return;
  btn.closest("tr").remove();
});

💡 querySelectorAll의 NodeList는 배열이 아니다 — map이 필요하면 [...rows]로 펼친다. textContent는 안전, innerHTML은 XSS 주의(jQuery text()/html() 관계와 동일).

디바운스 · 스로틀 (연타 입력 제어)

DOM·이벤트

// 디바운스: 입력이 "멈춘 뒤" 1번만 실행 — 검색 자동완성
function debounce(fn, ms) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), ms);
  };
}

// 스로틀: 연속 발생 중에도 ms당 최대 1번 — 스크롤·리사이즈
function throttle(fn, ms) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last < ms) return;
    last = now;
    fn.apply(this, args);
  };
}

const search = debounce((word) => console.log("검색:", word), 300);
document.querySelector("#keyword").addEventListener("input", (e) => {
  search(e.target.value);
});

💡 디바운스 = 마지막 한 번(입력이 끝나면), 스로틀 = 주기당 한 번(연속 중에도 간간이). 검색창은 디바운스, 스크롤 위치 계산은 스로틀.

파일 다운로드 만들기 (Blob · CSV 내보내기)

파일·저장

// 화면의 데이터를 파일로 내려주기 — 서버 없이 브라우저에서 생성
function downloadFile(filename, text, mime) {
  const blob = new Blob([text], { type: mime });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  a.click();
  URL.revokeObjectURL(url); // 메모리 해제
}

// CSV 내보내기 — 엑셀 한글 깨짐 방지: 맨 앞에 BOM
const rows = [["id", "이름"], [1, "홍길동"], [2, "김철수"]];
const csv = rows.map((r) => r.join(",")).join("\n");
downloadFile("users.csv", "\uFEFF" + csv, "text/csv;charset=utf-8");

💡 엑셀에서 한글이 깨지면 CSV 맨 앞에 BOM(\uFEFF)을 붙인다. 쉼표·줄바꿈이 든 값은 큰따옴표로 감싸고, 값 안의 큰따옴표는 두 번 겹쳐 쓴다.

파일 업로드 (FormData · multipart)

파일·저장 · 표준 fetch

// <input type="file" id="fileInput"> 의 파일을 업로드
async function uploadFile() {
  const file = document.querySelector("#fileInput").files[0];
  if (!file) {
    alert("파일을 선택하세요");
    return;
  }
  const fd = new FormData();
  fd.append("file", file);
  fd.append("memo", "정산 자료"); // 일반 필드도 함께 전송 가능

  const res = await fetch("/api/upload", { method: "POST", body: fd });
  if (!res.ok) throw new Error("HTTP " + res.status);
  return await res.json();
}

💡 Content-Type을 직접 지정하지 말 것 — 브라우저가 boundary까지 자동으로 붙인다. jQuery $.ajax로 보낼 땐 processData: false, contentType: false 두 개가 필수.

localStorage 저장 · 읽기 (JSON · 기본값)

파일·저장

// 객체는 JSON 문자열로 바꿔 저장한다 (localStorage는 문자열만)
const settings = { theme: "dark", pageSize: 20 };
localStorage.setItem("appSettings", JSON.stringify(settings));

// 읽기 — 없거나 깨졌으면 기본값
function loadSettings() {
  const fallback = { theme: "light", pageSize: 10 };
  try {
    const raw = localStorage.getItem("appSettings");
    return raw ? JSON.parse(raw) : fallback;
  } catch (e) {
    return fallback; // 깨진 JSON 방어
  }
}

// 삭제
localStorage.removeItem("appSettings");
localStorage.clear(); // 전부 삭제 — 신중히

💡 객체를 그대로 넣으면 "[object Object]"가 저장된다 — 반드시 JSON.stringify/parse를 거친다. 탭을 닫아도 남는 영구 저장이고, 세션 동안만 필요하면 sessionStorage.