Fastify 백엔드 서버 구성하기

곽태욱·2022년 12월 8일
2
post-thumbnail

Fastify 백엔드 서버를 구성해봅시다.

Yarn berry

https://yarnpkg.com/getting-started/install

corepack enable
mkdir 폴더이름
cd 폴더이름
yarn init -2

Git

https://www.toptal.com/developers/gitignore

.gitignore 파일을 생성하고 적절히 수정합니다.

.gitattributes 파일을 아래와 같이 생성합니다:

# Auto detect text files and perform LF normalization
* text eol=lf

# These files are binary and should be left untouched
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.webp binary
*.pdf binary

*.otf binary
*.ttf binary
*.woff binary
*.woff2 binary

그외 바이너리로 취급할 파일 목록

package.json

https://docs.npmjs.com/cli/v8/configuring-npm/package-json

package.json 파일을 수정합니다:

{
  "name": "프로젝트 이름",
  "version": "프로젝트 버전",
  "description": "프로젝트 설명",
  "homepage": "홈페이지 주소",
  "bugs": {
    "url": "버그 제보 주소",
    "email": "버그 제보 이메일"
  },
  "license": "라이선스",
  "author": "개발자",
  "main": "프로그램 진입점 파일 경로",
  "repository": "저장소 주소",
  "scripts": {
    // ...
  },
  "dependencies": {
    // ...
  },
  "devDependencies": {
    // ...
  },
  "engines": {
    "node": ">=18.2.0"
  },
  "private": true,
  "packageManager": "yarn@3.3.0"
}

TypeScript

https://www.typescriptlang.org/download \
https://stackoverflow.com/questions/72380007/what-typescript-configuration-produces-output-closest-to-node-js-18-capabilities/72380008#72380008

yarn add --dev typescript
yarn tsc --init

package.json 파일을 수정합니다:

{
  // ...
  "type": "module"
}

tsconfig.json 파일을 수정합니다:

{
  "compilerOptions": {
    // ...
    "allowSyntheticDefaultImports": true,
    "lib": ["ES2022"],
    "module": "ES2022",
    "moduleResolution": "node",
    "target": "ES2022"
  }
}

Prettier

https://prettier.io/docs/en/install.html

Prettier를 설치합니다.

yarn add --dev --exact prettier
echo {}> .prettierrc.json

.prettierrc.json 파일을 수정합니다:

{
  "printWidth": 100,
  "semi": false,
  "singleQuote": true
}

.prettierignore 파일을 아래와 같이 생성합니다:

.yarn
.pnp.*

ESLint

https://eslint.org/docs/latest/user-guide/getting-started \
https://github.com/standard/eslint-config-standard \
https://github.com/import-js/eslint-plugin-import \
https://github.com/weiran-zsd/eslint-plugin-node#readme \
https://github.com/xjamundx/eslint-plugin-promise \

npm init @eslint/config -- --config standard
✔ How would you like to use ESLint? · style
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · none
✔ Does your project use TypeScript? · No / Yes
✔ Where does your code run? · node
✔ How would you like to define a style for your project? · guide
✔ Which style guide do you want to follow? · standard
✔ What format do you want your config file to be in? · JSON
Checking peerDependencies of eslint-config-standard@latest
Local ESLint installation not found.
The config that you've selected requires the following dependencies:

@typescript-eslint/eslint-plugin@latest eslint-config-standard@latest eslint@^8.0.1 eslint-plugin-import@^2.25.2 eslint-plugin-n@^15.0.0 eslint-plugin-promise@^6.0.0 @typescript-eslint/parser@latest
✔ Would you like to install them now? · No

yarn add --dev @typescript-eslint/eslint-plugin@latest eslint-config-standard@latest eslint@^8.0.1 eslint-plugin-import@^2.25.2 eslint-plugin-n@^15.0.0 eslint-plugin-promise@^6.0.0 @typescript-eslint/parser@latest

eslintrc.json 파일을 수정합니다:

{
  "env": {
    "es2022": true,
    "node": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:import/recommended",
    "plugin:import/typescript",
    "plugin:n/recommended",
    "plugin:promise/recommended",
    "plugin:@typescript-eslint/recommended",
    "standard"
  ]
  // ...
}

ESLint + (Prettier, Jest)

https://github.com/prettier/eslint-config-prettier \
https://github.com/jest-community/eslint-plugin-jest

