DEV Community

Cover image for HTML 테이블에서 SQL INSERT 문까지 원클릭으로
circobit
circobit

Posted on

HTML 테이블에서 SQL INSERT 문까지 원클릭으로

웹사이트에서 데이터를 찾았습니다. 데이터베이스에 넣어야 합니다. 보통 워크플로우는: CSV로 내보내기, SQL 도구에 임포트, 타입 불일치 처리, 인코딩 문제 수정.

HTML 테이블에서 바로 유효한 SQL을 생성할 수 있다면 어떨까요?

이 가이드에서는 모든 웹 테이블에서 CREATE TABLEINSERT INTO 문을 생성하는 방법을 타입 추론, 식별자 정제, 적절한 이스케이프 처리와 함께 보여줍니다. HTML Table Exporter에서 사용하는 접근법입니다.

최종 결과

이런 테이블에서:

| Product Name    | Price  | In Stock |
|-----------------|--------|----------|
| Widget Pro      | 29.99  | Yes      |
| Gadget Basic    | 14.50  | No       |
Enter fullscreen mode Exit fullscreen mode

이렇게 생성:

-- Exported from HTML Table Exporter PRO

CREATE TABLE products (
  product_name TEXT,
  price REAL,
  in_stock TEXT
);

INSERT INTO products (product_name, price, in_stock) VALUES
  ('Widget Pro', 29.99, 'Yes'),
  ('Gadget Basic', 14.50, 'No');
Enter fullscreen mode Exit fullscreen mode

단계별로 구축해봅시다.

1단계: 테이블 매트릭스 추출

먼저 HTML 테이블에서 원시 데이터를 추출합니다:

function extractTableMatrix(table) {
  const rows = Array.from(table.rows);
  const grid = [];

  rows.forEach((rowEl, rowIndex) => {
    if (!grid[rowIndex]) grid[rowIndex] = [];
    let colIndex = 0;

    Array.from(rowEl.cells).forEach(cell => {
      while (grid[rowIndex][colIndex] !== undefined) colIndex++;

      const text = cell.textContent.trim();
      const rowSpan = parseInt(cell.rowSpan) || 1;
      const colSpan = parseInt(cell.colSpan) || 1;

      for (let r = 0; r < rowSpan; r++) {
        if (!grid[rowIndex + r]) grid[rowIndex + r] = [];
        for (let c = 0; c < colSpan; c++) {
          grid[rowIndex + r][colIndex + c] = text;
        }
      }
      colIndex += colSpan;
    });
  });

  return grid;
}
Enter fullscreen mode Exit fullscreen mode

출력: rows[0]이 헤더이고 rows[1+]이 데이터인 2D 배열.

2단계: 컬럼명 정제

SQL 식별자에는 엄격한 규칙이 있습니다. "Product Name"이나 "Price ($)" 같은 헤더는 정제가 필요합니다:

function sanitizeSqlIdentifier(header, fallbackIndex) {
  let id = (header || "").toString().trim();

  if (!id) {
    return `col_${fallbackIndex + 1}`;
  }

  // 유니코드 정규화 (악센트 제거)
  id = id.normalize("NFD").replace(/[\u0300-\u036f]/g, "");

  // 소문자 snake_case 변환
  id = id
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "");

  // 숫자로 시작하지 않도록
  if (/^\d/.test(id)) {
    id = `col_${id}`;
  }

  // 정제 후 비어있으면 폴백
  if (!id) {
    return `col_${fallbackIndex + 1}`;
  }

  return id;
}

// 예시:
// "Product Name" → "product_name"
// "Price ($)"    → "price"
// "2024 Revenue" → "col_2024_revenue"
// ""             → "col_1"
Enter fullscreen mode Exit fullscreen mode

3단계: 중복 컬럼명 처리

테이블에 중복 헤더가 있을 수 있습니다. SQL은 고유한 컬럼명을 요구합니다:

