💡 오늘 할 것!
- jsonDB 생성
- resolver - DB연동
- 서버 변경사항 클라이언트에 반영
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도 점점 익숙해지고,, 언젠간 이 코드를 다 이해하는 날이 오겠지? 😳
'REACT > 쇼핑몰 프로젝트' 카테고리의 다른 글
[REACT] React + Typescript + Vite + SCSS로 만드는 쇼핑몰 개인프로젝트 (8/10) (1) | 2024.01.27 |
---|---|
[REACT] React + Typescript + Vite + SCSS로 만드는 쇼핑몰 개인프로젝트 (7/10) (0) | 2024.01.24 |
[REACT] React + Typescript + Vite + SCSS로 만드는 쇼핑몰 개인프로젝트 (5/10) (0) | 2024.01.22 |
[REACT] React + Typescript + Vite + SCSS로 만드는 쇼핑몰 개인프로젝트 (4/10) (0) | 2024.01.22 |
[REACT] React + Typescript + Vite + SCSS로 만드는 쇼핑몰 개인프로젝트 (3/10) (0) | 2024.01.18 |