[Prisma] Prisma Timezone 설정하기

koline·2023년 12월 6일

prisma

목록 보기
6/6

Prisma Timezone 설정하기


Prisma를 사용할 때 가장 문제가 되었던 부분 중 하나는 Timezone을 지원하지 않는 점이다. 이 점 때문에 JavascriptDate 객체를 사용하여 시간을 전달하면 KST 시간(한국 표준시)이 아닌 UTC 시간(KTC - 9시간)이 저장된다. 만약 Database의 Timezone이 KST로 설정이 되어 있다면 다시 값을 가져올 때 UTC에 맞춰서 9시간이 더해져서 반환될 것이다. 즉 결과적으로 애플리케이션단에서 사용하는 시간 데이터 자체는 한국 표준시의 데이터겠지만 Database에 저장되는 시간은 9시간 이전인 UTC 시간이 저장된다.




해결법


1. 미들웨어 사용

Prisma client에 미들웨어를 적용하여 시간을 변경할 수 있다.

const prisma = new PrismaClient()

// Middleware 1
prisma.$use(async (params, next) => {
  // Manipulate params here
  const result = await next(params)
  // See results here
  return result
})

// Middleware 2
prisma.$use(async (params, next) => {
  // Manipulate params here
  const result = await next(params)
  // See results here
  return result
})

// Queries here

이 예시는 Prisma 공식 문서에 나와있는 미들웨어 생성법 예시이다.

model Member {
  id          Int        @id @default(autoincrement())
  username    String     @unique @db.VarChar(50)
  password    String     @db.VarChar(100)
  fullName    String     @db.VarChar(50)
  createdAt   DateTime   @db.DateTime
}

위와 같은 회원 model이 있을 때 아래와 같이 createdAtupdatedAt 속성을 커스텀 해줄 수 있다.

