REACT/쇼핑몰 프로젝트

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

예글 2024. 1. 23. 21:17

💡 오늘 할 것!

  1.  jsonDB 생성
  2.  resolver - DB연동
  3.  서버 변경사항 클라이언트에 반영

 

1️⃣  jsonDB 생성하기

1. src/dbController.ts 생성

💻 JSON 데이터베이스의 어떤 데이터를, 어떻게 처리할 것인지, 컨트롤러가 필요

import fs from "fs";
import { resolve } from "path";

export enum DBField {
  CART = "cart",
  PRODUCTS = "products",
}

const basePath = resolve(); // __dirname

const filenames = {
  [DBField.CART]: resolve(basePath, "src/db/cart.json"),
  [DBField.PRODUCTS]: resolve(basePath, "src/db/products.json"),
};

// 데이터를 조회할 경우 실행
export const readDB = (target: DBField) => {
  try {
    return JSON.parse(fs.readFileSync(filenames[target], "utf-8"));
  } catch (err) {
    console.log(err);
  }
};

// 데이터를 수정할 경우 실행
export const writeDB = (target: DBField, data: any) => {
  try {
    fs.writeFileSync(filenames[target], JSON.stringify(data));
  } catch (err) {
    console.log(err);
  }
};
  • JSON.parse : JSON 문자열을 JS 객체로 변환
  • JSON.stringfy : JS객체를 JSON 문자열로 변환
  • fs.readFileSync : 경로에 따른 동기파일로부터 데이터 읽어옴. 단, utf로 명시하여 인코딩이 되도록 해야함
  • fs.writeFileSync : 경로에 따른 동기파일에 데이터를 쓸 수 있음

📚 enum (enumerated type)

  • 열거하는 타입
  • 상수 값 중에서 비슷한 종류들을 묶어두기 위한 용도로 자주 사용됨
  • 사용하는 이유
    • 코드가 단순해지고 가독성이 좋음
    • enum이라는 이름 자체만으로도 열거 의도를 분명히 알 수 있음
    • 고정값으로 이용 가능
    • 오류 발생 줄임

2. src/db/products.json 생성

- mocking 데이터를 json 형식으로 저장

[
  {
    "price": 50000,
    "title": "임시상품1",
    "description": "임시상세내용1"
  },
  {
    "price": 50000,
    "title": "임시상품2",
    "description": "임시상세내용2"
  },
  {
    "price": 50000,
    "title": "임시상품3",
    "description": "임시상세내용3"
  },
  {
    "price": 50000,
    "title": "임시상품4",
    "description": "임시상세내용4"
  },
  {
    "price": 50000,
    "title": "임시상품5",
    "description": "임시상세내용5"
  },
  {
    "price": 50000,
    "title": "임시상품6",
    "description": "임시상세내용6"
  },
  {
    "price": 50000,
    "title": "임시상품7",
    "description": "임시상세내용7"
  },
  {
    "price": 50000,
    "title": "임시상품8",
    "description": "임시상세내용8"
  },
  {
    "price": 50000,
    "title": "임시상품9",
    "description": "임시상세내용9"
  },
  {
    "price": 50000,
    "title": "임시상품10",
    "description": "임시상세내용10"
  },
  {
    "price": 50000,
    "title": "임시상품11",
    "description": "임시상세내용11"
  },
  {
    "price": 50000,
    "title": "임시상품12",
    "description": "임시상세내용12"
  },
  {
    "price": 50000,
    "title": "임시상품13",
    "description": "임시상세내용13"
  },
  {
    "price": 50000,
    "title": "임시상품14",
    "description": "임시상세내용14"
  },
  {
    "price": 50000,
    "title": "임시상품15",
    "description": "임시상세내용15"
  },
  {
    "price": 50000,
    "title": "임시상품16",
    "description": "임시상세내용16"
  },
  {
    "price": 50000,
    "title": "임시상품17",
    "description": "임시상세내용17"
  },
  {
    "price": 50000,
    "title": "임시상품18",
    "description": "임시상세내용18"
  },
  {
    "price": 50000,
    "title": "임시상품19",
    "description": "임시상세내용19"
  },
  {
    "price": 50000,
    "title": "임시상품20",
    "description": "임시상세내용20"
  }
]

 

