REACT/쇼핑몰 프로젝트

[REACT] React + Typescript + Vite + SCSS로 만드는 쇼핑몰 개인프로젝트 (5/10)

예글 2024. 1. 22. 21:24

벌써 절반왔다 절반!! 끝까지 파이팅 💪🏻

 

💡 오늘 할 것

1. 모노레포로 변경 / 서버 환경 설정

2. schema 정의

3. resolver 정의

4. 서버 실행하기

 

1️⃣ BFF형식의 서버 생성하기 / monorepo workspace로 변경

- BFF 서버란?

: 프론트엔드 요구사항에 맞게 구현하기 위해 도움을 주는 서버

 

- monorepo란?

  • Workspaces는 단일 최상위 루트 패키지 내에서 로컬 파일 시스템의 여러 패키지를 관리할 수 있도록 지원하는 npm cli의 기능 집합을 가리키는 일반 용어
  • Workspace를 통해 로컬 파일 시스템에서 연결된 패키지를 훨씬 더 효율적으로 처리함

- root/package.json

{
  "name": "shopping-mall",
  "version": "1.0.0",
  "main": "index.js",
  "repository": "https://github.com/yegri/react_shoppingMall_project.git",
  "author": "Yeeun Lee <exsilver305@gmail.com>",
  "license": "MIT",
  "private": true,
  "workspaces": [
    "client",
    "server"
  ],
  "scripts": {
    "client": "yarn workspace client run dev",
    "server": "yarn workspace server run dev"
  }
}

 

- 폴더 구조

 

2️⃣ Apollo Server 설치하기

- GraphQL을 편하게 사용할 수 있도록 도와주는 라이브러리

- Apollo Server는 GraphQL API를 제공하는 서버를 개발할 수 있게 도와주는 패키지로 Node.js에서 사용하는 Express와 비슷한 역할 을 한다.

 

1. 설치

 

- 서버로 이동 후

yarn add express apollo-server apollo-server-express graphql
yarn add --dev ts-node @types/node
yarn add typescript

 

2. server/tsconfig.json

{
  "compilerOptions": {
    "target": "ES5",
    "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"]
}

 

3. server/package.json

 "scripts": {
    "dev": "nodemon --exec 'ts-node src/index.ts'"
  }

 

- 서버 실행할 때 계속 NOT FOUND MODULE .. .src/index.ts 이러면서 에러가 났었는데..

- 계속 구글링하고 강의 봐도 뭐가 문제인지 모르겠는 거였다,,, 진짜 너무너무 포기하고 싶었는데 각잡고 앉아서 chat GPT한테도 물어보고 계속 찾아보고 수정해보고.. 하다가 원인을 발견했다!

- .src/index.ts 이 경로가 안 맞아서 그런 거였다..! src/index.ts로 바꾸니까 해결..  😭 나에게 왜 자꾸 이런 시련을 주시나요 ㅠㅠ

 

4. server/nodemon.json

{
  "watch": ["src"],
  "ignore": ["db/**/*"],
  "env": {
    "NODE_ENV": "development"
  }
}

 

5. server/src/index.ts

import express from "express";
import { ApolloServer } from "apollo-server-express";
import schema from "./schema";
import resolvers from "./resolvers";

(async () => {
  const server = new ApolloServer({
    typeDefs: schema,
    resolvers,
  });

  const app = express();

  await server.start();

  server.applyMiddleware({
    app,
    path: "/graphql",
    cors: {
      origin: ["http://localhost:5173", "https://studio.apollographql.com"],
      credentials: true,
    },
  });

  await app.listen({ port: 8000 });
  console.log("server listening on 8000...");
})();

 

- typeDefs resolvers를 ApolloServer 생성자에 넘겨 GraphQL 서버 인스턴스를 생성하고 그 서버를 시작해주는 코드를 작성

3️⃣ Schema 정의

💻 스키마는 서버에서 어떻게 데이터를 요청할지 정의한 파일

- 요청 시 어떤 데이터를 얼마나 요청할지, 각각의 데이터의 자료형이 무엇이고, 어떤 데이터를 필수로 요청할지에 대한 정보가 담김

- 즉, 사용자는 반드시 스키마에 정의된 형태로 서버에 요청해야 함

 

💻 스키마에는 Query, Mutation과 같이 2가지 요청이 존재

- Query : 데이터베이스에서 데이터를 읽는 요청

- Mutation : 데이터베이스를 수정하는 요청

 

이렇게 구분을 하는 이유는 데이터베이스 읽기 요청은 무한정으로 동시에 수행 가능하지만, 데이터베이스 수정 요청은 순차적으로 수행되어야하기 때문!!

 

1. server/src/schema/product.ts

import { gql } from "apollo-server-express";

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

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

export default productSchema;

 

2. server/src/schema/cart.ts

import { gql } from "apollo-server-express";

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!]
  }
`;

export default cartSchema;

 

3. server/src/schema/index.ts

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, productSchema, cartSchema];

 

4️⃣ Resolver 정의

💻 Resolver는 사용자가 쿼리를 요청했을 때 이를 서버가 어떻게 처리할지 정의하는 파일

- resolver는 요청에 대해 단순히 데이터를 반환할 수도 있고, 직접 데이터를 찾거나, 메모리에 접근하거나, 다른 API에 요청해서 데이터를 가져올 수 있다

 

1. src/resolvers/product.ts

import { Resolver } from "./types";

// 상품 mock data
const mock_products = (() =>
  Array.from({ length: 20 }).map((_, i) => ({
    id: i + 1 + "",
    imageUrl: `https://source.unsplash.com/200x150/?nature/${i + 1}`,
    price: 50000,
    title: `임시상품${i + 1}`,
    description: `임시상세내용${i + 1}`,
    createdAt: new Date(1645735501883 + i * 1000 * 60 * 60 * 10).toString(),
  })))();

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

      if (found) return found;

      return null;
    },
  },
};

export default productResolver;

 

2. src/resolvers/cart.ts

import { Resolver } from "./types";

// 상품 mock data
const mock_products = (() =>
  Array.from({ length: 20 }).map((_, i) => ({
    id: i + 1 + "",
    imageUrl: `https://source.unsplash.com/200x150/?nature/${i + 1}`,
    price: 50000,
    title: `임시상품${i + 1}`,
    description: `임시상세내용${i + 1}`,
    createdAt: new Date(1645735501883 + i * 1000 * 60 * 60 * 10).toString(),
  })))();

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

const cartResolver: Resolver = {
  Query: {
    cart: (parent, args, context, info) => {
      return cartData;
    },
  },

  Mutation: {
    addCart: (parent, { id }, context, info) => {
      const newCartData = { ...cartData };
      const targetProduct = mock_products.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 newData = { ...cartData };
      if (!newData[id]) {
        throw new Error("없는 데이터입니다");
      }
      const newItem = {
        ...newData[id],
        amount,
      };
      newData[id] = newItem;
      cartData = newData;
      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;
    },
  },
};

export default cartResolver;

 

 

3. src/resolvers/index.ts

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

export default [productResolver, cartResolver];

 

5️⃣ 서버 실행

yarn run server

 

 

- 이 페이지가 보이면 성공!

 

💻 쿼리 작성해서 데이터 잘 불러오는지 확인

전체 쿼리
개별 쿼리

 

 

5일차 끝!!!

 

서버 생성하고 불러오고 쿼리 작성하고... 이런 건 처음 해보는데 오류 때문에 힘들었지만.. ㅎㅎㅎ

그래도 신기하고 재미있었다!

이렇게 한발짝 한발짝 나아가면 되는 거지~~