CSV JSON 파일 자동화 Node.js 읽기 검증 변환 저장

··설명 약 9분·작성: 해비·블로그 소개

Node.js로 CSV와 JSON을 처리할 때 파싱 오류와 행 검증 오류는 어떻게 나누나요?

파일 자동화는 읽기, 형식 해석, 행별 검사, 결과 저장을 나누면 실패 원인을 찾기 쉽습니다. JSON 문법 오류와 빈 제목 한 행의 오류는 다르게 처리하고 원본을 보존합니다.

적용 조건·범위: CSV의 인용부호·쉼표·줄바꿈을 일반 split으로 처리하지 않습니다. 지원하지 않는 인용 형식은 거절합니다. UTF-8과 헤더·열 수를 확인하고 파싱 실패와 행 검증 실패를 분리합니다.

본문 근거: CSV 형식 참고 RFC 4180 · Node.js 24 파일 API

두 파일로 JSON 읽기·검증·저장 확인

새 연습 폴더에 input.json과 json-io-demo.mjs 두 파일을 만듭니다. Node.js 24에서 실행하며 외부 계정이나 앞선 프로젝트는 필요 없습니다. CSV의 일반적인 따옴표 처리는 별도의 parser가 필요하므로 이 최소 실습은 JSON 파일만 다룹니다.

input.json

[{"id":"a","title":"  문서 읽기  "},{"id":"b","title":"   "}]

json-io-demo.mjs 전체 코드

import { readFile, writeFile } from 'node:fs/promises';

const input = JSON.parse(await readFile('input.json', 'utf8'));
if (!Array.isArray(input)) throw new Error('배열 입력이 필요합니다.');
const accepted = [];
const rejected = [];
for (const [index, row] of input.entries()) {
  if (!row || typeof row.title !== 'string' || !row.title.trim()) {
    rejected.push({ row: index + 1, reason: '빈 제목' });
    continue;
  }
  accepted.push({ ...row, title: row.title.trim() });
}
await writeFile('result.json', JSON.stringify({ accepted, rejected }, null, 2),
  { encoding: 'utf8', flag: 'wx' });
console.log(`정상 ${accepted.length}, 실패 ${rejected.length}`);
if (accepted.length > 0) console.log(accepted[0].title);

저장한 폴더에서 node json-io-demo.mjs로 실행합니다. 예상 출력은 다음과 같습니다.

정상 1, 실패 1
문서 읽기

직접 바꿔 보기: input.json의 두 번째 제목을 채운 뒤 별도 새 폴더에서 실행해 정상 2·실패 0인지 확인합니다. result.json은 wx 옵션으로 기존 파일 덮어쓰기를 거절합니다. 같은 폴더에서 재실행하면 EEXIST가 발생하므로 결과를 보존하고 새 연습 폴더를 사용하세요. 빈 배열은 정상 0·실패 0, 모든 제목이 비어 있으면 정상 0·실패 입력 수가 나오며 result.json에 결과가 저장됩니다. 정상 항목이 없을 때는 첫 제목을 출력하지 않습니다. JSON의 닫는 괄호를 제거하면 행 검사 전에 파싱 오류가 발생하는지도 구분합니다.

핵심 원리

파일 자동화는 읽기보다 검증이 중요합니다. CSV나 JSON을 읽었다고 곧바로 저장하지 않고, 인코딩·필수 필드·타입·중복을 확인한 뒤 정상 데이터와 실패 데이터를 분리합니다.

자동화는 한 번 성공하는 스크립트와 반복 운영 가능한 파이프라인이 다릅니다. 다음 회차로 갈수록 입력과 결과뿐 아니라 실패·재시도·중복 실행·감사 기록까지 관리합니다.

독립 실습: 파싱과 행 검증 나누기

JSON 문법이 깨졌으면 파일 전체를 해석할 수 없는 파싱 오류입니다. JSON 배열은 읽었지만 한 행의 title이 비어 있으면 해당 행의 검증 오류입니다. 두 오류를 같은 catch로 숨기지 않습니다.