yarn add --dev eslint eslint-plugin-jest eslint-config-prettier

eslintrc.json 파일을 수정합니다:

{
  "env": {
    // ...
    "jest/globals": true
  },
  "extends": [
    // ...
    // Make sure to put "prettier" last, so it gets the chance to override other configs
    "prettier"
  ],
  "overrides": [
    {
      "extends": ["plugin:jest/all"],
      "files": ["test/**"],
      "rules": {
        "jest/prefer-expect-assertions": "off"
      }
    }
  ]
  // ...
}

package.json 파일을 수정합니다:

{
  "scripts": {
    "lint": "eslint . --fix --ignore-path .gitignore",
    "format": "prettier . --write"
    // ...
  }
  // ...
}

Yarn berry + (ESLint, Prettier, TypeScript, VSCode, Next.js)

https://yarnpkg.com/getting-started/editor-sdks \
https://yarnpkg.com/cli/upgrade-interactive

yarn dlx @yarnpkg/sdks vscode
yarn plugin import interactive-tools

Husky

https://typicode.github.io/husky/#/?id=automatic-recommended \
https://typicode.github.io/husky/#/?id=yarn-on-windows

Husky를 설치합니다.

yarn dlx husky-init --yarn2 && yarn

.husky/common.sh 파일을 생성합니다:

command_exists () {
  command -v "$1" >/dev/null 2>&1
}

# Workaround for Windows 10, Git Bash and Yarn
if command*exists winpty && test -t 1; then
  exec < /dev/tty
fi

.husky/pre-push 파일을 수정합니다:

#!/usr/bin/env sh
. "$(dirname -- "$0")/*/husky.sh"
. "$(dirname -- "$0")/common.sh"

yarn tsc

VSCode

.vscode/settings.json 파일을 수정합니다:

{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnPaste": true,
  "editor.formatOnSave": true,
  "editor.insertSpaces": true,
  "editor.tabSize": 2,
  "files.autoSave": "onFocusChange",
  "files.eol": "\n",
  "sort-imports.default-sort-style": "module"

  // ...
}

.vscode/recommendations.json 파일을 수정합니다:

{
  "recommendations": [
    "visualstudioexptteam.vscodeintellicode",
    "christian-kohler.npm-intellisense",
    "christian-kohler.path-intellisense",
    "tabnine.tabnine-vscode",
    "amatiasq.sort-imports",
    "ms-azuretools.vscode-docker",
    "bradymholt.pgformatter",
    "foxundermoon.shell-format",
    "ckolkman.vscode-postgres"

    // ...
  ]
}

Environment variables

https://github.com/motdotla/dotenv

yarn add dotenv

아래 파일을 생성합니다:

파일 이름NODE_ENV환경용도
.envproduction클라우드실 서버
.env.devproduction클라우드스테이징 서버
.env.localproduction로컬코드 축소
.env.local.devdevelopment로컬Fast refresh
.env.local.dockerproduction로컬Docker 컨테이너

src/common/constants.ts 파일을 생성합니다:

// 자동
export const NODE_ENV = process.env.NODE_ENV as string
export const K_SERVICE = process.env.K_SERVICE as string // GCP에서 실행 중일 때
export const PORT = (process.env.PORT ?? '4000') as string

// 공통
export const PROJECT_ENV = (process.env.PROJECT_ENV ?? '') as string
export const JWT_SECRET_KEY = process.env.JWT_SECRET_KEY as string

if (!PROJECT_ENV) throw new Error('`PROJECT_ENV` 환경 변수를 설정해주세요.')
if (!JWT_SECRET_KEY) throw new Error('`JWT_SECRET_KEY` 환경 변수를 설정해주세요.')

// 개별
export const LOCALHOST_HTTPS_KEY = process.env.LOCALHOST_HTTPS_KEY as string
export const LOCALHOST_HTTPS_CERT = process.env.LOCALHOST_HTTPS_CERT as string

if (PROJECT_ENV.startsWith('local')) {
  if (!LOCALHOST_HTTPS_KEY) throw new Error('`LOCALHOST_HTTPS_KEY` 환경 변수를 설정해주세요.')
  if (!LOCALHOST_HTTPS_CERT) throw new Error('`LOCALHOST_HTTPS_CERT` 환경 변수를 설정해주세요.')
}

Nodemon

https://github.com/remy/nodemon

yarn add --dev nodemon

nodemon.json 파일을 생성합니다:

{
  "ext": "cjs",
  "watch": ["out"]
}

esbuild

https://esbuild.github.io/getting-started/#bundling-for-node \
https://esbuild.github.io/api/#metafile \
https://stackoverflow.com/a/35455532/16868717 \

yarn add --dev esbuild

esbuild.js 파일을 생성합니다:

import esbuild from 'esbuild'

const NODE_ENV = process.env.NODE_ENV

esbuild
  .build({
    bundle: true,
    entryPoints: ['src/index.ts'],
    loader: {
      '.sql': 'text',
    },
    metafile: true,
    minify: NODE_ENV === 'production',
    outfile: 'out/index.cjs',
    platform: 'node',
    target: ['node18'],
    treeShaking: true,
    watch: NODE_ENV === 'development' && {
      onRebuild: (error, result) => {
        if (error) {
          console.error('watch build failed:', error)
        } else {
          showOutfilesSize(result)
        }
      },
    },
  })
  .then((result) => showOutfilesSize(result))
  .catch((error) => {
    throw new Error(error)
  })

function showOutfilesSize(result) {
  const outputs = result.metafile.outputs
  for (const output in outputs) {
    console.log(`${output}: ${(outputs[output].bytes / 1_000_000).toFixed(2)} MB`)
  }
}

package.json 파일을 수정합니다:

{
  "scripts": {
    "dev": "NODE_ENV=development node esbuild.js & NODE_ENV=development nodemon -r dotenv/config out/index.cjs dotenv_config_path=.env.local.dev",
    "build": "NODE_ENV=production node esbuild.js",
    "start": "yarn build && NODE_ENV=production node -r dotenv/config out/index.cjs dotenv_config_path=.env.local"
    // ...
  }
  // ...
}

src/global-env.d.ts 파일을 생성합니다:

declare module '*.sql' {
  const content: string
  export default content
}

Fastify

https://www.fastify.io/docs/latest/Guides/Getting-Started/ \

yarn add fastify

src/routes/index.ts 파일을 생성합니다:

import Fastify from 'fastify'

import { K_SERVICE, NODE_ENV, PORT, PROJECT_ENV } from '../common/constants'
import productRoute from './product'
import userRoute from './user'

const fastify = Fastify({
  logger: NODE_ENV === 'production',
})

fastify.register(productRoute)
fastify.register(userRoute)

export default async function startServer() {
  try {
    await fastify.listen({ port: +PORT, host: K_SERVICE ? '0.0.0.0' : 'localhost' })
  } catch (err) {
    fastify.log.error(err)
    throw new Error()
  }
}

.vscode/typescript.code-snippets 파일을 생성합니다:

{
  "Fastify Routes": {
    "prefix": "route",
    "body": [
      "import { FastifyInstance } from 'fastify'",
      "",
      "export default async function routes(fastify: FastifyInstance, options: object) {",
      "  fastify.get('/${TM_FILENAME_BASE}', async (request, reply) => {",
      "    return { hello: '${TM_FILENAME_BASE}' }",
      "  })",
      "}",
      ""
    ],
    "description": "Fastify Routes"
  }
}

src/routes/product.ts 파일을 생성하고 route 단축어를 입력해 아래 코드를 자동 완성합니다:

import { FastifyInstance } from 'fastify'

export default async function routes(fastify: FastifyInstance, options: object) {
  fastify.get('/product', async (request, reply) => {
    return { hello: 'product' }
  })
}

src/routes/user.ts 파일을 생성하고 route 단축어를 입력해 아래 코드를 자동 완성합니다:

import { FastifyInstance } from 'fastify'

export default async function routes(fastify: FastifyInstance, options: object) {
  fastify.get('/user', async (request, reply) => {
    return { hello: 'user' }
  })
}

Fastify + HTTP2

https://www.fastify.io/docs/latest/Reference/HTTP2/

src/routes/index.ts 파일을 수정합니다:

import {
  // ...
  LOCALHOST_HTTPS_CERT,
  LOCALHOST_HTTPS_KEY,
  PROJECT_ENV,
} from '../common/constants'

const fastify = Fastify({
  // ...
  http2: true,
  ...(PROJECT_ENV.startsWith('local') && {
    https: {
      key: `-----BEGIN PRIVATE KEY-----\n${LOCALHOST_HTTPS_KEY}\n-----END PRIVATE KEY-----`,
      cert: `-----BEGIN CERTIFICATE-----\n${LOCALHOST_HTTPS_CERT}\n-----END CERTIFICATE-----`,
    },
  }),
})

