Prisma와 PlanetScale
prisma와 planetsacle은 간단하게 백엔드데이터를 구성할 수 있도록 도와주는 도구들이다.
prisma를 이용하여 유저정보 뿐만 아니라, 위시리스트, 상품데이터 등을 SQL쿼리작성없이 안전한 방식으로 이 데이터에 접근할 수 있다.
prisma를 통하여 데이터베이스 마이그레이션과 CRUD 작업을 보다 쉽게 할 수 있다.
planetscale은 MySql기반으로 분산 데이터베이스를 구축 및 관리할 수 있도록 도와준다.
위시리스트 데이터베이스 만들기
이전에 작성했던 prisma파일에서 model을 하나 생성해주면 된다.
yarn prisma db push
위 명령어를 통해 planetscale의 db를 생성할 수 있다. 아래는 만들어진 db의 테이블이다.
나는 이 db를 이용하여 찜하기를 누르면? wishlist db에 접근해 해당 아이템의 id값을 저장하도록 하고,
찜하기 취소를 누르면? 해당 id값을 제거하는 방식으로 wishlist의 db를 관리할 것이다.
동시에 unique로 설정해 두었던 사용자의 userId값을 가져와 유저 개인의 wishlist를 만들 수 있도록 했다.
Next api 만들기
먼저 사용자의 wishlist를 불러오는 기능을 할 api를 만들어보자.
prisma client 인스턴스를 먼저 생성한다.
getWishlist 함수에 userId값을 인자로 받고 userId에 대한 wishlist를 findUnique를 사용하여 db에서 찾도록 한다.
만약 wishlist가 있으면 productIds 문자열을 쉼표 기준으로 나누어 배열로 반환한다. [100, 101, 102...]
없다면 빈배열을 반환하도록 해준다. [ ]
NextAuth에서 제공하는 getServerSession을 사용하여 로그인한 사용자인지 session으로 확인할 수 있다.
세션이 없다면 빈배열을 반환한다.
세션이 있다면 getWishlist함수를 호출하여 session에 담긴 user.id를 string으로 감싸주고 wishlist를 가져온다.
getWishlist에서 session.user.id를 가져올 수 있도록 만들어둔 NextAtuh의 AuthOption에서 callbacks를 만들어준다. 이 callbacks는 session의 객체에 접근하여 조작할 수 있게 할 수 있다. getWishlist에서 session.user.id를 사용할 수 있도록 해당 데이터를 저장하고, 이 session.user.id를 사용해서 api 핸들러에서 해당 사용자의 wishlist를 조회할 수 있도록 해주는 것이다.
페이지에서 Wishlist를 구현해보자.
해당 페이지에서 wishlist의 데이터를 받아올 수 있도록 useQuery를 사용해서 데이터를 호출 및 캐싱해보자.
useQuery를 사용해서 axios를 이용해 서버로부터 위시리스트 데이터를 요청하고, 이를 클라이언트에서 사용한다. useQuery훅을 사용하서 첫번째 인자에 쿼리키를 입력한다. 쿼리의 결과로 반환된 data를 wishlist로 바로 사용할 수 있게 할당해준다.
현재는 데이터를 update하지 않아 wishlist에는 빈배열뿐이다. 그렇다면 찜하기라는 버튼을 만들어서 해당 제품의 id값을 만들어둔 wishlist db에 담아보자. 먼저 간단하게 로그인된 상태인지 확인하기 위해서 session이 있는지에 따라 로그인 여부를 판단하고 로그인 되어있지 않다면 로그인부터 시켜보자.
로그인이 확인되었으면, isWished라는 변수를 만들어 해당 제품이 찜이 되어있는지 여부를 확인해주는 조건을 하나 추가해주자.
wishlist.includes(productId)를 평가해서 wishlist 배열이 productId를 포함하고 있는지 여부에 따라 true, 아니면 false를 반환하도록 한다. 이렇게 작성하면 사용자가 찜한 상품은 찜했음으로 나오고, 찜하지 않은 상품은 찜하기로 나오게 된다. 위 값에 따라서 텍스트도 변경될 뿐 만 아니라 아이콘 및 색상도 추후에 변경되어 사용자에게 알릴 수 있도록 작성하면 된다.
session은 next-auth에서 제공하는 useSession()을 사용하여 가져올 수 있다.
const { data: session } = useSession();
update api
이제 wishlist를 읽어오는 api와 페이지에서 로그인 여부에 따라 읽을 수 있도록 최소한으로 구현했다. 이제 wishlist db에 데이터를 저장하는 api를 만들어 productId와 함께 wishlist에 update해보자.
const prisma = new PrismaClient();
async function updateWishlist(userId: string, productId: string) {
try {
const wishlist = await prisma.wishList.findUnique({
where: {
userId: userId,
},
});
const originWishlist =
wishlist?.productIds != null && wishlist.productIds !== ""
? wishlist.productIds.split(",")
: [];
const isWished = originWishlist.includes(productId);
const newWishlist = isWished
? originWishlist.filter((id) => id !== productId)
: [...originWishlist, productId];
const response = await prisma.wishList.upsert({
where: {
userId,
},
update: {
productIds: newWishlist.join(","),
},
create: {
userId,
productIds: newWishlist.join(","),
},
});
return response?.productIds.split(",") || [];
} catch (error) {
console.error(error);
return [];
}
}
type Data = {
items?: any;
message: string;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
const session = await getServerSession(req, res, authOption);
console.log(session);
const { productId } = req.body;
if (session == null) {
res.status(200).json({ items: [], message: "No session" });
return;
}
try {
const wishlist = await updateWishlist(
String(session.user.id),
String(productId)
);
res.status(200).json({ items: wishlist, message: "Success" });
} catch (error) {
res.status(400).json({ message: "Failed" });
}
}
updateWishlist 함수에 userId와 productId를 인자로 받도록 작성했다.
이후 where문으로 userId에 해당하는 사용자의 위시리스트를 db에서 찾도록 한다.
originWishlist를 작성하여 찾아온 wishlist에서 상품ID들을 배열로 변환한다. 데이터가 없다면 빈배열을 반환하도록 하자.
newWishlist를 통해 제품을 추가/제거 가능하도록 한다. isWished로 해당 상품이 리스트에 있다면 ? 삭제, 없다면? 추가하도록 한다.
이후 prisma.wishlist.upsert로 db에 업데이트하고, 존재하지 않는다면 새로 create한다.
useMutation
useMutation은 React-Query에서 제공하는 라이브러리이다. mutate를 사용하면 데이터를 변경하는 작업을 할 때, 상태관리를 쉽게 할 수 있다. isLoading과 error, data 등을 반환하고, mutation이 성공하면 onSuccess 함수가 호출된다. 이를 이용해서 wishlist를 업데이트 시켜보자.
mutate는 productId를 받아, update api에 post요청을 보낸다. 요청에 성공하면 data에서 items를 반환한다.
useMutation의 onSuccess option에 따라 성공시에 onSuccess가 실행되는데, queryClient.invalidateQueries를 통해 wishlist키가 무효화된다. 무효화된 데이터는 컴포넌트 렌더링 시에 쿼리를 다시 트리거하여 불러온다.
즉, 업데이트 이후에 wishlist쿼리를 다시 받아오도록 설정하는 것이다. refetch라고도 한다. 업데이트 이후 최신의 데이터를 받아오도록 설정하면 사용자가 찜하기가 등록되었는지 바로 알 수 있도록 만든것이다.
그러나 post에서 오류가 발생했다.
500에러인것을 보니 서버딴에서 update부분 중 뭔가 에러가 발생한 것 같다.
에러에 여러 가능성을 염두에 두면서 console.log()을 찍어 보니 productId가 undefined로 나왔다.
해당 부분에서 에러가 나왔다. JSON.parse로 req.body를 말아둔것이 에러의 원인이었다.
axios에 익숙해지기 전 fetch함수로 api를 호출했었는데, axios는 기본적으로 JSON.stringify를 사용하기 때문에, 별도로 작성할 필요 없다. 백엔드 서버 딴에서도 이미 객체형태로 받기 때문에 parse로 말아주지 않아도 된다. 위의 원인을 해결하고 나니 에러가 해결되었다.
실제 update되어 db에도 저장되는 것을 확인해봤다.
그러나 찜함과 찜하기의 버튼 변화가 부자연스러웠다. 사용자가 누르면 즉각적으로 변하지 않아 이질감이 들고, 바로 반응하지 않으면 다시 데이터를 삭제할 수 도 있다는 생각이 들었다. 보다 자연스럽게 만들기 위해서 onMutate를 활용할 수 있다.
Optimistic Update
Optimistic Update는 직역하면 낙관적인(?)이라는 뜻으로 "그냥 잘 될거야~ 라면서 실제 업데이트 이전에 미리 변경해 버리는 것" 이라고 이해하면 된다. 간단한 로직에다가 특별한 에러가 발생하지 않는 찜하기와 같은 기능에 사용하기 적합하다. 지금까지 구현으로는 실제 업데이트를 반영하기 까지 체감상 시간이 꽤 걸린다. 이를 그냥 업데이트 한다고 버튼부터 일단 바꿔버리고, 실제 업데이트가 되는데, 이렇게 하면 사용자에게 보다 더 빠르게 실행이 반영된다고 느낄 수 있다.
onMutate를 활용하면 예상되는 변경사항이 실제로 변경되기 전에 미리 반영해준다. 이를 이용해서 실제 업데이트까지 걸리는 시간을 사용자에게는 단축해서 보여줄 수 있다는 장점이 있다. 실제 업데이트 시간이 걸리지만, 어차피 업데이트에 대한 에러가 없다는 가정 하에 미리 바꿔주면 문제가 없다.
onMutate는 mutation 시작전에 호출된다. cancelQueries로 wishlist get을 취소하고, mutation이 실행되는 동안 쿼리의 추가요청을 발생하지 않도록 하여 서버에 부담을 줄일 수 있다.
prev로 현재 캐시에 저장된 쿼리의 상태를 저장한다. 만약 mutation이 실패하게 되더라도 prev로 롤백할 수 있다.
setQueryData를 사용해서 Optimistic update를 할 수 있다.
만약 old가 존재하고 productId가 이미 포함되어 있다면 찜하기를 해제하는 것이므로 productId를 제거하다.
만약 old가 존재하고 productId가 포함되어 있지 않다면 찜하기를 하는 것이므로 productId를 추가한다.
old가 없으면 빈배열을 반환한다.
onError를 통해 이전 상태를 반환한 prev를 통해 에러가 발생했을 때, 이전 상태로 되돌릴 수 있도록 한다.
Optimistic Update 최종 구현 결과
업데이트가 반영(get-wishlist)되기 전에 '찜하기'에서 '찜함'으로 변경되는 것을 확인할 수 있다.
'개발기록' 카테고리의 다른 글
[Nextjs] PageSpeed Insights SEO 및 웹 최적화 적용 (0) | 2023.10.09 |
---|---|
[Google OAuth] Google Login API (1) | 2023.09.15 |
[React-Query] React-Query와 Debounce를 활용한 효율적인 데이터 페칭 (0) | 2023.09.09 |
[Next.js] Google Map API 구현 (0) | 2023.09.05 |
[Next.js] Next.js + TypeScript + Tailwind CSS 초기세팅 (0) | 2023.08.31 |