Typescript, Graphql이 포함된 FullStack Nextjs

전단과 후단을 갖춘 완전한 창고 프로젝트를 세우는 것은 정말 고통스럽고 시간이 필요하다.최소한의 노력으로 시작하고 실행하는 데 도움을 줄 입문 프로젝트가 있어서 기쁩니다.따라서 본고에서 우리는 처음부터 완전한 창고 웹 응용 프로그램, 즉 typesafe과 사용graphql 을 구축하는 방법을 배울 것이다.
그렇다면 이 문장에서 우리는 어떤 기술을 사용할 것인가.
  • Typescript
  • Nextjs
  • Prisma
  • Nexus

  • PostgreSQL
  • 이것은 우리가 사용할 주요 기술이다.
    최종 결과를 보고 싶으면 바로 이것을 보세요repo

    Nextjs 프로젝트 만들기


    Nextjs 프로젝트를 만들려면 다음 명령을 실행하십시오npx create-next-app full-stack-nextjs --use-npm -e with-typescript npx는 npm 등록표에서 위탁 관리하는 의존항을 설치하고 관리하기 위한 CLI 도구이다.create-next-app은nextjs 프로젝트를 만들고 모든 의존 항목을 설치하는 도구입니다.full-stack-nextjs는 우리 프로젝트의 이름입니다.아니면, 당신은 당신의 프로젝트에 당신이 좋아하는 이름을 지어줄 수 있습니다.--use-npm 기본 패키지 관리자 사용npm-e 정확한 npm 패키지에 사용with-typescript 이 프로젝트는 typescript를 사용하여 미리 설정됩니다.
    기타 소프트웨어 패키지cd을(를) full-stack-nextjs로 변환하고 다음 명령을 실행하여 다른 패키지를 설치합니다.npm install @nexus/schema nexus-prisma apollo-server-micro @apollo/react-hooks apollo-client apollo-cache-inmemory @apollo/react-ssr apollo-link-http apollo-link-schema ts-node graphql graphql-tag express @prisma/cli @prisma/client --save열기tsconfig.json 모든 내용을 삭제하고 다음 코드를 붙여넣기
    {
      "compilerOptions": {
        /* 
          Note that the "module" setting will be overriden by nextjs automatically
          (cf. https://github.com/zeit/next.js/discussions/10780).
          If you need to change it, you should use the --compiler-options or provide a separate 
          tsconfig.json entirely.
        */
        "module": "esnext",
        "target": "ES2019",
        "lib": [
          "dom",
          "dom.iterable",
          "esnext"
        ],
        "allowJs": true,
        "skipLibCheck": true,
        "strict": false,
        "forceConsistentCasingInFileNames": true,
        "noEmit": true,
        "esModuleInterop": true,
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "isolatedModules": true,
        "jsx": "preserve"
      },
      "exclude": [
        "node_modules"
      ],
      "include": [
        "next-env.d.ts",
        "**/*.ts",
        "**/*.tsx"
      ]
    }
    
    더 알고 싶으면checkout this repo
    폴더 구조는 다음과 같습니다.만약 다르다면, 걱정하지 마라. 왜냐하면 우리는 어쨌든 대부분의 파일을 삭제할 것이다.
    components/
        Layout.tsx              --> remove this file
        List.tsx                --> remove this file
        ListDetail.tsx          --> remove this file
        ListItem.tsx            --> remove this file
    interface/
        index.tsx
    pages/
        api/
            users/              --> remove this folder
                index.tsx       
        users/                  --> remove this folder
            [id].tsx
            index.tsx 
        about.tsx               --> remove this file
        index.tsx
    utils/                      --> remove this folder
    
    
    파일 및 폴더 삭제 후 업데이트pages/index.tsx
    const IndexPage = () => (
      <>
        <h1>Hello Next.js 👋</h1>
      </>
    );
    
    export default IndexPage;
    
    
    폴더 구조는 다음과 같습니다.

    현재 실행 npm run dev 및 이동 localhost
    브라우저에서 유사한 내용을 보셔야 합니다

    우리의 응용 프로그램 전단은 이미 준비가 다 되었다.이제 백엔드를 만듭니다.

    API 라우팅


    Nextjs Api Routes는 Next를 사용하여 API를 구축하는 간단한 솔루션을 제공합니다.회사 명
    폴더pages/api의 모든 파일은 /api/* 에 매핑되며 page 대신 API 노드로 간주됩니다.서버 측 번들일 뿐 클라이언트 번들 크기는 증가하지 않습니다.
    우리는 이미 pages/api 목록을 가지고 있다.우리의 백엔드에는 별도의 작업 환경이 필요하지 않다.
    시작합시다prisma

    프리스마


    Prisma 은 소스 데이터베이스 도구 패키지입니다.
    모든 패키지를 설치하지 않은 경우 다음 명령을 실행하여 Prisma 클라이언트를 설치하십시오.npm install @prisma/cli @prisma/client --save-dev설치 후 다음 명령을 통해prisma를 초기화합니다npx prisma init상기 명령prisma을 실행하면 프로젝트의 루트 디렉터리에 디렉터리를 만듭니다. 이 디렉터리는 두 개의 init 파일을 포함합니다..evn 환경 변수에 사용됨 .gitignoreschema.prisma 우리prisma 모드에 사용.env 파일
    DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
    
    이제 자신의 데이터베이스를 가리키기 위해 연결 URL을 조정해야 합니다postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA
  • USER: 데이터베이스 사용자 이름
  • PASSWORD: 데이터베이스 사용자의 암호
  • PORT: 데이터베이스 서버에서 실행되는 포트(PostgreSQL의 경우 일반적5432
  • DATABASE: database의 이름
  • SCHEMA: 데이터베이스
  • 의 이름
    이 예에서, 나는 로컬 데이터베이스를 사용할 것이다.
    shcema.prisma 파일
    datasource db {
      provider = "postgresql"   //Database Alternatively you can use MySQL or SQLite 
      url      = env("DATABASE_URL") // url from .env file
    }
    
    generator client {
      provider = "prisma-client-js"  // To Genetate prisma client
    }
    
    
    Prisma 모드 추가
    datasource db {
      provider = "postgresql"   //Database Alternatively you can use MySQL or SQLite 
      url      = env("DATABASE_URL") // url from .env file
    }
    
    generator client {
      provider = "prisma-client-js"  // To Genetate prisma client
    }
    
    // Add Two Model User and Post
    model User {
      email    String  @unique
      password String
      id       Int     @default(autoincrement()) @id
      name     String?
      posts    Post[]
    }
    
    model Post {
      authorId  Int?
      content   String?
      id        Int     @default(autoincrement()) @id
      published Boolean @default(false)
      title     String
      author    User?   @relation(fields: [authorId], references: [id])
    }
    
    
    스크립트를 추가하십시오package.json
    "generate": "npm -s run generate:prisma && npm -s run generate:nexus",
    "dev:migrate": "prisma2 migrate save --experimental -c && prisma2 migrate up --experimental -c",
    "generate:prisma": "prisma generate",
    "generate:nexus": "ts-node --transpile-only -P nexus.tsconfig.json pages/api"
    
    schema
    현재 실행 npm run dev:migrate너는 이런 것을 보아야 한다

    * 유사한 컨텐츠를 보지 못하고 오류 메시지가 표시되면 데이터베이스 자격 증명이 제대로 추가되었는지 확인* 추가 정보를 찾을 수 있습니다
    우리의 모형을 직관적으로 보다
    실행npx prisma studio 및 액세스http://localhost:5555/프로젝트가 새 파일을 만드는 경우 루트 디렉토리nexus.tsconfig.json
    {
      /* 
        This file is used as a workaround for https://github.com/graphql-nexus/schema/issues/391
        It allows the nexus schema generation to work (done via `npm run generate:nexus`).
      */
      "compilerOptions": {
        "sourceMap": true,
        "outDir": "dist",
        "strict": true,
        "lib": ["esnext"],
        "esModuleInterop": true
      }
    }
    

    여기 있다 백엔드 서버

    pages/api에서 새 파일 만들기index.ts서버를 만듭니다.
    서버를 만들려면 설치되지 않은 경우apollo-server-micro를 사용합니다npm install apollo-server-micro.
    참고: 붙여넣기를 복사하는 경우 복사하지 마십시오.네가 복사해야 할 때 나는 쪽지를 남길 것이다
    import { ApolloServer } from 'apollo-server-micro'
    
    const server = new ApolloServer();
    
      export default server.createHandler({
        path: "/api",
      });
    
    
    하지만 우리 아폴로 서버는 패턴이 필요해.하나 만듭니다.
    같은 파일에 다음 코드 추가
    import { makeSchema } from "@nexus/schema";
    import path from "path";
    
    const schema = makeSchema({
      types: [], // we will create types later
      outputs: {
        typegen: path.join(process.cwd(), "pages", "api", "nexus-typegen.ts"),
        schema: path.join(process.cwd(), "pages", "api", "schema.graphql"),
      },
    });
    
    
    생성TypeDefsprisma 가져오기@prisma/client
    import { PrismaClient } from '@prisma/client'
    const prisma = new PrismaClient()
    
    사용자 및 게시 모델 유형
    const User = objectType({
      name: 'User',
      definition(t) {
        t.int('id')
        t.string('name')
        t.string('email')
        t.list.field('posts', {
          type: 'Post',
          resolve: parent =>
            prisma.user
              .findOne({
                where: { id: Number(parent.id) },
              })
              .posts(),
        })
      },
    })
    
    const Post = objectType({
      name: 'Post',
      definition(t) {
        t.int('id')
        t.string('title')
        t.string('content', {
          nullable: true,
        })
        t.boolean('published')
        t.field('author', {
          type: 'User',
          nullable: true,
          resolve: parent =>
            prisma.post
              .findOne({
                where: { id: Number(parent.id) },
              })
              .author(),
        })
      },
    })
    
    * 변경 및 질의*
    
    
    const Query = objectType({
      name: 'Query',
      definition(t) {
        t.field('post', {
          type: 'Post',
          args: {
            postId: stringArg({ nullable: false }),
          },
          resolve: (_, args) => {
            return prisma.post.findOne({
              where: { id: Number(args.postId) },
            })
          },
        })
    
        t.list.field('feed', {
          type: 'Post',
          resolve: (_parent, _args, ctx) => {
            return prisma.post.findMany({
              where: { published: true },
            })
          },
        })
    
        t.list.field('drafts', {
          type: 'Post',
          resolve: (_parent, _args, ctx) => {
            return prisma.post.findMany({
              where: { published: false },
            })
          },
        })
    
        t.list.field('filterPosts', {
          type: 'Post',
          args: {
            searchString: stringArg({ nullable: true }),
          },
          resolve: (_, { searchString }, ctx) => {
            return prisma.post.findMany({
              where: {
                OR: [
                  { title: { contains: searchString } },
                  { content: { contains: searchString } },
                ],
              },
            })
          },
        })
      },
    })
    
    
    const Mutation = objectType({
      name: "Mutation",
      definition(t) {
        t.field("signupUser", {
          type: "User",
          args: {
            name: stringArg(),
            email: stringArg({ nullable: false }),
            password: stringArg({ nullable: false }),
          },
          resolve: (_, { name, email, password }, ctx) => {
            return prisma.user.create({
              data: {
                name,
                email,
                password,
              },
            });
          },
        });
    
        t.field("deletePost", {
          type: "Post",
          nullable: true,
          args: {
            postId: stringArg(),
          },
          resolve: (_, { postId }, ctx) => {
            return prisma.post.delete({
              where: { id: Number(postId) },
            });
          },
        });
    
        t.field("createDraft", {
          type: "Post",
          args: {
            title: stringArg({ nullable: false }),
            content: stringArg(),
            authorEmail: stringArg(),
          },
          resolve: (_, { title, content, authorEmail }, ctx) => {
            return prisma.post.create({
              data: {
                title,
                content,
                published: false,
                author: {
                  connect: { email: authorEmail },
                },
              },
            });
          },
        });
    
        t.field("publish", {
          type: "Post",
          nullable: true,
          args: {
            postId: stringArg(),
          },
          resolve: (_, { postId }, ctx) => {
            return prisma.post.update({
              where: { id: Number(postId) },
              data: { published: true },
            });
          },
        });
      },
    });
    
    유형을 우리 모드로 전달
    
    const schema = makeSchema({
      types: [Query, Mutation, Post, User],
      outputs: {
        typegen: path.join(process.cwd(), "pages", "api", "nexus-typegen.ts"),
        schema: path.join(process.cwd(), "pages", "api", "schema.graphql"),
      },
    });
    
    지금 너의 서류는 이렇게 될 것이다
    참고: 이 코드를 복사하여 서버에 붙여넣을 수 있습니다.ts 파일
    import { makeSchema, objectType, stringArg } from "@nexus/schema";
    import { PrismaClient } from "@prisma/client";
    import { ApolloServer } from "apollo-server-micro";
    import path from "path";
    
    const prisma = new PrismaClient();
    
    const User = objectType({
      name: "User",
      definition(t) {
        t.int("id");
        t.string("name");
        t.string("email");
        t.list.field("posts", {
          type: "Post",
          resolve: (parent) =>
            prisma.user
              .findOne({
                where: { id: Number(parent.id) },
              })
              .posts(),
        });
      },
    });
    
    const Post = objectType({
      name: "Post",
      definition(t) {
        t.int("id");
        t.string("title");
        t.string("content", {
          nullable: true,
        });
        t.boolean("published");
        t.field("author", {
          type: "User",
          nullable: true,
          resolve: (parent) =>
            prisma.post
              .findOne({
                where: { id: Number(parent.id) },
              })
              .author(),
        });
      },
    });
    
    const Query = objectType({
      name: "Query",
      definition(t) {
        t.field("post", {
          type: "Post",
          args: {
            postId: stringArg({ nullable: false }),
          },
          resolve: (_, args) => {
            return prisma.post.findOne({
              where: { id: Number(args.postId) },
            });
          },
        });
    
        t.list.field("feed", {
          type: "Post",
          resolve: (_parent, _args, ctx) => {
            return prisma.post.findMany({
              where: { published: true },
            });
          },
        });
    
        t.list.field("drafts", {
          type: "Post",
          resolve: (_parent, _args, ctx) => {
            return prisma.post.findMany({
              where: { published: false },
            });
          },
        });
    
        t.list.field("filterPosts", {
          type: "Post",
          args: {
            searchString: stringArg({ nullable: true }),
          },
          resolve: (_, { searchString }, ctx) => {
            return prisma.post.findMany({
              where: {
                OR: [
                  { title: { contains: searchString } },
                  { content: { contains: searchString } },
                ],
              },
            });
          },
        });
      },
    });
    
    const Mutation = objectType({
      name: "Mutation",
      definition(t) {
        t.field("signupUser", {
          type: "User",
          args: {
            name: stringArg(),
            email: stringArg({ nullable: false }),
            password: stringArg({ nullable: false }),
          },
          resolve: (_, { name, email, password }, ctx) => {
            return prisma.user.create({
              data: {
                name,
                email,
                password,
              },
            });
          },
        });
    
        t.field("deletePost", {
          type: "Post",
          nullable: true,
          args: {
            postId: stringArg(),
          },
          resolve: (_, { postId }, ctx) => {
            return prisma.post.delete({
              where: { id: Number(postId) },
            });
          },
        });
    
        t.field("createDraft", {
          type: "Post",
          args: {
            title: stringArg({ nullable: false }),
            content: stringArg(),
            authorEmail: stringArg(),
          },
          resolve: (_, { title, content, authorEmail }, ctx) => {
            return prisma.post.create({
              data: {
                title,
                content,
                published: false,
                author: {
                  connect: { email: authorEmail },
                },
              },
            });
          },
        });
    
        t.field("publish", {
          type: "Post",
          nullable: true,
          args: {
            postId: stringArg(),
          },
          resolve: (_, { postId }, ctx) => {
            return prisma.post.update({
              where: { id: Number(postId) },
              data: { published: true },
            });
          },
        });
      },
    });
    
    export const schema = makeSchema({
      types: [Query, Mutation, Post, User],
      outputs: {
        typegen: path.join(process.cwd(), "pages", "api", "nexus-typegen.ts"),
        schema: path.join(process.cwd(), "pages", "api", "schema.graphql"),
      },
    });
    
    export const config = {
      api: {
        bodyParser: false,
      },
    };
    
    export default new ApolloServer({ schema }).createHandler({
      path: "/api",
    });
    
    
    

    Apollo 클라이언트를 통해 백엔드를 프런트엔드에 연결


    우리 프로젝트의 루트 디렉터리에 새 파일 apollo/clinet.js 을 만들고 다음 코드를 붙여넣습니다.
    참고: 이러한 패키지가 필요합니다@apollo/react-hooks apollo-client apollo-cache-inmemory @apollo/react-ssr apollo-link-http apollo-link-schema
    import React from 'react'
    import Head from 'next/head'
    import { ApolloProvider } from '@apollo/react-hooks'
    import { ApolloClient } from 'apollo-client'
    import { InMemoryCache } from 'apollo-cache-inmemory'
    
    let apolloClient = null
    
    /**
     * Creates and provides the apolloContext
     * to a next.js PageTree. Use it by wrapping
     * your PageComponent via HOC pattern.
     * @param {Function|Class} PageComponent
     * @param {Object} [config]
     * @param {Boolean} [config.ssr=true]
     */
    export function withApollo(PageComponent, { ssr = true } = {}) {
      const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
        const client = apolloClient || initApolloClient(apolloState)
        return (
          <ApolloProvider client={client}>
            <PageComponent {...pageProps} />
          </ApolloProvider>
        )
      }
    
      // Set the correct displayName in development
      if (process.env.NODE_ENV !== 'production') {
        const displayName =
          PageComponent.displayName || PageComponent.name || 'Component'
    
        if (displayName === 'App') {
          console.warn('This withApollo HOC only works with PageComponents.')
        }
    
        WithApollo.displayName = `withApollo(${displayName})`
      }
    
      if (ssr || PageComponent.getInitialProps) {
        WithApollo.getInitialProps = async ctx => {
          const { AppTree } = ctx
    
          // Initialize ApolloClient, add it to the ctx object so
          // we can use it in `PageComponent.getInitialProp`.
          const apolloClient = (ctx.apolloClient = initApolloClient())
    
          // Run wrapped getInitialProps methods
          let pageProps = {}
          if (PageComponent.getInitialProps) {
            pageProps = await PageComponent.getInitialProps(ctx)
          }
    
          // Only on the server:
          if (typeof window === 'undefined') {
            // When redirecting, the response is finished.
            // No point in continuing to render
            if (ctx.res && ctx.res.finished) {
              return pageProps
            }
    
            // Only if ssr is enabled
            if (ssr) {
              try {
                // Run all GraphQL queries
                const { getDataFromTree } = await import('@apollo/react-ssr')
                await getDataFromTree(
                  <AppTree
                    pageProps={{
                      ...pageProps,
                      apolloClient,
                    }}
                  />
                )
              } catch (error) {
                // Prevent Apollo Client GraphQL errors from crashing SSR.
                // Handle them in components via the data.error prop:
                // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
                console.error('Error while running `getDataFromTree`', error)
              }
    
              // getDataFromTree does not call componentWillUnmount
              // head side effect therefore need to be cleared manually
              Head.rewind()
            }
          }
    
          // Extract query data from the Apollo store
          const apolloState = apolloClient.cache.extract()
          return {
            ...pageProps,
            apolloState,
          }
        }
      }
    
      return WithApollo
    }
    
    /**
     * Always creates a new apollo client on the server
     * Creates or reuses apollo client in the browser.
     * @param  {Object} initialState
     */
    function initApolloClient(initialState) {
      // Make sure to create a new client for every server-side request so that data
      // isn't shared between connections (which would be bad)
      if (typeof window === 'undefined') {
        return createApolloClient(initialState)
      }
    
      // Reuse client on the client-side
      if (!apolloClient) {
        apolloClient = createApolloClient(initialState)
      }
    
      return apolloClient
    }
    
    /**
     * Creates and configures the ApolloClient
     * @param  {Object} [initialState={}]
     */
    function createApolloClient(initialState = {}) {
      const ssrMode = typeof window === 'undefined'
      const cache = new InMemoryCache().restore(initialState)
    
      return new ApolloClient({
        ssrMode,
        link: createIsomorphLink(),
        cache,
      })
    }
    
    function createIsomorphLink() {
      const { HttpLink } = require('apollo-link-http')
      return new HttpLink({
        uri: 'http://localhost:3000/api',
        credentials: 'same-origin',
      })
    }
    
    
    이제 pages/index.ts 로 이동하여 가져오기WithApollo
    import { withApollo } from "../apollo/client";
    
    const IndexPage = () => (
      <>
        <h1>Hello Next.js 👋</h1>
      </>
    );
    
    export default withApollo(IndexPage);
    
    
    우리 package.json 에는 generate 라는 스크립트가 있습니다."generate": "npm -s run generate:prisma && npm -s run generate:nexus",이 명령은 생성 유형과 패턴을 책임집니다.
    이 명령을 실행하면 pages/api nexus-typegen.tsschema.graphql 에서 두 개의 파일을 볼 수 있습니다
    이제 계속합시다http://localhost:3000/api
    여기 있습니다.너는 창고 응용 프로그램을 구축하기 위해 이 프로젝트를 계속할 수 있다.
    다음 글에서, 이 흐름을 사용하여 신분 검증을 할 수 있음을 보여 드리겠습니다.

    좋은 웹페이지 즐겨찾기