3. src/db/cart.json 생성

[
  {
    "id": "1",
    "amount": 1
  },
  {
    "id": "2",
    "amount": 2
  }
]

 

4. src/index.ts 

const server = new ApolloServer({
    typeDefs: schema,
    resolvers,
    
    // context 추가
    context: {
      db: {
        products: readDB(DBField.PRODUCTS),
        cart: readDB(DBField.CART),
      },
    },
  });

 

- 콘솔로 DB를 찍어보면

 

성공!

 

2️⃣ resolver - DB 연동

1. src/resolvers/product.ts

import { Resolver } from "./types";

const productResolver: Resolver = {
  Query: {
    products: (parent, args, { db }) => {
      return db.products;
    },

    product: (parent, { id }, { db }) => {
      const found = db.products.find((item) => item.id === id);

      if (found) return found;

      return null;
    },
  },
};

export default productResolver;

 

2. src/resolvers/cart.ts

📚 Query

  • 장바구니 상품 조회

📚 Mutation

  • 장바구니 상품 추가 (add)
  • 장바구니 상품 수량 변경 (update)
  • 장바구니 상품 삭제 (delete)
  • 장바구니 상품 결제 (execute)
import { DBField, writeDB } from "../dbController";
import { Cart, Resolver } from "./types";

const setJSON = (data: Cart) => writeDB(DBField.CART, data);

const cartResolver: Resolver = {
  Query: {
    cart: (parent, args, { db }) => {
      return db.cart;
    },
  },

  Mutation: {
    addCart: (parent, { id }, { db }, info) => {
      if (!id) throw Error("상품id가 없다!");
      const targetProduct = db.products.find((item) => item.id === id);

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

      const existCartIndex = db.cart.findIndex((item) => item.id === id);
      if (existCartIndex > -1) {
        const newCartItem = {
          id,
          amount: db.cart[existCartIndex].amount + 1,
        };

        db.cart.splice(existCartIndex, 1, newCartItem);
        setJSON(db.cart);
        return newCartItem;
      }

      const newItem = {
        id,
        amount: 1,
      };
      db.cart.push(newItem);
      setJSON(db.cart);

      return newItem;
    },

    updateCart: (parent, { id, amount }, { db }) => {
      const existCartIndex = db.cart.findIndex((item) => item.id === id);

      if (existCartIndex < 0) {
        throw new Error("없는 데이터입니다.");
      }

      const newCartItem = {
        id,
        amount,
      };

      db.cart.splice(existCartIndex, 1, newCartItem);
      setJSON(db.cart);
      return newCartItem;
    },

    deleteCart: (parent, { id }, { db }) => {
      const existCartIndex = db.cart.findIndex((item) => item.id === id);

      if (existCartIndex < 0) {
        throw new Error("없는 데이터입니다.");
      }

      db.cart.splice(existCartIndex, 1);
      setJSON(db.cart);
      return id;
    },

    executePay: (parent, { ids }, { db }) => {
      const newCartData = db.cart.filter(
        (cartItem) => !ids.includes(cartItem.id)
      );

      db.cart = newCartData;
      setJSON(db.cart);
      return ids;
    },
  },

  CartItem: {
    product: (cartItem, args, { db }) =>
      db.products.find((product) => product.id === cartItem.id),
  },
};

export default cartResolver;

 

✏️  setJSON(db.cart)를 넣는 이유!

 

- src/index.ts

const server = new ApolloServer({
    typeDefs: schema,
    resolvers,
    context: {
      db: {
        products: readDB(DBField.PRODUCTS),
        cart: readDB(DBField.CART),
      },
    },
  });

 

context에서 db 설정한 대로 초기화 되기 때문!