입력 해석 결과 처리
유효한 JSON 배열 행별 검사 가능 정상/실패를 분리
닫는 대괄호 누락 JSON 문법 오류 원본 보존 후 입력 수정
빈 title 한 행 스키마 오류 행 번호와 이유 기록
따옴표 안 쉼표가 있는 CSV 단순 split으로 해석 불가 지원 parser 사용 또는 명시 거부

CSV는 따옴표 안에 쉼표나 줄바꿈이 들어갈 수 있습니다. 이 과정의 무의존성 예제는 통제된 단순 CSV만 받으며 일반 CSV parser라고 부르지 않습니다. 따옴표를 무시한 채 잘못된 열로 저장하는 것보다 지원하지 않는 형식을 거절하는 편이 정확합니다. CSV 형식 참고 RFC 4180

파일 입출력은 UTF-8을 명시하고 입력 파일을 그대로 보존합니다. writeFile은 같은 경로에 쓰면 기존 내용을 대체할 수 있으므로 출력 경로를 분리하고 쓰기 완료를 await로 확인합니다. Node.js 24 파일 API

자주 발생하는 오류와 해결

CSV의 인용부호·쉼표·줄바꿈을 일반 split으로 처리하지 않습니다. 지원하지 않는 인용 형식은 거절합니다. UTF-8과 헤더·열 수를 확인하고 파싱 실패와 행 검증 실패를 분리합니다.

학습 완료 체크리스트

  • JSON 문법 오류와 읽은 배열의 행 검증 오류를 구분했습니다.
  • 정상·거절 행 수와 result.json의 accepted·rejected를 대조했습니다.
  • 입력이 빈 배열이거나 모두 거절되어도 처리 결과가 저장되는지 확인했습니다.
  • 원본과 결과 경로를 구분하고 기존 결과에는 EEXIST로 덮어쓰기를 거절함을 확인했습니다.
선택 실습: 같은 프로젝트로 연습하기

TaskFlow Ops Automation은 작업 처리 상태와 실패·재시도를 관리하는 자동화 예제입니다. 이 선택 실습에서는 처리 흐름을 이어서 만듭니다.

이번 주제: CSV JSON 파일 자동화 Node.js 읽기 검증 변환 저장.

선택: 연재 프로젝트와 전체 자료

연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.

TaskFlow Ops Automation 누적 적용

프로젝트에서 추가로 확인할 항목

  • 통제된 CSV의 정상·실패 행 수를 대조합니다.
  • 지원하지 않는 CSV 인용 형식이 거절되는지 확인합니다.

이어서 시작하기: 작은 자동화 실습 입력 처리 결과와 중복 방지의 완료 상태를 사용합니다. 중간 합류용 시작 ZIP과 이번 완료 ZIP은 아래 기존 다운로드 안내에서 구분해 선택합니다.

시작 체크포인트: automation-step-02-first-pipeline. 이전 회차의 로컬 실습을 이어가거나 이번 회차의 완성 ZIP을 새 폴더에 풀어 시작할 수 있습니다.

현재 파일 구조는 아래 전체 코드의 경로 목록과 같습니다. 생성된 output·logs는 실행 결과이며 배포 소스가 아닙니다. 원본 input 파일을 덮어쓰지 않습니다.

이번 추가 파일: automation/input/tasks.csv, automation/input/tasks.json, src/automation/batch.mjs, src/automation/read-csv.mjs, src/automation/read-json.mjs, tests/files.test.mjs.

이번 변경 파일: README.md.

JSON 배열·통제된 CSV 리더와 batch 진입점을 추가합니다. 기존 normalizeTask와 run을 재사용하고 정상·거절·중복 수를 같은 계약으로 기록합니다.

Before는 이전 체크포인트까지의 기능만 있는 상태입니다. 적용은 위 파일을 해당 경로에 저장하고 README 순서대로 실행하는 단계이며, After는 아래 기대 결과와 실패 경로를 설명할 수 있는 상태입니다. 이미 수정한 파일이 있다면 새 ZIP으로 덮어쓰지 말고 별도 폴더에서 비교합니다.

