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
합성 데이터와 로컬 파일·localhost 모의 서버로 실행합니다. 외부 서비스 쓰기·실제 알림·운영 예약 작업은 실행하지 않습니다.
글 소개·학습 준비·전체 목차

업무·자동화 · 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 객체·배열과 비동기 함수의 기초가 필요합니다.
목차
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 11개 · 필수 11개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.