Next.js | prisma로 csv 파일 DB에 저장하기

dayannne·2024년 6월 16일
0

현재 구현 중인 Next.js 프로젝트에 필요한 오픈 API를 찾던 중
필요한 데이터가 CSV로만 제공하는 데이터였고, CSV 파일을 DB에서 저장해 관리해야 하는 문제가 있었다.
그래서 prisma를 사용해 DB에 미리 CSV 파일을 업로드해 저장하는 로직을 구현해보기로 했는데...,

구글링 해보니 관련 자료가 없었고, 있더라도 딱 세팅까지의 내용만 있어서
이후는 혼자 시행착오를 겪으며 결국 성공한😭 업로드까지의 과정을 남겨보려 한다.


1. prisma 환경 세팅

프로젝트를 vercel로 배포되어 있고,
db는 vercel postgres를 선택해 미리 기본 스키마 및 db를 세팅해 둔 상태이다.

그리고 나는 아래와 같은 산책로 공공테이터를 DB에 업로드해보려 한다.
내 주변 산책로 데이터 URL

1) prisma 관련 패키지 다운로드하기

npm i @prisma/client
npm i prisma --save-dev- 

2) 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
 }

3) DB 업데이트

정의한 스키마를 db에 업데이트 해주면 세팅 끝!

npx prisma generate // db 업데이트 (실행 후 migrations 폴더에 이전 기록이 만들어진다.)
npx prisma migrate dev --name init // 기존 db 초기화

2. csv 파일로 DB에 적용하기

1) csv-parser 설치

csv 파일을 파싱하기 위한 csv-parser 패키지를 설치한다.
(패키지 설치 없이 csv 파일을 불러올 수 있는 방법도 있다. 아래에 이 방법도 함께 언급할 예정)

npm i csv-parser

2) tsconfig.json 수정

{
  "compilerOptions": {
    //...다른 설정
    "module": "commonjs",
    "esModuleInterop": true,
    "outDir": "./dist",
  },
  "include": ["**/*.ts", ...],
}

위 속성을 꼭 작성해 주어야 한다.

3) csv import 파일 작성(csv-parser 사용.ver)

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 모듈 가져오기: 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() 함수 정의 및 실행
  1. fs.createReadStream()을 사용하여 csv 파일 읽어오기

    ⚠️ 에러 : 첫번째 데이터만 undefined로 출력되는 문제
    키값에 따옴표가 붙어서 파싱되는 문제 때문에 첫번째 데이터만 undefined로 불러오지 못하는 문제가 있었다.

    {
        'ESNTL_ID': 'KCCWSPO20N000001623',
        WLK_COURS_FLAG_NM: '관동별곡 8백리길',
        WLK_COURS_NM: '총 13코스',,
    }

    mapHeaders: ({ header }: { header: string }) => header.trim() 을 사용해 열 이름을 불러와 trim() 메소드를 실행해주니 해결되었다.

  2. csvParser()를 사용하여 CSV 파일을 파싱하고, 각 행을 Trail 타입의 객체로 변환
  3. 파싱된 데이터를 선언한 배열에 저장
  4. 모든 데이터 처리가 완료되면, prisma.(schema에 저장한 모델명).create()를 사용하여 각 객체를 데이터베이스에 저장
  5. 예외 처리와 함께 main() 함수 호출

+) csv import 파일 작성(csv-parser 사용 없이.ver)

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();
});

3. DB에 업로드

이제 작성한 파일을 실행시켜주면 DB에 업로드할 수 있게 된다!

나는 초기 데이터를 db에 미리 저장하려고 하기 때문에 node 환경에서 importTrail 파일을 실행해주었다.
(그렇지 않을 경우 원하는 컴포넌트에 위 파일을 불러아 async await으로 main()함수를 실행하면 된다)

1) js 로 변환

npx tsc dist/importTrails.ts ts // js로 변환

명령어 실행 후 dist 폴더에 js 파일이 생겨난다.

2) import 파일 실행하기

변환한 js 파일을 node 환경에서 실행한다.

node dist\importTrails.js // 실행

💡 DB에 들어갈 데이터가 많을 경우 업로드 시간이 좀 걸리기 때문에 위 코드처럼 console.log를 찍어 진행 상태와 완료 여부를 확인하는 것이 좋다.

모든 데이터가 삽입되었습니다. 출력 후 npx prisma studio 실행 시 db에 아래와 같이 업로드된 것을 확인할 수 있다.
(캡처한 이미지는 업로드 진행 중일 때 확인한 것으로, 업로드 진행 중에 studio를 들어가면 새로고침할 때마다 실시간으로 데이터가 들어오고 있는 것을 볼 수 있다.)

참고

Velog - CSV파일 prisma 이용해서 DB에 저장하기

profile
☁️

0개의 댓글