// ...

HTTPS 인증서를 생성합니다:

Fastify + CORS

https://github.com/fastify/fastify-cors

yarn add @fastify/cors

src/routes/index.ts 파일을 수정합니다:

import cors from '@fastify/cors'

// ...

fastify.register(cors, {
  origin: [
    'http://localhost:3000',
    // ...
  ],
})

Fastify + Prevent DoS

https://github.com/fastify/fastify-rate-limit

yarn add @fastify/rate-limit

src/routes/index.ts 파일을 수정합니다:

import rateLimit from '@fastify/rate-limit'

// ...

fastify.register(rateLimit, {
  ...(NODE_ENV === 'development' && {
    allowList: ['127.0.0.1'],
  }),
})

Fastify + JWT

https://github.com/fastify/fastify-jwt

yarn add @fastify/jwt

src/routes/index.ts 파일을 수정합니다:

import fastifyJWT from '@fastify/jwt'
import {
  // ...
  JWT_SECRET_KEY,
} from '../common/constants'

// ...

fastify.register(fastifyJWT, {
  secret: JWT_SECRET_KEY,
})

type QuerystringJWT = {
  Querystring: {
    jwt?: string
  }
}

fastify.addHook<QuerystringJWT>('onRequest', async (request, reply) => {
  const jwt = request.headers.authorization ?? request.query.jwt
  if (!jwt) return

  request.headers.authorization = jwt

  try {
    await request.jwtVerify()
  } catch (err) {
    reply.send(err)
  }
})

Fastify + Schema

https://www.fastify.io/docs/latest/Reference/Type-Providers/ \
https://github.com/sinclairzx81/typebox \
https://github.com/fastify/fastify-type-provider-typebox

yarn add @sinclair/typebox
yarn add --dev @fastify/type-provider-typebox

src/routes/index.ts 파일을 수정합니다:

import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
import { Type } from '@sinclair/typebox'

// ...

const fastify = Fastify({
  // ...
}).withTypeProvider<TypeBoxTypeProvider>()

const schema = {
  schema: {
    querystring: Type.Object({
      foo: Type.Optional(Type.Number()),
      bar: Type.Optional(Type.String()),
    }),
    response: {
      200: Type.Object({
        hello: Type.String(),
        foo: Type.Optional(Type.Number()),
        bar: Type.Optional(Type.String()),
      }),
    },
  },
}

fastify.get('/', schema, async (request, _) => {
  const { foo, bar } = request.query
  return { hello: 'world', foo, bar }
})

Fastify + Swagger

yarn add @fastify/swagger

Fastify + Google Cloud File uploader

https://github.com/fastify/fastify-multipart \
https://github.com/googleapis/nodejs-storage#readme \
https://cloud.google.com/storage/docs/reference/libraries#client-libraries-install-nodejs \
https://cloud.google.com/storage/docs/uploading-objects-from-memory \
https://cloud.google.com/storage/docs/streaming-uploads#code-samples

yarn add @fastify/multipart @google-cloud/storage

src/routes/index.ts 파일을 수정합니다:

import multipart from '@fastify/multipart'

// ..

fastify.register(multipart, {
  limits: {
    fileSize: 10_000_000,
    fieldSize: 1_000,
    files: 10,
  },
})

src/routes/upload.ts 파일을 생성합니다:

import { randomUUID } from 'crypto'
import path from 'path'

import { FastifyInstance } from 'fastify'

import { bucket } from '../common/google-storage'

type UploadResult = {
  fileName: string
  url: string
}

export default async function routes(fastify: FastifyInstance) {
  fastify.post('/upload/images', async function (request, reply) {
    // if (!request.userId) throw UnauthorizedError('로그인 후 시도해주세요')

    const files = request.files()
    const result: UploadResult[] = []

    for await (const file of files) {
      if (file.file) {
        // if (!file.mimetype.startsWith('image/'))
        //   throw BadRequestError('이미지 파일만 업로드할 수 있습니다')

        const timestamp = ~~(Date.now() / 1000)
        const fileExtension = path.extname(file.filename)
        const fileName = `${timestamp}-${randomUUID()}${fileExtension}`

        bucket
          .file(fileName)
          .save(await file.toBuffer())
          .then(() =>
            result.push({
              fileName: file.filename,
              url: `https://storage.googleapis.com/${bucket.name}/${fileName}`,
            })
          )
          .catch((error) => console.log(error))
      }
    }

    reply.status(201).send(result)
  })
}