function makeUniqueColumnNames(headers) {
  const used = new Set();

  return headers.map((header, idx) => {
    let name = sanitizeSqlIdentifier(header, idx);
    let candidate = name;
    let counter = 1;

    while (used.has(candidate)) {
      candidate = `${name}_${counter}`;
      counter++;
    }

    used.add(candidate);
    return candidate;
  });
}

// ["Name", "Name", "Value"] → ["name", "name_1", "value"]
Enter fullscreen mode Exit fullscreen mode

4단계: SQL 타입 추론

데이터를 분석하여 적절한 SQL 타입 결정:

function inferSqlColumnTypes(rows, headerRowIndex = 0) {
  const headerRow = rows[headerRowIndex] || [];
  const dataRows = rows.slice(headerRowIndex + 1);
  const types = new Array(headerRow.length).fill("TEXT");

  for (let col = 0; col < headerRow.length; col++) {
    // 컬럼당 최대 50개 값 샘플링
    const values = [];
    for (let r = 0; r < Math.min(dataRows.length, 50); r++) {
      const cell = dataRows[r][col];
      const v = cell != null ? String(cell).trim() : "";
      if (v !== "") values.push(v);
    }

    if (values.length === 0) {
      types[col] = "TEXT";
      continue;
    }

    let allInt = true;
    let allNumeric = true;

    for (const v of values) {
      if (!/^[-+]?\d+$/.test(v)) {
        allInt = false;
      }
      if (!/^[-+]?\d+([.,]\d+)?$/.test(v)) {
        allNumeric = false;
      }
    }

    if (allInt) {
      types[col] = "INTEGER";
    } else if (allNumeric) {
      types[col] = "REAL";
    } else {
      types[col] = "TEXT";
    }
  }

  return types;
}
Enter fullscreen mode Exit fullscreen mode

타입 우선순위: INTEGER > REAL > TEXT

5단계: 값 올바르게 이스케이프

SQL 인젝션은 보안 문제만이 아닙니다—데이터를 손상시킵니다. 적절한 이스케이프:

function sqlEscapeValue(raw, type) {
  // NULL 처리
  if (raw == null) return "NULL";

  const v = String(raw).trim();
  if (v === "") return "NULL";

  // 숫자 타입
  if (type === "INTEGER" || type === "REAL") {
    const normalized = v.replace(",", ".");
    const num = Number(normalized);

    if (!Number.isNaN(num) && Number.isFinite(num)) {
      return normalized;
    }
  }

  // TEXT: 작은따옴표를 두 배로 이스케이프
  const escaped = v.replace(/'/g, "''");
  return `'${escaped}'`;
}

// 예시:
// sqlEscapeValue("Hello", "TEXT")       → "'Hello'"
// sqlEscapeValue("It's good", "TEXT")   → "'It''s good'"
// sqlEscapeValue("29.99", "REAL")       → "29.99"
// sqlEscapeValue("", "TEXT")            → "NULL"
// sqlEscapeValue(null, "TEXT")          → "NULL"
Enter fullscreen mode Exit fullscreen mode

6단계: SQL 생성

모든 것을 합치기:

function tableToSql(tableInfo) {
  const rows = tableInfo.rows || [];
  if (rows.length < 2) return "";

  const headerRowIndex = tableInfo.headerRowIndex || 0;
  const headerRow = rows[headerRowIndex];
  const dataRows = rows.slice(headerRowIndex + 1);

  if (!headerRow || headerRow.length === 0) return "";

  const columnNames = makeUniqueColumnNames(headerRow);
  const types = inferSqlColumnTypes(rows, headerRowIndex);

  const rawTableName = tableInfo.name || tableInfo.slug || "table";
  const tableName = sanitizeSqlIdentifier(rawTableName, 0) || "table_export";

  // CREATE TABLE 문
  const createLines = columnNames.map((col, i) => 
    `  ${col} ${types[i] || "TEXT"}`
  );
  const createStmt = `CREATE TABLE ${tableName} (\n${createLines.join(",\n")}\n);`;

  // INSERT 문
  const insertHeader = `INSERT INTO ${tableName} (${columnNames.join(", ")}) VALUES`;

  const valueLines = dataRows.map(row => {
    const values = columnNames.map((_, i) => {
      const cell = row[i];
      const type = types[i] || "TEXT";
      return sqlEscapeValue(cell, type);
    });
    return `  (${values.join(", ")})`;
  });

  let sql = `-- Exported from HTML Table Exporter\n\n${createStmt}\n\n`;

  if (valueLines.length > 0) {
    sql += `${insertHeader}\n${valueLines.join(",\n")};\n`;
  }

  return sql;
}
Enter fullscreen mode Exit fullscreen mode

엣지 케이스 처리

NULL 값

많은 웹 테이블이 누락 데이터를 다르게 표현합니다:

const NULL_PATTERNS = ["N/A", "n/a", "-", "--", "null", "none", "."];

function sqlEscapeValue(raw, type, nullPatterns = NULL_PATTERNS) {
  if (raw == null) return "NULL";

  const v = String(raw).trim();
  if (v === "" || nullPatterns.includes(v.toLowerCase())) {
    return "NULL";
  }

  // ... 나머지 함수
}
Enter fullscreen mode Exit fullscreen mode

대규모 데이터셋

수천 행이 있는 테이블에서는 INSERT를 배치 처리:

function tableToSqlBatched(tableInfo, batchSize = 1000) {
  // ... 같은 설정 ...

  const batches = [];
  for (let i = 0; i < valueLines.length; i += batchSize) {
    const batch = valueLines.slice(i, i + batchSize);
    batches.push(`${insertHeader}\n${batch.join(",\n")};`);
  }

  return `${createStmt}\n\n${batches.join("\n\n")}`;
}
Enter fullscreen mode Exit fullscreen mode

데이터베이스별 구문

SQLite, PostgreSQL, MySQL에는 약간의 구문 차이가 있습니다:

function tableToSql(tableInfo, dialect = "sqlite") {
  // ... 같은 컬럼/타입 로직 ...

  const typeMap = {
    sqlite: { INTEGER: "INTEGER", REAL: "REAL", TEXT: "TEXT" },
    postgresql: { INTEGER: "INTEGER", REAL: "NUMERIC", TEXT: "TEXT" },
    mysql: { INTEGER: "INT", REAL: "DECIMAL(10,2)", TEXT: "VARCHAR(255)" }
  };

  const dialectTypes = typeMap[dialect] || typeMap.sqlite;

  const createLines = columnNames.map((col, i) => 
    `  ${col} ${dialectTypes[types[i]] || dialectTypes.TEXT}`
  );

  // ... 나머지 함수 ...
}
Enter fullscreen mode Exit fullscreen mode

사용 시기

적합한 경우:

  • SQLite/PostgreSQL에 빠른 데이터 임포트
  • 테스트 데이터베이스 시딩
  • 일회성 데이터 마이그레이션
  • 예시를 통한 SQL 학습

부적합한 경우:

  • 대규모 ETL (전용 도구 사용)
  • 복잡한 스키마 (외래 키, 제약 조건)
  • 바이너리 데이터

프로덕션 데이터 파이프라인에는 검증 기능이 있는 적절한 ETL 도구가 낫습니다. "이 테이블을 지금 당장 데이터베이스에 넣어야 해"라면 직접 SQL 생성이 빠릅니다.

코드 없이 사용하기

직접 구축하고 싶지 않다면 HTML Table Exporter PRO가 원클릭으로 SQL을 생성합니다. 무료 버전은 CSV/JSON/Excel로 내보내기 가능하고, PRO는 SQL, NDJSON, 데이터 정제를 추가합니다.

다양한 내보내기 접근법의 비교는 HTML 테이블을 CSV로 내보내는 방법 가이드를 참고하세요.

gauchogrid.com/ko/html-table-exporter에서 자세히 알아보거나 Chrome 웹 스토어에서 사용해보세요.


어떤 데이터베이스에 웹 데이터를 임포트하고 계신가요? 사용 사례가 궁금합니다.

Top comments (0)