prisma.$use(async (params, next) => {
  switch (params.model) {
    case 'Member':
      switch (params.action) {
        case 'create':
          // create 될 때 9시간 더해서 한국 표준시로 insert
          const createdAt = params.args.data.createdAt;
          const newCreatedAt = createdAt.setHours(createdAt.getHours() + 9;
          params.args.data.createdAt = newCreatedAt;
          break;
        case 'findMany': case 'findOne':
		  // find 할 때 9시간 빼서 한국 표준시로 select
          const createdAt = params.args.data.createdAt;
          const newCreatedAt = createdAt.setHours(createdAt.getHours() - 9;
          params.args.data.createdAt = newCreatedAt;
          break;
      }
  }
  return next(params)
})

이런 방식으로 미들웨어를 통해 모델별, 속성별로 시간을 설정해줄 수 있다. 다만, 이 방식의 단점은 findMany, findUnique, findOne, create, update 등 모든 action에 대한 정의가 이뤄져야 한다.

또한 위 모델에서는 createdAt 속성만 사용했으나 여기에 updatedAt 속성이 추가되었을 경우, 또는 특정 모델에서 날짜와 시간을 사용하는 속성이 있을 경우 등 분기가 많아지면 코드가 복잡해진다는 단점이 있다.

그래서 database에 데이터를 입력하거나 수정할 때는 모델에 default값을 사용함으로써 코드의 양을 줄일 수 있다.

model Member {
  id          Int        @id @default(autoincrement())
  username    String     @unique @db.VarChar(50)
  password    String     @db.VarChar(100)
  fullName    String     @db.VarChar(50)
  createdAt   DateTime   @default(dbgenerated("NOW()")) @db.DateTime
  updatedAt   DateTime   @default(dbgenerated("NOW() ON UPDATE NOW()")) @db.DateTime
}

위와 같이 createdAt 속성과 updatedAt 속성에 자동으로 database가 NOW() 함수의 결과값을 입력하도록 하고 UPDATE 될 때마다 updatedAt속성을 NOW() 함수로 수정하도록 함으로써 값의 입력이 발생할 때는 Database의 timezone으로 시간을 입력하도록 설정할 수 있다.

prisma.$use(async (params, next) => {
  switch (params.action) {
    case 'findMany': case 'findOne': case 'findUnique':
      // find 할 때 9시간 빼서 한국 표준시로 select
      const createdAt = params.args.data.createdAt;
      const newCreatedAt = createdAt.setHours(createdAt.getHours() - 9;
      params.args.data.createdAt = newCreatedAt;
      break;
  }
  return next(params)
})

이 방식을 사용하면 이렇게 find 관련 action이 실행 될 때에 대한 처리만 해주면 되기 때문에 코드의 양이 매우 줄어든다. 하지만 속성이 추가되는 경우에 대한 처리는 여전히 일일히 해줘야 한다는 단점이 있다.

2. Custom Scalar 사용 (GraphQL)

두번째 방법은 GraphQL을 사용할 때 사용할 수 있는 방법이다. GraphQL의 데이터 모델링에는 커스텀 타입(Scalar)를 사용할 수 있는데, 이 Scalar의 정의에 시간을 9시간 빼주도록 설정하면 된다.

const { GraphQLScalarType, Kind } = require('graphql');

const formatDateTime = (datetime) => {

  // prisma timezone issue로 인해 9시간 빼줌
  // (db에서 받아올때 timezone 000Z로 받아와서 getDate시 한국 표준시로 9시간 더해짐)
  datetime.setHours(datetime.getHours()-9);

  const year = datetime.getFullYear();
  const month = String(datetime.getMonth() + 1).padStart(2, '0');
  const day = String(datetime.getDate()).padStart(2, '0');
  const hours = String(datetime.getHours()).padStart(2, '0');
  const minutes = String(datetime.getMinutes()).padStart(2, '0');
  const seconds = String(datetime.getSeconds()).padStart(2, '0');

  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}

const DATE_TIME_FORMAT = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;

const validateDateTime = (value) => {
  const formattedDateTime = formatDateTime(value);
  const isValidFormat = DATE_TIME_FORMAT.test(formattedDateTime);
  if (!isValidFormat) {
    throw new Error('Input value is not in a valid format');
  }
  return formattedDateTime;
};

const DateTimeScalar = new GraphQLScalarType({
  name: 'DateTime',
  description: 'DateTime Type',
  serialize(value) {
    return validateDateTime(value);
  },
  parseValue(value) {
    return validateDateTime(value);
  },
  parseLiteral(ast) {
    if (ast.kind === Kind.STRING) {
      return validateDateTime(ast.value);
    }
    return null;
  },
});

export default DateTimeScalar;

DateTimeScalar를 보면 serialize, parseValue, parseLiteral 3개의 함수가 정의 되어 있다. GraphQL 라이브러리인 Apollo공식문서의 설명은 다음과 같다.

serialize
serialize 메서드는 Apollo Server가 작업 응답에 이를 포함할 수 있도록 스칼라의 백엔드 표현을 JSON 호환 형식으로 변환합니다.

parseValue
ParseValue 메서드는 스칼라의 JSON 값을 Resolver의 인수(args)에 추가하기 전에 백엔드 표현으로 변환합니다.

parseLiteral
들어오는 쿼리 문자열에 하드 코딩된 인수 값으로 스칼라가 포함된 경우 해당 값은 쿼리 문서의 AST(추상 구문 트리)의 일부입니다. Apollo Server는 값의 AST 표현을 스칼라의 백엔드 표현으로 변환하기 위해parseLiteral 메서드를 호출합니다.

즉, serialize 메소드는 server가 요청을 받으면 JSON 으로 응답을 보내주기 위해 Date 객체와 같은 Javascript 타입 등 백엔드에서 사용하는 타입을 어떻게 JSON 호환 형식으로 변환할지에 대한 정의이고,

parseValue 메소드는 client로 부터 받은 요청의 데이터를 커스텀 스칼라가 어떻게 처리하여 받을 것인가에 대한 정의이고,

마지막으로 parseLiteral 메소드는 이 client로 부터 받은 데이터가 해당 스칼라가 요구하는 타입과 다를 경우에 대한 데이터 처리의 정의이다.




참고


[GraphQL] GraphQL이란?

[GraphQL] Schema 등록하기

[GraphQL] Apollo란?

profile
개발공부를해보자

0개의 댓글