전체 파일·코드 보기

실습 프로젝트 전체 코드

이 체크포인트는 Node.js 24 이상의 로컬 비식별 실습입니다. 외부 전송과 실제 예약 실행은 포함하지 않습니다.

automation/input/task.json

{
  "sourceId": "sample-1",
  "title": "  주간 작업 점검  ",
  "status": "todo",
  "dueAt": null
}

automation/input/tasks.csv

sourceId,title,status,dueAt
sample-1, 주간 점검 ,todo,
sample-2,결과 검토,doing,2026-09-20T00:00:00Z
sample-3,,todo,
sample-1, 주간 점검 ,todo,

automation/input/tasks.json

[
  { "sourceId": "sample-1", "title": " 주간 점검 ", "status": "todo", "dueAt": null },
  { "sourceId": "sample-2", "title": "결과 검토", "status": "doing", "dueAt": "2026-09-20T00:00:00Z" },
  { "sourceId": "sample-3", "title": "", "status": "todo", "dueAt": null },
  { "sourceId": "sample-1", "title": " 주간 점검 ", "status": "todo", "dueAt": null }
]

docs/automation-decision.md

# TaskFlow Ops Automation 도구 선택

입력은 개인정보가 없는 Task JSON입니다. sourceId로 원본을 연결하고 제목을 검증·정규화한 후 로컬 결과와 실행 기록을 저장합니다.

| 방식 | 선택 기준 | 운영 책임 |
| --- | --- | --- |
| Zapier / Make | 제공 커넥터와 시각적 연결이 업무에 맞음 | 권한·비용·실패 이력·재실행 확인 |
| n8n | 시각적 흐름과 코드 조합이 필요함 | 배포 방식별 업데이트·보안·상태 저장 책임 확인 |
| Node.js | 파일과 오류 처리를 직접 제어하며 학습함 | 테스트·배포·상태·복구를 직접 설계 |

이 과정은 Node.js 24 기본 모듈을 선택합니다. npm install은 필요하지 않습니다. 실제 외부 계정·토큰·개인정보를 사용하지 않습니다. API와 Webhook은 127.0.0.1 모의 서버, 예약은 수동 시뮬레이션만 사용합니다.

실패 시 입력 형식과 실행 결과를 먼저 확인합니다. 외부 전송·업로드·알림은 사람이 범위와 권한을 검토한 별도 작업입니다. 2편의 재실행은 별도 결과를 만드므로 멱등성 완성이 아닙니다. 9편에서 파일 상태와 결과를 확인하며 재개합니다.

package.json

{"name":"taskflow-ops-automation","private":true,"type":"module","engines":{"node":">=24"},"scripts":{"test":"node --test","start":"node src/automation/run.mjs"}}

README.md

# TaskFlow Ops Automation · 03 CSV·JSON 배치

Node.js 24 이상, 설치할 패키지 없음. ZIP을 푼 폴더에서 실행합니다.

    node --test
    node src/automation/run.mjs
    node src/automation/run.mjs

정상 결과: 각 실행 successCount=1, failedCount=0. automation/output/실행ID/valid.json과 rejected.json, automation/logs/실행ID.json이 생성됩니다. 두 번 실행하면 서로 다른 실행ID 폴더가 생깁니다. 이 단계는 실행 내 sourceId 중복만 거르며 영속 멱등성은 9편에서 구현합니다.

입력: automation/input/task.json. sourceId는 1~80자 영문·숫자·밑줄·하이픈, title은 공백 제거 후 비어 있지 않은 200자 이하 문자열입니다. status는 todo/doing/done(기본 todo), dueAt은 null(기본) 또는 UTC ISO 날짜입니다. 예: 2026-09-21T00:00:00Z. 원본을 덮어쓰지 않습니다.

검증 실패는 rejected.json에 row/sourceId/field/reason/input으로 분리하며 CLI exit 1입니다. 읽기·파싱·저장 자체의 실패도 exit 1입니다. 로그는 집계만 남기며 실제 개인정보는 입력하지 않습니다. 테스트는 임시 폴더에 작은 교육 결과를 남깁니다.

