[ React ] 클라이언트-서버 분리& ApolloServer Schema, Resolver & Unexpected end of JSON , Unknown file extension ".ts" 에러 해결

CJY00N·2023년 7월 14일
0

react

목록 보기
6/10
post-thumbnail

⚡️ 클라이언트와 서버 분리 (모노레포)

모노레포

: 여러 개의 프로젝트를 단일 코드베이스 내에 저장하는 구조

현재까지 진행한 프로젝트 파일을 client와 server를 따로 두어 분리할 것이다.

client workspace

- 먼저, 현재 까지의 파일들을 모두 client 디렉토리에 넣는다.
- package.json 파일의 name 속성을 client로 변경한다.
- "name": "client",

루트 프로젝트

- 루트 프로젝트 경로에서 yarn init -y를 입력하고, package.json 파일이 생긴 것을 확인한다.
- package.json에 아래를 추가한다.

  "private": true,
  "workspaces": [
    "client",
    "server"
  ],
  "scripts": {
    "client": "yarn workspace client run dev",
    "server": "yarn workspace server run dev"
  }

- client를 실행할 땐 yarn client로 실행하면 된다.

server workspace

- 루트 프로젝트 경로에서 server 디렉토리를 생성한 후, yarn init -y를 입력한다.
- 필요한 dependency를 설치하면 된다.
- express, apollo-server, apollo-server-express, graphql, uuid, nodemon(dev) 를 설치할 것이다.
yarn add express apollo-server apollo-server-express graphql uuid
yarn add --dev nodemon ts-node typescript @types/node @types/uuid
- package.json에 "type":"module", "scripts": { "dev": "nodemon --exec 'ts-node .src/index.ts'" } }를 추가한다.

⚡️ Apollo server

https://www.apollographql.com/docs/apollo-server/
- Apollo Server는 GraphQL API를 만드는데 사용되는 커뮤니티 주도의 오픈 소스 서버이다.
- Apollo Server를 사용하면 SQL, REST, NoSQL 또는 다른 유형의 백엔드 데이터 소스에서 데이터를 가져와 사용자 정의 API를 생성할 수 있다.

  • 스키마 (Scheme)
    : GraphQL 스키마는 API의 모양을 정의하며, API를 통해 어떤 종류의 객체를 가져올 수 있는지, 그 객체들이 어떤 필드를 가지는지를 명시하고, 스키마는 가능한 모든 요청 타입과 반환 타입을 명시한다.
  • 리졸버(Resolver)
    : 실제 데이터를 반환하는 함수이며, 리졸버는 스키마에 정의된 필드와 연결되며, 해당 필드가 요청될 때 호출된다.
  • server/src에 resovers와 schema 디렉토리를 각각 추가한다.

스키마 정의하기

product 스키마 정의

- client에서 정의한 Product type을 참고하여 gql의 문법에 맞게 작성한다.
▼ server/src/schema/product.ts

const productSchema = gql`
  type Product {
    id: ID!
    imageUrl: String!
    price: Int
    title: String!
    description: String
    createdAt: Float
  }

  extend type Query {
    products: [Product!]
    product(id: ID!): Product!
  }
`;

cart 스키마 정의

▼ server/src/schema/cart.ts

const cartSchema = gql`
  type CartItem {
    id: ID!
    imageUrl: String!
    price: Int
    title: String!
    amount: Int
  }

  extend type Query {
    cart: [CartItem!]
  }

  extend type Mutation {
    addCart(id: ID!): CartItem!
    updateCart(id: ID!, amount: Int!): CartItem!
    deleteCart(id: ID!): ID!
    executePay(ids: [ID!]): [ID!]
  }
`;

스키마 합치기

▼ server/src/schema/index.ts

  • 위에서 작성한 product 스키마와 cart 스키마를 합쳐주는? 역할을 한다.
  • _:Boolean을 작성하는 이유는 아무것도 작성하지 않으면 오류가 나기 때문이라고 한다.
import { gql } from "apollo-server-express";
import productSchema from "./product";
import cartSchema from "./cart";

const linkSchema = gql`
  type Query {
    _: Boolean
  }
  type Mutation {
    _: Boolean
  }
`;

export default [linkSchema, cartSchema, productSchema];

Resolver 정의하기

Resolver type 정의

export type Resolver = {
  [k: string]: {
    [key: string]: (
      parent: any,
      args: { [key: string]: any },
      context: any,
      info: any
    ) => any;
  };
};

product resovler 정의

▼ server/src/resolver/product.ts

