현재 구현 중인 Next.js 프로젝트에 필요한 오픈 API를 찾던 중
필요한 데이터가 CSV로만 제공하는 데이터였고, CSV 파일을 DB에서 저장해 관리해야 하는 문제가 있었다.
그래서 prisma를 사용해 DB에 미리 CSV 파일을 업로드해 저장하는 로직을 구현해보기로 했는데...,
구글링 해보니 관련 자료가 없었고, 있더라도 딱 세팅까지의 내용만 있어서
이후는 혼자 시행착오를 겪으며 결국 성공한😭 업로드까지의 과정을 남겨보려 한다.
프로젝트를 vercel로 배포되어 있고,
db는 vercel postgres를 선택해 미리 기본 스키마 및 db를 세팅해 둔 상태이다.
그리고 나는 아래와 같은 산책로 공공테이터를 DB에 업로드해보려 한다.
내 주변 산책로 데이터 URL
npm i @prisma/client
npm i prisma --save-dev-
schema.prisma
파일 작성프로젝트 가장 상위에 prisma
폴더 생성 후 schema.prisma
라는 이름의 파일이 추가되어 있음을 가정한다.
이 곳에 DB schema 정의가 되어 있을텐데, 업로드하고자 하는 csv 파일의 컬럼명을 참고해 모델을 추가 작성한다.
만약 이렇게 데이터에 컬럼명 정의서가 있다면 이를 참고해 작성하면 된다.
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL")
directUrl = env("POSTGRES_URL_NON_POOLING")
}
// 다른 model...
model Trail{
ESNTL_ID String @id @unique
WLK_COURS_FLAG_NM String
WLK_COURS_NM String
COURS_DC String
SIGNGU_NM String
COURS_LEVEL_NM String
COURS_LT_CN String
COURS_DETAIL_LT_CN String
ADIT_DC String
COURS_TIME_CN String
OPTN_DC String
TOILET_DC String
CVNTL_NM String
LNM_ADDR String
COURS_SPOT_LA String
COURS_SPOT_LO String
}
정의한 스키마를 db에 업데이트 해주면 세팅 끝!
npx prisma generate // db 업데이트 (실행 후 migrations 폴더에 이전 기록이 만들어진다.)
npx prisma migrate dev --name init // 기존 db 초기화
csv 파일을 파싱하기 위한 csv-parser
패키지를 설치한다.
(패키지 설치 없이 csv 파일을 불러올 수 있는 방법도 있다. 아래에 이 방법도 함께 언급할 예정)
npm i csv-parser
{
"compilerOptions": {
//...다른 설정
"module": "commonjs",
"esModuleInterop": true,
"outDir": "./dist",
},
"include": ["**/*.ts", ...],
}
위 속성을 꼭 작성해 주어야 한다.
dist
폴더 생성 후 ts파일을 만들어 준다.
// dist\importTrails.ts
'use server'; // prisma를 사용하기때문에 "use server" 선언
import { PrismaClient } from '@prisma/client';
import * as fs from 'fs';
const csvParser = require('csv-parser');
const prisma = new PrismaClient();
type Trail = {
ESNTL_ID: string;
WLK_COURS_FLAG_NM: string;
WLK_COURS_NM: string;
COURS_DC: string;
SIGNGU_NM: string;
COURS_LEVEL_NM: string;
COURS_LT_CN: string;
COURS_DETAIL_LT_CN: string;
ADIT_DC: string;
COURS_TIME_CN: string;
OPTN_DC: string;
TOILET_DC: string;
CVNTL_NM: string;
LNM_ADDR: string;
COURS_SPOT_LA: string;
COURS_SPOT_LO: string;
};
async function main() {
const trails: Trail[] = [];
fs.createReadStream('dist/trails.csv')
.pipe(
csvParser({
mapHeaders: ({ header }: { header: string }) => header.trim(),
}),
)
.on('data', (row: Trail) => {
trails.push(row);
})
.on('end', async () => {
console.log('CSV 파일이 성공적으로 처리되었습니다.');
for (const trail of trails) {
await prisma.trail.create({ data: trail });
}
console.log('모든 데이터가 삽입되었습니다.');
await prisma.$disconnect();
});
}
main().catch((e) => {
console.error(e);
prisma.$disconnect();
});
use server
: Prisma를 사용하기 위해 서버 측 코드임을 명시PrismaClient
)와 파일 시스템 모듈(fs
)을 사용require('csv-parser')
: csv-parser 모듈 불러오기 ⚠️ 에러 :
import csv from 'csv-parser’
사용 시 아래와 같은 에러가 발생할 수 있다.
"This module is declared with 'export =', and can only be used with a default import when using the 'esModuleInterop' flag."
-csv-parser
모듈은 'export ='를 사용하여 내보내기가 선언되어 있기 때문
-tsconfig.json
파일에서esModuleInterop
플래그가 활성화되어 있지 않을 때에도 발생하나,esModuleInterop : true
로 설정하더라도import
형식으로 불러올 때 동일하게 에러가 발생하였다.
type Trail
- csv 타입 정의 : CSV 파일에서 읽어온 데이터를 저장하기 위한 타입 정의하기main()
함수 정의 및 실행fs.createReadStream()
을 사용하여 csv
파일 읽어오기⚠️ 에러 : 첫번째 데이터만 undefined로 출력되는 문제
키값에 따옴표가 붙어서 파싱되는 문제 때문에 첫번째 데이터만 undefined로 불러오지 못하는 문제가 있었다.{ 'ESNTL_ID': 'KCCWSPO20N000001623', WLK_COURS_FLAG_NM: '관동별곡 8백리길', WLK_COURS_NM: '총 13코스', …, }
mapHeaders: ({ header }: { header: string }) => header.trim()
을 사용해 열 이름을 불러와 trim() 메소드를 실행해주니 해결되었다.
csvParser()
를 사용하여 CSV 파일을 파싱하고, 각 행을 Trail 타입의 객체로 변환prisma.(schema에 저장한 모델명).create()
를 사용하여 각 객체를 데이터베이스에 저장main()
함수 호출csv-parser
모듈을 사용 안할 경우 기본으로 내장되어 있는readline
모듈로도 csv 파일을 읽어올 수 있다.
'use server'; // prisma를 사용하기때문에 "use server" 선언
import { PrismaClient } from '@prisma/client';
import * as fs from 'fs';
import * as readline from 'readline';
const prisma = new PrismaClient();
type Trail = {
ESNTL_ID: string;
WLK_COURS_FLAG_NM: string;
WLK_COURS_NM: string;
COURS_DC: string;
SIGNGU_NM: string;
COURS_LEVEL_NM: string;
COURS_LT_CN: string;
COURS_DETAIL_LT_CN: string;
ADIT_DC: string;
COURS_TIME_CN: string;
OPTN_DC: string;
TOILET_DC: string;
CVNTL_NM: string;
LNM_ADDR: string;
COURS_SPOT_LA: string;
COURS_SPOT_LO: string;
};
async function main() {
const trails: Trail[] = [];
const fileStream = fs.createReadStream('dist/trails.csv');
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
});
let isFirstLine = true; // 첫 번째 줄인지 여부를 확인하기 위한 플래그
for await (const line of rl) {
if (isFirstLine) {
isFirstLine = false;
continue; // 첫 번째 줄인 경우 스킵하고 다음 줄로 이동
}
const row = line.split(',');
trails.push({
ESNTL_ID: row[0],
WLK_COURS_FLAG_NM: row[1],
WLK_COURS_NM: row[2],
COURS_DC: row[3],
SIGNGU_NM: row[4],
COURS_LEVEL_NM: row[5],
COURS_LT_CN: row[6],
COURS_DETAIL_LT_CN: row[7],
ADIT_DC: row[8],
COURS_TIME_CN: row[9],
OPTN_DC: row[10],
TOILET_DC: row[11],
CVNTL_NM: row[12],
LNM_ADDR: row[13],
COURS_SPOT_LA: row[14],
COURS_SPOT_LO: row[15],
});
}
console.log('CSV 파일이 성공적으로 처리되었습니다.');
for (const trail of trails) {
await prisma.trail.create({ data: trail });
}
console.log('모든 데이터가 삽입되었습니다.');
await prisma.$disconnect();
}
main().catch((e) => {
console.error(e);
prisma.$disconnect();
});
이제 작성한 파일을 실행시켜주면 DB에 업로드할 수 있게 된다!
나는 초기 데이터를 db에 미리 저장하려고 하기 때문에 node 환경에서 importTrail
파일을 실행해주었다.
(그렇지 않을 경우 원하는 컴포넌트에 위 파일을 불러아 async await으로 main()
함수를 실행하면 된다)
npx tsc dist/importTrails.ts ts // js로 변환
명령어 실행 후 dist 폴더에 js 파일이 생겨난다.
변환한 js 파일을 node 환경에서 실행한다.
node dist\importTrails.js // 실행
💡 DB에 들어갈 데이터가 많을 경우 업로드 시간이 좀 걸리기 때문에 위 코드처럼 console.log를 찍어 진행 상태와 완료 여부를 확인하는 것이 좋다.
모든 데이터가 삽입되었습니다.
출력 후 npx prisma studio
실행 시 db에 아래와 같이 업로드된 것을 확인할 수 있다.
(캡처한 이미지는 업로드 진행 중일 때 확인한 것으로, 업로드 진행 중에 studio를 들어가면 새로고침할 때마다 실시간으로 데이터가 들어오고 있는 것을 볼 수 있다.)