## 이번 편 추가

    node src/automation/batch.mjs automation/input/tasks.json
    node src/automation/batch.mjs automation/input/tasks.csv

두 형식 모두 successCount=2, failedCount=1, skippedCount=1, partial_failure와 exit 1이 정상 관찰 결과입니다. 오류 행을 의도적으로 포함했습니다. rejected의 row는 헤더를 제외한 입력 레코드 번호(1부터)입니다. 출력 경로는 각 명령의 runId를 확인합니다.

CSV는 sourceId,title,status,dueAt의 단순 UTF-8 예제만 허용합니다. 따옴표가 있는 CSV, 필드 내부 쉼표·줄바꿈은 지원하지 않고 명시적으로 거부합니다. 실제 복잡한 CSV에는 별도 검증된 파서가 필요합니다. 파일 파싱 실패는 전체 중단, 레코드 검증 실패는 나머지 행 처리를 계속합니다.

src/automation/batch.mjs

import { extname } from 'node:path';
import { readJSON } from './read-json.mjs';
import { readCSV } from './read-csv.mjs';
import { run } from './run.mjs';
import { isMain } from './paths.mjs';
export async function batch(file, options) {
  const ext = extname(String(file)).toLowerCase();
  if (!['.json', '.csv'].includes(ext)) throw new Error('JSON 또는 CSV 경로가 필요합니다.');
  return run(await (ext === '.csv' ? readCSV(file) : readJSON(file)), options);
}
if (isMain(import.meta.url)) {
  try {
    const result = await batch(process.argv[2] ?? new URL('../../automation/input/tasks.json', import.meta.url));
    console.log(JSON.stringify(result, null, 2));
    if (result.failedCount) process.exitCode = 1;
  } catch { console.error('batch 읽기·파싱·저장 실패'); process.exitCode = 1; }
}

src/automation/normalize-task.mjs

export function invalid(field, reason) {
  return Object.assign(new Error(reason), { field });
}

export function normalizeTask(input) {
  if (!input || typeof input !== 'object' || Array.isArray(input)) throw invalid('record', '객체가 필요합니다.');
  const { sourceId, title, status = 'todo', dueAt = null } = input;
  if (typeof sourceId !== 'string' || !/^[a-zA-Z0-9_-]{1,80}$/.test(sourceId)) throw invalid('sourceId', '영문·숫자·밑줄·하이픈 식별자가 필요합니다.');
  if (typeof title !== 'string' || !title.trim() || title.length > 200) throw invalid('title', '1~200자 제목이 필요합니다.');
  if (!['todo', 'doing', 'done'].includes(status)) throw invalid('status', 'todo, doing, done 중 하나여야 합니다.');
  if (dueAt !== null && (typeof dueAt !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(dueAt) || !Number.isFinite(Date.parse(dueAt)) || new Date(dueAt).toISOString() !== dueAt.replace(/Z$/, dueAt.includes('.') ? 'Z' : '.000Z'))) {
    throw invalid('dueAt', '유효한 UTC ISO 날짜 또는 null이 필요합니다.');
  }
  return { sourceId, title: title.trim(), status, dueAt };
}

src/automation/paths.mjs

import { fileURLToPath } from 'node:url';
import { resolve } from 'node:path';
export const automationRoot = fileURLToPath(new URL('../../automation/', import.meta.url));
export function isMain(url) {
  return Boolean(process.argv[1]) && resolve(process.argv[1]) === fileURLToPath(url);
}

src/automation/read-csv.mjs

