최근 Prisma 2에 대해 관심이 있고 조금씩 사용해보는 중이어서 이를 이용한 boilerplate 를 찾아보던 중, 노마드코더의 프로젝트들 중 아래의 인스타그램 클론개발 Repos.를 주의깊게 살펴봤다.
여기서 Backend는 Prisma v1 및 GraphQL 을 이용하고있는데, Prisma는 현재 버전 2가 나와있어서 그냥 한번 Prisma 2를 이용하도록 수정해보고 싶었고. Frontend는 apollo-boost와 React 를 이용하고 있는데, Vue의 Apollo 플러그인인 vue-apollo 를 이용하여 구현해보고싶어져서 틈틈히 취미삼아 해보았음.
1. Prisma 2를 이용한 Backend 구현
기존의 노마드코더의 Prismagram Backend를 Fork하여 Prisma 2를 사용하도록 수정했다.
최종 결과물은 여기(https://github.com/heonie/prismagram2)
Prisma 1 서버 아키텍쳐
Prisma 1과 2의 가장 큰 차이점은 별도의 Prisma Server 가 존재하는지의 여부로 보인다.
그 외 차이점에 대해서는 여기(https://www.prisma.io/blog/announcing-prisma-2-zq1s745db8i5) 와 여기(https://gmyankee.tistory.com/265?category=1084683) 에 잘 정리해놓은듯.
기존 Prisma 1에서는 한 서버를 만들기 위해 최소 두 가지 서버를 구축하도록 요구하는데, DB의 일종의 프록시 역할을 하는 Prisma Server와 실제 API를 제공하기 위한 API Server(서버 어플리케이션)가 그것이다.
- Prisma Server: 데이터 구조를 정의하는 Prisma 고유 포맷인 Datamodel 파일을 분석하고 이를 DB에 반영한다(Migrate:
prisma deploy). DB에 직접 커넥션을 맺고 통신하면서 Datamodel을 기반으로 DB에 쿼리와 트랜젝션을 날려주는 GraphQL API를 노출한다. (기존에 이미 구조를 가진 DB에서 Datamodel을 추출하려면prisma init또는prisma introspect)
- API Server(서버 애플리케이션): Frontend에 노출될 API (GraphQL, REST API 등)를 제공하고 API 를 통해 Frontend의 요청을 받아DB 에 필요한 처리를 하기위해 Prisma Client 를 통해 Prisma Server와 통신한다. Prisma client는 Datamodel 변경시마다
prisma generate를 통해 생성한다.
Prisma 1의 Datamodel에 대한 내용은 여기(https://v1.prisma.io/docs/1.34/datamodel-and-migrations/datamodel-MYSQL-knul/)를 참조.
Prisma 2 에서 바뀐점
Prisma 2에서는 Prisma Server의 역할을 별도의 라이브러리로 분리하고 이를 Prisma CLI와 (개발할 서버 애플리케이션에서 참조하는)Prisma Client 에서 수행하도록 변경되었다.
또한 Prisma 1에서
prisma.yml, dataschema.prisma 등으로 정의하던 스키마는, Prisma 2에서 schema.prisma 로 통합되었고 약간 확장/변형 되었다.DB구조를 반영하여 생성된 Prisma Client 라이브러리를 이용하여 DB 를 조작한다는 서버 애플리케이션에서의 개발 방법은 그대로이지만, Prisma Client 의 API가 버전 1과 2간에 조금씩 바뀐점이 있어서 이 부분에 대해 주의가 필요하다.
당연히 CLI 도 완전히 바뀌었지만, 실행 명령어는 prisma 로 동일하기 때문에, 기존에 Prisma 1의 CLI 를 전역으로 설치해두었다면 새로 설치하는것이 필요하다.
npm i @prisma/cli --save-dev 로 프로젝트 로컬에 설치하고 npx prisma로 사용하면 될듯.Prisma 2 DB 설정, 테이블 생성
공식 문서에서는 새로운 DB 를 설정하여 Prisma 를 이용하는 경우,
prisma init을 통해 기본적인 DB접속정보만을 설정하고
- DB에
CREATE TABLE...을 이용하여 테이블 생성 후
prisma introspect로 DB 스키마를schema.prisma에 반영
하는 방식을 기본으로 가이드하고 있다. 하지만 나는 SQL을 사용하지 않고 하는 방식이 더 마음에 들어서,
prisma init을 통해 기본적인 DB접속정보 설정,
schema.prisma에 Prisma Schema 를 이용하여 model 구조 정의 (레퍼런스를 어느정도 숙지 필요)
prisma migrate save로 DB 스키마 업데이트 할 내용을 로컬에 생성하고
prisma migrate up으로 DB 에 테이블 생성
하는 방식으로 초기화 하였다.
여기서 좀 귀찮은점은, 아직 Prisma 2의 (schema) migrate 기능은 Experimental이기때문에 뒤에
--experimental 을 붙여서 해줘야 한다는 점과, migrate 기능을 이용하면 생성되는 migration 파일들이 관리하기 어렵다는 점. (Django에서도 비슷한 일을 겪은 바 있음=ㅅ=)기존의 Prisma 1의
datamode.prisma에서 Prisma 2의 schema.prisma로 변경하면서 주로 변경한 부분은 아래와 같다.- 기존에는 GraphQL의 타입 정의에 몇가지 directive를 추가하는 방식이었지만, Prisma 2에서는 그와 비슷하지만 고유의 양식으로 바뀌었다. “type” 은 “model”로, 필드명과 타입 사이에 “:”이 없어진것 등.
- 필드의 타입 정의시, Prisma 1에서는
String과String?같이 NOT NULL을 기본으로 하고 있지만, Prisma 2에서는 반대로String!과String처럼 기본이 Nullable이다.
- 기존에 String타입의 CUID를 자동으로 생성해주던 ID 타입이 없어졌고,
Int나String타입으로 선언해야 한다. Integer 타입으로 할 경우에는id Int @id @default(autoincrement())와 같이 선언하거나, String타입의 CUID 를 생성하도록 하고싶다면,id String @id @default(cuid())와 같이 선언해야 한다. 여기서 cuid()는 Prisma Client에 의해 삽입시에 생성된다.
- @createdAt 은 없어졌다.
createdAt DateTime @default(now())와 같이 현재 시간을 자동으로 넣도록 정의 필요하다.
- @updatedAt 은 남아있다.
updatedAt DateTime @updatedAt과 같이 정의하면 된다.
- Relation 정의는 조금 더 명확하게 정의하도록 바뀌었다. 공식문서를 보고 잘 정의할 필요가 있음.
어찌됐든 위와 같이 schema.prisma를 생성하여 DB 를 초기화했다. DB 는 그냥 sqlite를 쓰기로… =ㅅ=
Prisma 2의 Prisma Client 를 사용하도록 변경
기존의 Prismagram backend에서 prisma client 를 사용하던 부분을 찾아다니며 Prisma 2의 클라이언트를 사용하도록변경하였다. 아래와 같이 수정 필요하다.
Prisma client import path 수정
Prisma Client 는 Prisma 스키마가 변결될 때 마다
prisma generate 를 통해 다시 생성해주어야 한다.(Prisma 1과 동일)Prisma 1에서는
prisma generate를 실행하면 프로젝트 로컬에 클라이언트 라이브러리가 생성되었으나 Prisma 2는 node_modules/.prisma/client에 생성되어 @prisma/client 의 경로로 PrismaClient생성자를 import 할 수 있다.import { PrismaClient } from '@prisma/client' const prisma = new PrismClient();
Prisma Client 사용방법 변경
기존 prisma는 하나의 unitque한 row를 얻을땐
const user = prisma.user({id}); , 여러개를 얻을 땐 const users = prisma.users({where: {username_contains: args.term}});와 같이, type의 단수 복수형을 이용하도록 네이밍 되어있었고, arguments도 where 필드가 필요하다 안하다 하는 모습이었으다.Prisma 2에서는 좀 더 명확하고 일관성이 생긴 느낌이다.
const user = prisma.users.findOne({where: {id}}); 와 const users = prisma.users.findMany({where: {username: {contain: args.term}}}); 와 같은 식이다.어쨌든 이 또한 Docs를 보고 수정하였다. 그 외 자세한 내용은 Github의 Repos. 참고.
Subscription 구현
Prisma 1에서
prisma.$subscribe를 통해 지원하던 subscription은 Prisma 2에서 더이상 지원하지 않는다. 따라서, Apollo 에서 GraphQL의 subscription을 지원하는 방법인 graphql-subscription을 이용한 PubSub방식으로 구현하였다. (참고: https://github.com/apollographql/graphql-subscriptions)GraphQLServer 에서 context로 PubSub 객체를 생성하여 넘겨주어 공유하도록 하고,
import { GraphQLServer, PubSub } from "graphql-yoga"; ... const pubsub = new PubSub(); const server = new GraphQLServer({ schema, context: ({ request }) => ({ request, isAuthenticated, prisma, pubsub }) }); ...
새로운 메시지에 대한 subscribe resolver 에서, 특정 채널의 pubsub AsyncIterator를 리턴하도록 아래와 같이 구현,
import {CHANNEL_NEW_MESSAGE} from "../../../Constants"; import { withFilter } from 'graphql-subscriptions'; export default { Subscription: { newMessage: { subscribe: withFilter( (_, args, {pubsub}) => pubsub.asyncIterator(CHANNEL_NEW_MESSAGE), (payload, {roomId}) => payload.roomId === roomId ), resolve: (payload) => payload.newMessage } } }
새로운 Message 가 추가되었을때 또한 pubsub으로 publish 하도록 아래와 같이 추가하였다.
import {CHANNEL_NEW_MESSAGE} from "../../../Constants"; export default { Mutation: { sendMessage: async (_, args, {request, isAuthenticated, prisma, pubsub}) => { ... const message = await prisma.message.create({ data: msgData }); pubsub.publish(CHANNEL_NEW_MESSAGE, { newMessage: message, roomId: room.id }); return message; } } }
여기까지 진행해서 기존의 Prisma 1을 사용하던 Prisma Backend를 Prisma 2를 이용하도록 바꿀 수 있었다.
어쨌거나 서버를 하나 덜 돌려도 된다는점은 백엔드단의 구조를 단순화 하는데 도움이 되는 것 같고, Prisma 2가 MongoDB를 아직 지원하지 않음이 아쉽긴 하지만, 서비스의 프로토타이핑을 하는데에 Prisma를 이용한 GraphQL 서버 구축을 종종 사용하게 될 듯.
Prisma의 기존의 typeorm이나 sequelize 같은 기존 ORM대비 장점은 개인적으로는 아직은 와 닿지 않는다.