JSON을 Excel로 내보내기: 셀 타입·선행 0·셀당 비용까지 설계하기

운영팀이 재고 목록을 엑셀로 내려받고 싶다고 하면 데이터 배열을 파일로 바꾸는 작업만 떠올리기 쉽습니다. 실제 문제는 그다음에 드러납니다. 상품 코드의 앞자리 0이 사라지고, 수량이 문자열로 저장되어 합계가 맞지 않거나, 필요 없는 열까지 포함되어 사용량이 늘어납니다. 다운로드 기능은 데이터의 의미를 보존하는 작은 인터페이스입니다.
APICK JSON 데이터 EXCEL 파일 변환 API는 객체 배열 또는 2차원 배열을 XLSX 파일로 변환합니다. 이 글은 출고 검토용 재고 목록을 예로 들어 문자열과 숫자 구분, 중첩 폼 필드 작성, 파일 오류 처리, 셀당 비용 계산을 살펴봅니다. 화면에서 보이는 내용을 무조건 전부 내보내기보다 실제 업무에 필요한 열을 먼저 정의하는 방식으로 시작합니다.
1. 내보내기 스키마를 먼저 고정하기
가상 예제로 상품코드, 수량, 확인완료, 비고 네 열을 사용하겠습니다. 상품코드는 00123처럼 앞자리 0이 의미 있으므로 문자열, 수량은 계산에 사용할 숫자, 확인완료는 불리언, 아직 작성하지 않은 비고는 null로 정합니다. 빈 문자열과 null을 같은 뜻으로 사용할지 구분할지도 업무 규칙에 포함하세요. 파일을 받은 사람이 어느 값이 누락되었고 어느 값이 의도적으로 비어 있는지 이해할 수 있어야 합니다.
객체 배열을 쓸 때는 모든 행의 키를 같은 순서와 이름으로 구성하는 것을 권장합니다. 2차원 배열은 모든 행의 열 개수가 같아야 합니다. 원래 데이터에 열이 조금씩 다르다면 변환 전에 표준 스키마로 맞추세요. 어떤 행에만 내부 메모가 들어 있거나 권한 없는 열이 섞이는 상황을 막는 데도 도움이 됩니다. 내보내기 스키마를 별도 함수로 두면 화면 변경과 파일 형식 변경을 독립적으로 검수할 수 있습니다.
2. JSON이라는 이름이어도 전송은 multipart/form-data
공식 요청 명세의 요청 경로는 POST https://apick.app/rest/json_to_excel이며 Bearer 인증을 사용합니다. 필수 입력 data_list는 배열, 선택 입력 sheet_name은 시트 이름 문자열입니다. 현재 공식 REST 가이드는 중첩 배열과 객체를 개별 폼 필드로 전송하도록 안내합니다. 아래 예제에서는 data_list 전체를 JSON 문자열 하나로 넣지 않고 각 셀의 경로를 이름으로 사용합니다.
| 필드 | 형식과 처리 기준 |
|---|---|
data_list[0][상품코드] | 객체 배열 첫 행의 상품코드 값입니다. 타입 생략 시 string으로 처리됩니다. |
data_list[0][0] | 2차원 배열 첫 행의 첫 셀을 나타내는 경로입니다. |
__apick_type[data_list] | 최상위 자료구조 타입을 array로 지정합니다. |
__apick_type[data_list][0] | 첫 행 타입을 object 또는 array로 지정합니다. |
__apick_type[data_list][0][수량] | 해당 셀 타입을 number로 지정하는 예입니다. |
__apick_type[data_list][0][확인완료] | 해당 셀 타입을 boolean으로 지정합니다. |
__apick_type[data_list][0][비고] | null로 지정할 때 해당 셀의 폼 값도 빈 문자열로 함께 보냅니다. |
sheet_name | 선택 시트명입니다. 생략하면 서버 기본 이름을 사용합니다. |
응답 본문 및 헤더 | 본문은 XLSX 바이너리입니다. success, cost, ms, Content-Type, Content-Disposition 헤더가 안내되어 있습니다. |
허용된 타입 메타데이터는 string, number, boolean, null, array, object입니다. 빈 배열과 빈 객체도 타입과 빈 폼 값을 함께 보내고, 비어 있지 않은 컨테이너는 타입 및 하위 필드를 보냅니다. 특히 객체 열 이름이 숫자라면 행 타입 object를 명시해야 배열 경로와 구분할 수 있습니다. 이 규격은 현재 공식 개발가이드의 입력 설명을 기준으로 작성했습니다.
3. curl: 00123은 문자열, 수량은 숫자로 전송하기
아래는 가상 상품 한 행을 네 셀로 구성하는 요청입니다. 상품코드에는 number 태그를 붙이지 않아 문자열 의미를 유지합니다. null 값은 타입만 전송하지 않고 빈 폼 값도 같이 전송합니다. HTTP 오류가 없을 때에만 파일 검사를 진행하며 JSON 응답을 거르고 XLSX의 필수 구성 파일이 있는지 확인한 뒤 최종 이름으로 저장합니다.
curl --fail-with-body --request POST 'https://apick.app/rest/json_to_excel' \
--header "Authorization: Bearer ${APICK_API_KEY:?Set APICK_API_KEY}" \
--form-string '__apick_type[data_list]=array' \
--form-string '__apick_type[data_list][0]=object' \
--form-string 'data_list[0][상품코드]=00123' \
--form-string 'data_list[0][수량]=3' \
--form-string '__apick_type[data_list][0][수량]=number' \
--form-string 'data_list[0][확인완료]=true' \
--form-string '__apick_type[data_list][0][확인완료]=boolean' \
--form-string 'data_list[0][비고]=' \
--form-string '__apick_type[data_list][0][비고]=null' \
--form-string 'sheet_name=출고검토' \
--dump-header response.headers --output response.tmp &&
python3 - <<'CHECK'
from pathlib import Path
from io import BytesIO
from zipfile import ZipFile
headers = Path("response.headers").read_text().lower()
body = Path("response.tmp").read_bytes()
if "application/json" in headers or "+json" in headers or body.lstrip().startswith((b"{", b"[")):
raise SystemExit("JSON response received; do not save as XLSX")
with ZipFile(BytesIO(body)) as workbook:
names = set(workbook.namelist())
if not {"[Content_Types].xml", "xl/workbook.xml"}.issubset(names):
raise SystemExit("Unexpected workbook structure")
with open("inventory.xlsx", "xb") as output:
output.write(body)
print("XLSX saved after preliminary checks")
CHECK이 검사는 잘못된 응답을 엑셀 파일로 저장하는 일을 줄이는 예시입니다. 셀의 표시 형식과 수식 해석, 실제 업무 내용까지 검증하는 완전한 파일 검수는 아닙니다. 임시 응답 파일은 최소 권한으로 취급하고 처리 후 보관 정책에 따라 정리하세요. 파일 크기가 커질 수 있는 서비스에서는 메모리 사용량과 다운로드 제한도 애플리케이션에서 별도로 설계해야 합니다.
4. Node.js에서 같은 폼 계약으로 만들기
아래 예제는 복잡한 범용 직렬화기를 직접 만드는 대신 네 가지 셀 타입을 분명하게 넣습니다. 여러 행으로 확장할 때는 열 목록과 타입 맵을 따로 두고 반복문을 구성하면 됩니다. Content-Type을 직접 고정하면 boundary가 빠질 수 있으므로 FormData가 헤더를 구성하도록 두세요. 반환된 바이트의 ZIP 시그니처 검사는 기본 검사이며 실제 XLSX 구조 검사는 추가 검수 단계에서 수행합니다.
import { writeFile } from "node:fs/promises";
const apiKey = process.env.APICK_API_KEY;
if (!apiKey) throw new Error("APICK_API_KEY is required");
const form = new FormData();
const fields = [
["__apick_type[data_list]", "array"],
["__apick_type[data_list][0]", "object"],
["data_list[0][상품코드]", "00123"],
["data_list[0][수량]", "3"],
["__apick_type[data_list][0][수량]", "number"],
["data_list[0][확인완료]", "true"],
["__apick_type[data_list][0][확인완료]", "boolean"],
["data_list[0][비고]", ""],
["__apick_type[data_list][0][비고]", "null"],
["sheet_name", "출고검토"]
];
for (const [key, value] of fields) form.append(key, value);
const response = await fetch("https://apick.app/rest/json_to_excel", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` },
body: form,
signal: AbortSignal.timeout(120_000)
});
if (!response.ok) throw new Error(`Excel HTTP ${response.status}`);
if ((response.headers.get("content-type") ?? "").toLowerCase().includes("json"))
throw new Error("Excel service returned JSON, not an XLSX file");
const bytes = new Uint8Array(await response.arrayBuffer());
const prefix = new TextDecoder().decode(bytes.subarray(0, 64)).trimStart();
if (prefix.startsWith("{") || prefix.startsWith("["))
throw new Error("Possible JSON error body");
if (bytes[0] !== 0x50 || bytes[1] !== 0x4b || bytes[2] !== 0x03 || bytes[3] !== 0x04)
throw new Error("Expected a ZIP-based XLSX response");
await writeFile("inventory.xlsx", bytes, { flag: "wx" });
const rawCost = response.headers.get("cost");
const cost = rawCost !== null && rawCost.trim() !== "" && Number.isFinite(Number(rawCost))
? Number(rawCost) : null;
console.log({ bytes: bytes.length, cost });5. SDK의 입력 검사와 업무 검사를 구분하기
공식 SDK에는 client.jsonToExcel(data, { sheetName }) 메서드가 있습니다. 확인한 SDK 구현에서는 data가 배열인지 검사하고 선택 시트명을 확인합니다. 폼 직렬화 구현에서는 중첩 값에 타입 메타데이터를 붙입니다. 그렇다고 모든 업무상 타입 오류나 행 길이 차이까지 자동 검증된다고 확대해서 해석하지 마세요.
애플리케이션은 내보내기 전에 수량이 유한한 숫자인지, 상품코드가 문자열인지, 각 행에 필요한 열이 있는지를 검사해야 합니다. 날짜는 서비스에서 정한 문자열 형식으로 미리 정규화하면 표현이 분명해집니다. 객체나 배열을 셀 하나에 표시하려면 원하는 평탄화 방식을 정하고 전처리하는 편이 좋습니다. 라이브러리 버전을 고정하고 설치한 버전의 실제 동작을 샘플 파일로 확인하세요.
import { ApickClient } from "apick-api";
const rows = [
{ 상품코드: "00123", 수량: 3, 확인완료: true, 비고: null }
];
// Application-level checks; these are not claimed SDK guarantees.
for (const row of rows) {
if (typeof row.상품코드 !== "string" || !Number.isFinite(row.수량)
|| typeof row.확인완료 !== "boolean"
|| !(row.비고 === null || typeof row.비고 === "string"))
throw new TypeError("Unexpected inventory row");
}
const client = new ApickClient(process.env.APICK_API_KEY);
const workbook = await client.jsonToExcel(rows, { sheetName: "출고검토" });
// SDK errors must be handled by the caller; inspect the sample workbook before release.
await workbook.save("inventory-sdk.xlsx");6. 비용 단위는 호출당이 아니라 셀당 1P
2026-10-11 UTC에 확인한 공식 요금표는 JSON 데이터 EXCEL 파일 변환을 셀당 1P로 안내합니다. 과금 대상으로 계산된 셀이 1,000개라면 단순 예상은 1,000P입니다. 예를 들어 과금 대상 범위가 100행 × 6열로 산정되는 상황은 600P, 같은 행 수에 8열이면 800P로 계산할 수 있습니다. 호출 횟수만으로 예산을 잡으면 데이터가 늘어날 때 사용량을 잘못 예측할 수 있습니다.
다만 자동 제목 행, 비어 있는 값, 희소한 객체의 열을 어떻게 셀 수에 포함하는지는 확인한 자료만으로 단정하지 않습니다. 위 계산은 과금 대상 셀 수가 정해졌다는 가정의 예시입니다. 도입할 때 작은 샘플의 출력 구조와 응답 cost를 대조하고 실제 내보내기 크기에 맞는 예상 사용량을 잡으세요. 큰 목록은 생성 전에 행 수와 열 수를 보여주고 과도한 요청을 제한하는 방식도 고려할 수 있습니다.
필요 없는 내부 열을 제외하는 것은 정보 보호와 사용량 관리에 동시에 도움이 됩니다. 사용자가 같은 조건으로 여러 번 다운로드할 때 기존 결과를 재사용할지, 최신 데이터로 다시 만들지도 서비스에서 정하세요. 이전 파일 재사용 시에는 기준 시각을 표시하고 필터, 정렬, 열 목록, 데이터 버전이 같은지 확인해야 합니다. 현재 엔드포인트에 중복 과금 방지 기능이 있다고 가정해 자동 재시도를 걸지는 마세요.
7. 파일이 열리는 것과 업무에 맞는 것은 별도 검수
검수용 데이터에는 앞자리 0이 있는 식별자, 0과 음수 수량, true와 false, 빈 문자열, null, 긴 한글, 쉼표와 줄바꿈이 포함된 메모를 준비하세요. 줄바꿈의 세부 전송 처리와 표시 결과는 사용 중인 SDK 버전 또는 직접 연동 방식에서 확인합니다. 계산용 숫자가 실제로 계산에 사용되는지, 열 이름과 순서가 예상과 같은지 파일을 열어 살펴보는 단계가 필요합니다.
사용자 입력 문자열이 스프레드시트에서 수식으로 해석되는지도 검수 항목에 넣으세요. 현재 공식 가이드만으로 수식 주입 방어를 보장한다고 말할 수는 없습니다. 내보내기 정책에 따라 위험한 입력을 제한하거나 안전한 텍스트로 처리하고 결과를 확인해야 합니다. 고객 연락처나 내부 메모는 다운로드 권한이 있는 열만 선택하고, API 키는 서버에서 보관합니다. 완성 파일의 공유 권한과 보관 기간까지 정의해야 내보내기 기능을 안정적으로 운영할 수 있습니다.
자주 묻는 질문
00123을 숫자로 보내도 앞자리 0이 유지되나요?
식별자는 문자열로 보내세요. number 타입으로 바꾸면 숫자의 의미가 되므로 앞자리 0이 필요한 코드와 구분해야 합니다.
data_list를 JSON 문자열 하나로 넣어도 되나요?
이 글은 현재 REST 가이드의 중첩 폼 경로와 타입 메타데이터를 따릅니다. 직접 연동에서는 공식 요청 예시와 동일한 규격으로 구성하세요.
한 번의 다운로드는 언제나 1P인가요?
아닙니다. 확인한 요금은 셀당 1P입니다. 셀 집계의 세부 기준과 실제 차감액은 도입 시 샘플 응답 및 사용 내역으로 확인하세요.
다음 단계: 실제 화면 하나에 연결해 보기
연동을 시작할 때는 APICK 공식 개발가이드에서 현재 요청 규격을 확인하고, 요금 안내에서 적용 단가를 다시 확인하세요. 서버 프로젝트에서 SDK를 사용한다면 공식 Node.js 한국어 가이드도 함께 볼 수 있습니다. 테스트 범위를 작게 정한 뒤 입력 검증, 오류 화면, 사용 포인트 기록까지 연결하면 운영 단계에서 바꿀 부분이 선명해집니다.
확인 기준: 2026-10-11 UTC. 이 글의 API 필드와 단가는 위 공식 자료를 확인해 작성했습니다. 코드는 설명용 예시이며 실제 유료 API 호출로 실행 검증하지 않았습니다. 예제의 입력은 가상 데이터 또는 사용자가 직접 설정할 자리표시자입니다. 서비스 정책과 문서는 변경될 수 있습니다.
댓글
댓글 쓰기