import { readFile } from 'node:fs/promises';
export function parseCSV(raw) {
  if (raw.includes('"')) throw new Error('이 예제는 따옴표·quoted comma·줄바꿈 필드를 지원하지 않습니다.');
  const lines = raw.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n').trimEnd().split('\n');
  const headers = lines.shift().split(',').map((v) => v.trim());
  const allowed = ['sourceId', 'title', 'status', 'dueAt'];
  if (!headers.includes('sourceId') || !headers.includes('title') || new Set(headers).size !== headers.length || headers.some((h) => !allowed.includes(h))) throw new Error('CSV 헤더 오류');
  return lines.map((line, index) => {
    const values = line.split(',');
    if (values.length !== headers.length) throw new Error(`CSV ${index + 2}행 필드 수 오류`);
    return Object.fromEntries(headers.map((key, i) => [key, key === 'dueAt' && !values[i] ? null : values[i]]));
  });
}
export async function readCSV(file) { return parseCSV(await readFile(file, 'utf8')); }

src/automation/read-json.mjs

import { readFile } from 'node:fs/promises';
export async function readJSON(file) {
  let value;
  try { value = JSON.parse((await readFile(file, 'utf8')).replace(/^\uFEFF/, '')); }
  catch { throw new Error('JSON 읽기 또는 파싱 실패'); }
  if (!Array.isArray(value)) throw new Error('JSON 배열이 필요합니다.');
  return value;
}

src/automation/ready.mjs

const major = Number(process.versions.node.split('.')[0]);
if (major < 24) {
  console.error('Node.js 24 이상이 필요합니다.');
  process.exitCode = 1;
} else {
  console.log('TaskFlow Ops Automation 준비 완료: 로컬 비식별 실습만 실행합니다.');
}

src/automation/run.mjs

import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
import { join } from 'node:path';
import { normalizeTask } from './normalize-task.mjs';
import { automationRoot, isMain } from './paths.mjs';

export async function run(inputs, { root = automationRoot, trigger = 'manual' } = {}) {
  if (!Array.isArray(inputs)) throw new Error('입력 배열이 필요합니다.');
  const runId = randomUUID();
  const startedAt = new Date().toISOString();
  const valid = [], rejected = [], seen = new Set();
  let skippedCount = 0;
  inputs.forEach((input, index) => {
    try {
      const task = normalizeTask(input);
      if (seen.has(task.sourceId)) { skippedCount += 1; return; }
      seen.add(task.sourceId);
      valid.push(task);
    } catch (error) {
      // 원본은 비식별 교육 fixture만 저장합니다. 운영 개인정보는 별도 보호가 필요합니다.
      rejected.push({ row: index + 1, sourceId: input?.sourceId ?? null, field: error.field ?? 'record', reason: error.message, input });
    }
  });
  const output = join(root, 'output', runId);
  await mkdir(output, { recursive: true });
  await mkdir(join(root, 'logs'), { recursive: true });
  const json = (value) => JSON.stringify(value, null, 2) + '\n';
  await writeFile(join(output, 'valid.json'), json(valid), { flag: 'wx' });
  await writeFile(join(output, 'rejected.json'), json(rejected), { flag: 'wx' });
  const log = { runId, trigger, startedAt, finishedAt: new Date().toISOString(), inputCount: inputs.length, successCount: valid.length, failedCount: rejected.length, skippedCount, status: rejected.length ? (valid.length ? 'partial_failure' : 'failed') : 'completed', output };
  await writeFile(join(root, 'logs', runId + '.json'), json(log), { flag: 'wx' });
  return log;
}

if (isMain(import.meta.url)) {
  try {
    const file = process.argv[2] ?? join(automationRoot, 'input', 'task.json');
    const value = JSON.parse(await readFile(file, 'utf8'));
    const result = await run(Array.isArray(value) ? value : [value]);
    console.log(JSON.stringify(result, null, 2));
    if (result.failedCount) process.exitCode = 1;
  } catch { console.error('입력 읽기·파싱 또는 결과 저장 실패. 경로와 형식을 확인하세요.'); process.exitCode = 1; }
}

tests/core.test.mjs

import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, readFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { normalizeTask } from '../src/automation/normalize-task.mjs';
import { run } from '../src/automation/run.mjs';