const mockProducts = Array.from({ length: 20 }).map((_, i) => ({
  id: `${i + 1}`,
  imageUrl: `https://picsum.photos/id/${i * 20}/200/150`,
  price: 50000,
  title: `임시상품${i + 1}`,
  description: `임시상세내용${i + 1}`,
  createdAt: new Date(1654567890123 + i * 1000 * 60 * 60 * 24).toString(),
}));

const productResolver = {
  Query: {
    products: (parent, args, context, info) => {
      return mockProducts;
    },
    product: (parent, { id }, context, info) => {
      const found = mockProducts.find((item) => item.id === id);
      if (found) return found;
      return null;
    },
  },
};

cart resolver 정의

▼ server/src/resolver/cart.ts

const mockProducts = Array.from({ length: 20 }).map((_, i) => ({
  id: `${i + 1}`,
  imageUrl: `https://picsum.photos/id/${i * 20}/200/150`,
  price: 50000,
  title: `임시상품${i + 1}`,
  description: `임시상세내용${i + 1}`,
  createdAt: new Date(1654567890123 + i * 1000 * 60 * 60 * 24).toString(),
}));

let cartData = [
  { id: "1", amount: 1 },
  { id: "2", amount: 2 },
];

const cartResolver = {
  Query: {
    cart: (parent, args, context, info) => {
      return cartData;
    },
    Mutation: {
      addCart: (parent, { id }, context, info) => {
        const newCartData = { ...cartData };
        const targetProduct = mockProducts.find((item) => item.id === id);

        if (!targetProduct) {
          throw new Error("상품이 없습니다.");
        }

        const newItem = {
          ...targetProduct,
          amount: (newCartData[id]?.amount || 0) + 1,
        };

        newCartData[id] = newItem;
        cartData = newCartData;
        return newItem;
      },
      updateCart: (parent, { id, amount }, context, info) => {
        const newCartData = { ...cartData };
        const newData = { ...cartData };
        if (!newCartData[id]) {
          throw new Error("없는 데이터입니다.");
        }
        const newItem = {
          ...newCartData[id],
          amount,
        };
        newCartData[id] = newItem;
        cartData = newCartData;
        return newItem;
      },
      deleteCart: (parent, { id }, context, info) => {
        const newData = { ...cartData };
        delete newData[id];
        cartData = newData;
        return id;
      },
      executePay: (parent, { ids }, context, info) => {
        const newCartData = cartData.filter(
          (cartItem) => !ids.includes(cartItem.id)
        );
        cartData = newCartData;
        return ids;
      },
    },
  },
};

resolver 합치기

▼ server/src/resolver/index.ts

import productResolver from "./product";
import cartResolver from "./cart";

export default [productResolver,cartResolver]

index.js에서 ApolloServer 정의

import schema from "./schema";
import resolvers from "./resolvers";

(async () => {
  const server = new ApolloServer({
    typeDefs: schema,
    resolvers,
    // context:{

    // }
  });

그러고나서 루트디렉토리에서 yarn server로 실행시키면, 에러가 난다..!

Error1 : Unexpected end of JSON input at JSON.parse

[nodemon] Failed to parse config /
SyntaxError: Unexpected end of JSON input
at JSON.parse ()
at /
at FSReqCallback.readFileAfterClose as oncomplete
error Command failed with exit code 1.

  • nodemon.json 파일이 비어있거나 유효한 JSON 구문이 없어서 발생하는 Error이다.
  • nodemon.json 파일을 열어 비어있다면 {}을 입력해주면 Error 해결

Error2 : Unknown file extension ".ts"

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts"

  • 엄청 많은 구글링과 gpt 괴롭히기를 해서 겨우 해결했다.
  • 아마 버전 간 충돌 등으로 인해 발생한 것 같다.
  • package.json 파일의 "type": "module"을 지우고,
  • tsconfig.json 파일을 아래와 같이 지정하면 해결된다.
  {
  "compilerOptions": {
    "target": "ES2018",
    "module": "CommonJS",
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": false,
    "outDir": "dist"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

서버 실행

yarn server로 실행에 성공하면, 터미널에 server listing on 8000 이 뜨고, http://localhost:8000/graphql 에 접속하면, 아래와 같은 화면이 뜬다.

index.ts에서 server.applyMiddleware 부분에 아래 코드를 추가해야 한다.

origin: [
      "http://localhost:5173",
      "https://studio.apollographql.com",
    ],

예시 쿼리 작성

GET_PRODUCTS 쿼리를 작성하고 실행하면 오른쪽 창처럼 response가 온다.

profile
COMPUTER SCIENCE ENGINEERING / Web Front End

0개의 댓글