src/common/constants.ts 파일을 수정합니다:

// ...

export const GOOGLE_CLOUD_STORAGE_BUCKET_NAME = process.env
  .GOOGLE_CLOUD_STORAGE_BUCKET_NAME as string

PostgreSQL

https://node-postgres.com/ \
https://pgtyped.vercel.app/docs/cli \
https://stackoverflow.com/a/20909045/16868717 \
https://github.com/brianc/node-postgres/issues/2089

yarn add pg
yarn add --dev @types/pg @pgtyped/cli @pgtyped/query

src/common/constants.ts 파일을 수정합니다:

// ...
export const PGURI = process.env.PGURI as string
export const POSTGRES_CA = process.env.POSTGRES_CA as string

src/common/postgres.ts 파일을 생성합니다:

import pg from 'pg'

import { PGURI, POSTGRES_CA, PROJECT_ENV } from '../common/constants'

const { Pool } = pg

export const pool = new Pool({
  connectionString: PGURI,

  ...((PROJECT_ENV === 'cloud-dev' ||
    PROJECT_ENV === 'cloud-prod' ||
    PROJECT_ENV === 'local-prod') && {
    ssl: {
      ca: `-----BEGIN CERTIFICATE-----\n${POSTGRES_CA}\n-----END CERTIFICATE-----`,
      checkServerIdentity: () => {
        return undefined
      },
    },
  }),
})

pgtyped.config.json 파일을 생성합니다:

{
  "transforms": [
    {
      "mode": "sql",
      "include": "**/*.sql",
      "emitTemplate": "{{dir}}/{{name}}.ts"
    }
  ],
  "srcDir": "src/"
}

package.json 파일을 수정합니다. dev 스크립트가 길어지기 때문에 아래처럼 sh 파일로 분리합니다:

{
  "scripts": {
    "dev": "src/dev.sh"
    // ...
  }
  // ...
}

src/dev.sh 파일을 생성합니다:

#!/bin/sh
export $(grep -v '^#' .env.local.dev | xargs) && pgtyped --watch --config pgtyped.config.json &
sleep 2 && NODE_ENV=development node esbuild.js &
sleep 2 && NODE_ENV=development nodemon -r dotenv/config out/index.cjs dotenv_config_path=.env.local.dev

PostgreSQL CSV

https://www.postgresqltutorial.com/postgresql-tutorial/export-postgresql-table-to-csv-file/ \
https://dba.stackexchange.com/q/137140 \
https://github.com/brianc/node-pg-copy-streams

yarn add pg-copy-streams
yarn add --dev @types/pg-copy-streams

database/index.ts 파일을 생성합니다:

import dotenv from 'dotenv'
import pg from 'pg'

const { Pool } = pg

// 환경 변수 설정
const env = process.argv[2]
export let CSV_PATH: string

if (env === 'prod') {
  dotenv.config()
  CSV_PATH = 'prod'
} else if (env === 'dev') {
  dotenv.config({ path: '.env.development' })
  CSV_PATH = 'dev'
} else {
  dotenv.config({ path: '.env.development.local' })
  CSV_PATH = 'local'
}

const PROJECT_ENV = process.env.PROJECT_ENV as string
const PGURI = process.env.PGURI as string
const POSTGRES_CA = process.env.POSTGRES_CA as string

if (!PROJECT_ENV) throw new Error('`PROJECT_ENV` 환경 변수를 설정해주세요.')
if (!PGURI) throw new Error('`PGURI` 환경 변수를 설정해주세요.')
if (!POSTGRES_CA) throw new Error('`POSTGRES_CA` 환경 변수를 설정해주세요.')

console.log(PGURI)

// PostgreSQL 서버 연결
export const pool = new Pool({
  connectionString: PGURI,

  ...((PROJECT_ENV === 'cloud-dev' ||
    PROJECT_ENV === 'cloud-prod' ||
    PROJECT_ENV === 'local-prod') && {
    ssl: {
      ca: `-----BEGIN CERTIFICATE-----\n${POSTGRES_CA}\n-----END CERTIFICATE-----`,
      checkServerIdentity: () => {
        return undefined
      },
    },
  }),
})

database/export.ts 파일을 생성합니다:

/* eslint-disable no-console */
import { createWriteStream, mkdirSync, rmSync } from 'fs'
import { exit } from 'process'

import pgCopy from 'pg-copy-streams'

import { CSV_PATH, pool } from './index.js'

const { to } = pgCopy

// 폴더 다시 만들기
rmSync(`database/data/${CSV_PATH}`, { recursive: true, force: true })
mkdirSync(`database/data/${CSV_PATH}`, { recursive: true })

let fileCount = 0

const client = await pool.connect()

const { rows } = await client.query('SELECT schema_name FROM information_schema.schemata')

for (const row of rows) {
  const schemaName = row.schema_name
  if (schemaName !== 'pg_catalog' && schemaName !== 'information_schema') {
    const { rows: rows2 } = await client.query(
      `SELECT tablename FROM pg_tables WHERE schemaname='${schemaName}'`
    )

    for (const row2 of rows2) {
      const tableName = row2.tablename
      fileCount += 1
      console.log(`👀 - ${schemaName}.${tableName}`)

      const csvPath = `database/data/${CSV_PATH}/${schemaName}.${tableName}.csv`
      const fileStream = createWriteStream(csvPath)

      const sql = `COPY ${schemaName}.${tableName} TO STDOUT WITH CSV DELIMITER ',' HEADER ENCODING 'UTF-8'`
      const stream = client.query(to(sql))
      stream.pipe(fileStream)

      stream.on('end', () => {
        fileCount -= 1
        if (fileCount === 0) {
          client.release()
          exit()
        }
      })
    }
  }
}

database/import.ts 파일을 생성합니다:

/* eslint-disable no-console */
import { createReadStream, readFileSync } from 'fs'
import { exit } from 'process'
import { createInterface } from 'readline'

import pgCopy from 'pg-copy-streams'

import { CSV_PATH, pool } from './index.js'

const { from } = pgCopy

const client = await pool.connect()

try {
  console.log('BEGIN')
  await client.query('BEGIN')

  const initialization = readFileSync('database/initialization.sql', 'utf8').toString()
  await client.query(initialization)

  // 테이블 생성 순서와 동일하게
  const tables = [
    // ...
  ]

  // GENERATED ALWAYS AS IDENTITY 컬럼이 있는 테이블
  const sequenceTables = [
    // ...
  ]

  for (const table of tables) {
    console.log('👀 - table', table)

    try {
      const csvPath = `database/data/${CSV_PATH}/${table}.csv`
      const columns = await readFirstLine(csvPath)
      const fileStream = createReadStream(csvPath)

      const sql = `COPY ${table}(${columns}) FROM STDIN WITH CSV DELIMITER ',' HEADER ENCODING 'UTF-8'`
      const stream = client.query(from(sql))
      fileStream.pipe(stream)
    } catch (error) {
      console.log('👀 - error', error)
    }
  }

  for (const sequenceTable of sequenceTables) {
    console.log('👀 - sequenceTable', sequenceTable)

    client.query(`LOCK TABLE ${sequenceTable} IN EXCLUSIVE MODE`)
    client.query(
      `SELECT setval(pg_get_serial_sequence('${sequenceTable}', 'id'), COALESCE((SELECT MAX(id)+1 FROM ${sequenceTable}), 1), false)`
    )
  }

  console.log('COMMIT')
  await client.query('COMMIT')
} catch (error) {
  console.log('ROLLBACK')
  await client.query('ROLLBACK')
  throw error
} finally {
  client.release()
}

exit()

// Utils
async function readFirstLine(path: string) {
  const inputStream = createReadStream(path)
  // eslint-disable-next-line no-unreachable-loop
  for await (const line of createInterface(inputStream)) return line
  inputStream.destroy()
}

database/tsconfig.json 파일을 생성합니다:

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2021"],
    "module": "ES2020",
    "moduleResolution": "node",
    "outDir": "dist",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["./"]
}

package.json 파일의 스크립트를 수정합니다:

{
  "scripts": {
    "export": "tsc --project database/tsconfig.json && node database/dist/export.js",
    "import": "tsc --project database/tsconfig.json && node database/dist/import.js"
    // ...
  }
  // ...
}

Docker

Docker Compose

OAuth

Jest ?

profile
이유와 방법을 알려주는 메모장 겸 블로그. 블로그 내용에 대한 토의나 질문은 언제나 환영합니다.

0개의 댓글