test('sourceId와 제목을 보존하고 공백을 정규화한다', () => {
  assert.deepEqual(normalizeTask({ sourceId: 'sample-1', title: '  점검  ' }),
    { sourceId: 'sample-1', title: '점검', status: 'todo', dueAt: null });
});
test('빈 제목과 잘못된 식별자를 거부한다', () => {
  assert.throws(() => normalizeTask({ sourceId: 'sample-1', title: ' ' }));
  assert.throws(() => normalizeTask({ sourceId: '../unsafe', title: '점검' }));
});
test('한 건 처리와 동일 입력의 별도 실행 기록을 확인한다', async () => {
  const root = await mkdtemp(join(tmpdir(), 'taskflow-core-'));
  const input = { sourceId: 'sample-1', title: ' 점검 ' };
  const first = await run([input], { root });
  const second = await run([input], { root });
  assert.equal(first.successCount, 1);
  assert.notEqual(first.runId, second.runId);
  const output = JSON.parse(await readFile(join(root, 'output', first.runId, 'valid.json'), 'utf8'));
  assert.equal(output[0].sourceId, 'sample-1');
});
test('검증 실패는 rejected와 실패 상태로 남는다', async () => {
  const root = await mkdtemp(join(tmpdir(), 'taskflow-invalid-'));
  const result = await run([{ sourceId: 'sample-2', title: '' }], { root });
  assert.equal(result.status, 'failed');
  assert.equal(result.failedCount, 1);
  const rejected = JSON.parse(await readFile(join(root, 'output', result.runId, 'rejected.json'), 'utf8'));
  assert.equal(rejected[0].row, 1);
  assert.equal(rejected[0].field, 'title');
});

tests/files.test.mjs

import test from 'node:test';
import assert from 'node:assert/strict';
import { parseCSV } from '../src/automation/read-csv.mjs';
import { readJSON } from '../src/automation/read-json.mjs';
import { run } from '../src/automation/run.mjs';
import { mkdtemp } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

test('통제된 UTF-8 CSV를 읽는다', () => {
  assert.deepEqual(parseCSV('sourceId,title,status,dueAt\r\nsample-1,한글,todo,\r\n'), [{ sourceId: 'sample-1', title: '한글', status: 'todo', dueAt: null }]);
});
test('따옴표 CSV와 필드 수·중복 헤더 오류를 명시 거부한다', () => {
  for (const raw of ['sourceId,title\na,"b,c"', 'sourceId,title\na,b,c', 'sourceId,sourceId\na,b']) assert.throws(() => parseCSV(raw));
});
test('JSON과 CSV의 동일 모델, 잘못된 행과 중복 분리', async () => {
  const input = await readJSON(new URL('../automation/input/tasks.json', import.meta.url));
  const root = await mkdtemp(join(tmpdir(), 'taskflow-batch-'));
  const result = await run(input, { root });
  assert.equal(result.successCount, 2);
  assert.equal(result.failedCount, 1);
  assert.equal(result.skippedCount, 1);
  assert.equal(result.status, 'partial_failure');
});

독자 확인: 파일 한 행이 잘못되어도 정상 행을 보존할 수 있을까요?

아래는 비식별 로컬 예제 또는 설계표를 직접 점검하기 위한 절차입니다. 이전에 기록된 로컬 테스트 결과와 구분해 읽으세요.

확인 항목 실습과 판단 기준
입력 automation/input/tasks.json과 tasks.csv의 비식별 4행을 사용하고, 빈 title 한 행과 중복 sourceId 한 행을 구분합니다.
처리 각 파일을 batch.mjs에 전달하고 inputCount, successCount, failedCount, skippedCount 및 행 번호를 기록합니다. JSON 문법을 일부러 깨뜨린 경우는 행 검증과 분리해 관찰합니다.
예상 결과 제공된 fixture의 예상 집계는 4입력·2성공·1실패·1건너뜀입니다. 정상 출력과 rejected 출력의 sourceId 및 원본 행 번호를 서로 대조합니다. partial_failure이면 exit code 1을 성공으로 오인하지 않습니다.
같은 입력 재실행 같은 sourceId의 재입력을 단순히 두 번 저장했는지, 한 배치 안에서 건너뛰었는지 구분합니다. 실행 간 중복 방지는 이 단계의 검증 대상이 아닙니다.
실패와 복구 JSON.parse 오류라면 원본을 고쳐 파일 전체를 다시 읽습니다. CSV의 인용부호 안 쉼표·개행은 단순 split 파서 범위 밖이므로 실패 데이터를 보존하고 검증된 파서를 고려합니다.
다음 학습 4회차에서 검증한 데이터를 로컬 HTTP API와 연결합니다.