setJSON(db.cart)를 통해 수정한 값이 들어오게 하기 위함

 

💡 중간중간 apollo에서 쿼리 날리면서 잘 동작하는지 확인할 것!

 

3️⃣ Client에 반영하기

하.. 이 부분에서 강의랑 똑같이 해도 버전 문제인지 오류가 빵빵 터져서 해결하느라 시간이 많이 뺏겼다 ㅠㅠ


🔥 에러 1

Cannot return null for non-nullable field Product.imageUrl.

-> GraphQL 스키마에서 정의된 필드에 대해 "non-nullable" 설정이 되어 있는데, 해당 필드의 값이 null로 반환되고 있다는 것을 나타냄

 

해결법 : GraphQL 스키마 검토 - GraphQL 스키마에서 Product 객체의 imageUrl 필드가 nullable이 아닌지(!로 표시된지) 확인

 

- server/src/schema/product.ts

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

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!
  }

  extend type Mutation {
    addProduct(
      imageUrl: String
      price: Int!
      title: String!
      description: String!
    ): Product!
    updateProduct(
      id: ID!
      imageUrl: String
      price: Int
      title: String
      description: String
    ): Product!
    deleteProduct(id: ID!): ID!
  }
`;

export default productSchema;

 

- 나의 경우 이 부분에서 imageUrl : String! 여기에서 !를 빼주었더니 해결


🔥 에러 2

- 이 부분은 버전이 올라가면서 타입 정의하는 부분이 바뀌어서 생겨난 것이었다

// 이걸
const { data } = useQuery<{ product: Product }>([QueryKeys.PRODUCTS, id], () =>
    graphqlFetcher(GET_PRODUCT, { id }),
)
  
// 이렇게 바꿨더니 해결
const { data } = useQuery<{ product: Product }>(
  [QueryKeys.PRODUCTS, id],
   () => graphqlFetcher<{ product: Product }>(GET_PRODUCT, { id })
);

 


🔥 에러 3

'data'은(는) 'unknown' 형식입니다.

-> TypeScript에서 "unknown"은 모든 값에 대해 더 엄격한 타입 검사를 수행하는 타입 중 하나. 해당 오류가 발생하면 TypeScript가 특정 변수의 타입을 알 수 없다는 것을 의미. 이는 보다 안전한 코드를 유도하기 위한 목적으로 도입된 개념 중 하나!

 

// 이걸
const CartIndex = () => {
  const { data } = useQuery(QueryKeys.CART, () => graphqlFetcher(GET_CART), {
    staleTime: 0,
    cacheTime: 1000,
  });

  const cartItems = data.cart as Cart[];

  if (!cartItems) return <div>장바구니가 비었어요</div>;

  return <CartList items={cartItems} />;
};

// 이렇게 바꾸었더니 해결!
const CartIndex = () => {
  const { data } = useQuery<{ cart: Cart[] }>(
    QueryKeys.CART,
    () => graphqlFetcher(GET_CART),
    {
      staleTime: 0,
      cacheTime: 1000,
    }
  );

  const cartItems = data?.cart;

  if (!cartItems) return <div>장바구니가 비었어요</div>;

  return <CartList items={cartItems} />;
};

 

- 이것도 역시나 타입 오류,,, Typescript 빡세네🥲

 


🔥 에러 4

이 호출과 일치하는 오버로드가 없습니다. 오버로드 1/4('(mutationFn: MutationFunction<{ updateCart: any; }, { id: string; amount: number; }>, options?: Omit<UseMutationOptions<{ updateCart: any; }, unknown, { id: string; amount: number; }, Cart[] | null>, "mutationFn"> | undefined): UseMutationResult<...>')에서 다음 오류가 발생했습니다. '({ id, amount }: { id: string; amount: number; }) => Promise<unknown>' 형식의 인수는 'MutationFunction<{ updateCart: any; }, { id: string; amount: number; }>' 형식의 매개 변수에 할당될 수 없습니다. 'Promise<unknown>' 형식은 'Promise<{ updateCart: any; }>' 형식에 할당할 수 없습니다. 'unknown' 형식은 '{ updateCart: any; }' 형식에 할당할 수 없습니다. 오버로드 2/4('(mutationKey: MutationKey, options?: Omit<UseMutationOptions<{ updateCart: any; }, unknown, { id: string; amount: number; }, Cart[] | null>, "mutationKey"> | undefined): UseMutationResult<...>')에서 다음 오류가 발생했습니다. '({ id, amount }: { id: string; amount: number; }) => Promise<unknown>' 형식의 인수는 'MutationKey' 형식의 매개 변수에 할당될 수 없습니다. 오버로드 3/4('(mutationKey: MutationKey, mutationFn?: MutationFunction<unknown, void> | undefined, options?: Omit<UseMutationOptions<unknown, unknown, void, unknown>, "mutationFn" | "mutationKey"> | undefined): UseMutationResult<...>')에서 다음 오류가 발생했습니다. '({ id, amount }: { id: string; amount: number; }) => Promise<unknown>' 형식의 인수는 'MutationKey' 형식의 매개 변수에 할당될 수 없습니다.

 

- 지긋지긋한 이 호출과 일치하는 오버로드가 없습니다...

- 이 오류는 이 프로젝트를 진행하면서 정말 정말 많이 본 오류인데,, 이것 또한 타입 지정 오류이다

 

- components/cart/item.tsx

 // 장바구니 수량 업데이트 / 이 부분을
  const { mutate: updateCart } = useMutation(
    ({ id, amount }: { id: string; amount: number }) =>
      graphqlFetcher(UPDATE_CART, { id, amount }),
    
    
  // 장바구니 수량 업데이트 / 이렇게 바꾸었더니 해결
  const { mutate: updateCart } = useMutation<
    Cart,
    unknown,
    { id: string; amount: number }
  >(({ id, amount }) => graphqlFetcher(UPDATE_CART, { id, amount }),

 


🔥 에러 5

'Cart' 형식에 'updateCart' 속성이 없습니다.

-> 바로 updateCart를 참조하도록 수정해야 하는 것이었음

 

    // 이거를
    onSuccess: ({updateCart}) => {
      
    },
    
    // 이렇게 수정하니까 해결!
    onSuccess: (updateCart) => {
      
    },

 


🔥 에러 6

message: "Field "deleteCart" must not have a selection since type "ID!" has no subfields

-> 이 오류는 GraphQL 쿼리에서 반환되는 필드에 서브필드(subfields)를 선택할 필요가 없는데, 선택이 시도되었을 때 발생하는 것.

오류 메시지에 따르면 "deleteCart" 필드의 반환 타입이 "ID!"인데, 해당 타입은 서브필드를 가질 수 없는 타입으로 정의되어 있음

이 문제를 해결하려면 GraphQL 쿼리에서 "deleteCart" 필드에 서브필드를 선택하지 않아야 함. 아마도 "deleteCart" 필드에는 반환할 필드가 이미 명시되어 있거나, 해당 필드 자체가 서브필드를 가질 수 없는 형태로 정의되어 있을 것..!

 

- client/src/graphql/cart.ts

// 이거를
export const DELETE_CART = gql`
  mutation DELETE_CART($id: ID!) {
    deleteCart(cartId: $id) {
      id
    }
  }
`

// 이렇게 바꾸니까 해결!
export const DELETE_CART = gql`
  mutation DELETE_CART($id: ID!) {
    deleteCart(id: $id)
  }
`

 

 

하... 많다 많아,,

 

이렇게 하나하나 고쳐 나가다 보니 다행히도 잘 돌아간다.. 행복쓰,,

처음엔 에러메시지 봐도 뭐가 어디서 잘못된 건지 몰라서 멘붕이었는데 6일차인 지금은 어렴풋이 알 것 같다 ㅎㅎ

 

graphql도 점점 익숙해지고,, 언젠간 이 코드를 다 이해하는 날이 오겠지? 😳