이번 편집의 검증 범위: 문서와 공개 본문 반영을 확인합니다. 이 회차의 자동화·외부 서비스·예약 작업을 이번 편집에서 새로 실행하지 않았으므로 실행 결과는 미확인입니다. 위 수치는 기존 예제의 예상값이며 실제 환경에서 직접 확인해야 합니다.

실행과 검증 결과

ZIP을 푼 프로젝트 루트에서 다음 명령을 각각 실행합니다. Node.js 24 기준이며 외부 패키지 설치는 필요하지 않습니다.

node --test
node src/automation/batch.mjs automation/input/tasks.json
node src/automation/batch.mjs automation/input/tasks.csv

테스트 7개가 통과했습니다. JSON·CSV 모의 입력은 각각 inputCount=4, successCount=2, failedCount=1, skippedCount=1로 확인됐습니다. 의도적으로 빈 제목 한 행을 포함하므로 partial_failure와 종료 코드 1이 기대 결과입니다. row는 헤더를 제외한 레코드 번호로 1부터 셉니다.

검증은 비식별 로컬 실습 범위입니다. 실제 외부 API·인증·메시지·게시·운영 예약 실행은 포함하지 않습니다.

완료 체크포인트

automation-step-03-file-io

다음 학습

4회차 REST API 업무 자동화 fetch 인증 페이지네이션 기준

실습 파일 다운로드

automation-step-03-file-io 전체 파일 ZIP

시작 체크포인트: 2회차 전체 파일 ZIP

합성 데이터와 로컬 파일·localhost 모의 서버로 실행합니다. 외부 서비스 쓰기·실제 알림·운영 예약 작업은 실행하지 않습니다.

글 소개·학습 준비·전체 목차
표와 문서를 정리하는 장치로 표현한 CSV·JSON 파일 자동화 표지

업무·자동화 · 3 / 11

CSV JSON 파일 자동화 Node.js 읽기 검증 변환 저장

반복 업무에서 가장 흔한 파일 기반 입력을 다룹니다. JSON 배열과 통제된 형식의 CSV를 읽고 각 행을 TaskFlow 내부 데이터 구조로 변환한 뒤 성공 데이터와 실패 데이터를 별도 파일로 저장합니다.

상단 실습은 이전 TaskFlow 파일 없이 시작합니다. 아래 누적 프로젝트에서는 같은 원리를 파일 입력·API·복구 기록에 차례로 적용합니다.

이번 글에서 배우는 내용

  • fs/promises로 파일 읽기·쓰기
  • JSON.parse 오류 처리
  • CSV의 header/row 개념
  • 단순 split(',') 방식이 quoted comma/newline에서 깨지는 한계
  • 실제 복잡한 CSV는 검증된 parser 도입을 고려해야 하는 이유
  • 원본 데이터를 덮어쓰지 않고 output을 새로 만드는 기준
  • 실패 행을 버리지 않고 rejected 파일로 보존하는 이유

선수 지식

Node.js에서 .mjs 실행, JSON 객체·배열과 비동기 함수의 기초가 필요합니다.

목차

  1. 핵심 원리
  2. 독립 실습: 파싱과 행 검증 나누기
  3. 자주 발생하는 오류와 해결
  4. 학습 완료 체크리스트
  5. TaskFlow Ops Automation 누적 적용
  6. 실습 프로젝트 전체 코드
  7. 실행과 검증 결과
  8. 완료 체크포인트
  9. 다음 학습

이 글이 도움이 되었나요?

조회 중

읽음 기록

읽음 — / 전체 11개 · 필수 11개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기