상세페이지라고 뭐가 특별한것이 아니다. 우리가 등록한 활동의 사진, 제목, 설명 등등 기본적인 정보가 들어가고 약간의 사용자 기능이 추가된 페이지일 뿐이다.

구현된 레이아웃만 봐도 되게 일반적이고 평범한 페이지이다. 그래서 하나씩 내가 어떻게 구현했는지 나열하는 것은 의미없다고 생각한다. 그중에 조회수와 좋아요 기능, 공유기능까지만 어떻게 구현했고 개선시켰는지 보겠다.
조회수는 활동 데이터 안에 들어가 있고 조회수는 페이지에 사용자가 접근했을때에만 올라간다. 우선 조회수가 올라가는 로직을 먼저 확인해보겠다.
export const increaseActivityCount = async (activityId: string): Promise<ActionType<null>> => {
try {
const cookieStore = cookies();
const viewCookie = cookieStore.get(`viewed_${activityId}`);
if (viewCookie) {
return { success: false, message: '이미 조회수를 올린 유저입니다.' };
}
await db.activity.update({
where: { id: activityId },
data: { views: { increment: 1 } },
});
cookies().set(`viewed_${activityId}`, 'true', { maxAge: 30 * 60 });
return { success: true, message: '조회수 증가' };
} catch (error) {
return { success: false, message: '에러 발생' };
}
};
조회수가 올라가는 함수를 작성해서 조회수를 올려주고 있다. 조회수를 올려주는 기준은 쿠키에 저장된 액티비티 id값이 없을때 액티비티의 조회수를 1올리는것이다. 그리고 매번 접근할때 올라가는게 아니라 쿠키의 시간을 30분으로 설정해 30분동안은 조회수가 1만 올라가도록 해놨다.
그래서 이 함수는 페이지에 접근한 순간에 동작해야 하기 때문에 클라이언트 이벤트일수밖에 없다. 그래서 useEffect를 사용해서 페이지에 접근한 순간 함수를 실행시켜준다.
useEffect(() => {
const action = async () => {
const result = await increaseActivityCount(activityDetail.id);
};
action();
}, [activityDetail.id]);
이렇게 해주면 접근했을때 조회수가 올라간다. 이 함수를 통해 액티비티의 조회수가 올라가면 사용자의 화면에도 즉각적으로 반영이 되어야한다. 맨 처음에는 실시간 데이터를 받아오기 위해 조회수가 올라가면
revalidatePath(`/${activityId}`);
경로에 저장된 캐시를 초기화하고 받아오는 방법이 있다. 하지만 겨우 조회수같은 사소한 디테일때문에 데이터 전체를 revalidate시켜서 받아올 필요까지 없다. 그래서 나는 react-query에서 사용했었던 optimistic update를 비슷하게 적용하려고 한다.
optimistic update?
간단하게 설명하면 낙관적인 업데이트로 서버의 데이터를 가져와 업데이트하는것이 아니라 클라이언트, 브라우저에서 직접 업데이트 시키는 방식이다.
optimistic update를 적용하기 전에는
<div className="flex items-center gap-1">
<Eye width={20} />
<p className="text-xs">{views}</p>
</div>
이렇게 데이터에 있는 값을 그대로 사용해줬다. 하지만 optimistic update를 하려면 이 값을 state로 바꿔줘 클라이언트 요소로 바꿔줘야 한다.
const [viewCount, setViewCount] = useState(activityDetail.views);
우선 state값을 만들어주고 기본값으로 액티비티의 조회수를 넣어준다.
useEffect(() => {
const action = async () => {
const result = await increaseActivityCount(activityDetail.id);
if (result.success) {
setViewCount(viewCount + 1);
}
};
action();
}, [activityDetail.id, viewCount]);
이후에 조회수를 올리는 useEffect에서 조회수 올리는 것이 성공하면 state의 값을 1 추가해주는 것이다. 물론 해당 페이지에서 조회수가 중요한 정보가 아니기때문에 응답에 상관없이 무조건 1을 올려줘도 된다. 하지만 우리는 30분이라는 제한 시간이 있기때문에 서버 데이터에 조회수 올라가는 것이 성공했을때에만 조회수를 올리도록 설정했다.
좋아요 기능도 크게 다르지 않다.
const toggleFavorite = async (activityId: string): Promise<ActionType<Favorite>> => {
const userId = await getCurrentUserId();
try {
const existingFavorite = await db.favorite.findFirst({
where: {
activityId,
userId,
},
});
if (existingFavorite) {
await db.favorite.delete({
where: {
id: existingFavorite.id,
},
});
return { success: true, message: '즐겨찾기에서 삭제되었습니다.' };
}
const newFavorite = await db.favorite.create({
data: {
userId,
activityId,
},
});
if (!newFavorite) return { success: false, message: '즐겨찾기 추가에 실패하였습니다.' };
return {
success: true,
message: '즐겨찾기에 추가되었습니다.',
data: newFavorite,
};
} catch (error) {
return {
success: false,
message: '즐겨찾기 토글 중에 에러가 발생하였습니다.',
};
}
};
즐겨찾기를 토글하는 함수를 하나 만들어줬다. 그리고 페이지에 즐겨찾기와 관련된 요소가 두개있다.

우측 상단에 좋아요 버튼은 좋아요 여부에 따라 색상이 변경되고 제목 밑에 있는 좋아요는 좋아요 개수를 나타낸다. 위에서 조회수를 구현한것과 같이
const [favorite, setFavorite] = useState(activityDetail.isFavorite);
const [likeCount, setLikeCount] = useState(activityDetail._count.favorites);
두개의 값을 만들어서 클라이언트 요소로 만들어준다.
const toggleActivityLike = async () => {
setFavorite(!favorite);
if (favorite) {
setLikeCount(likeCount - 1);
} else {
setLikeCount(likeCount + 1);
}
const result = await toggleFavorite(activityDetail.id);
toast.message(result.message);
};
그리고 토글하는 함수를 작성해주면 된다. 우선 서버에 요청을 보내기 전에 미리 좋아요상태를 토글하고 count도 좋아요 여부에 따라 count를 변경시켜준다. 그리고 이후에 서버에 요청을 보내서 toast로 결과를 보여주면 된다.
우리의 서비스에서 약속잡기나 질문하기와 같은 기능들은 핵심적인 기능이고 만약 에러가 발생했을때 잘못된 동작을 알려주고 즉각적인 피드백이 필요하다. 하지만 부가적인 기능이고 잘못된 동작이 발생해도 서비스 이용에 전혀 문제가 없는 요소들도 존재한다. 그런 기능들이 좋아요와 조회수라고 판단하고 결정했다.
만약 좋아요를 눌렀는데 추가가 안됬다면 사용자는 본인의 실수로 판단해 다시 요청을 보내면 끝이기 때문이다. 각 요소에 어떤 방법을 통해 업데이트하는지 정답은 없지만 여러 방법을 시도해보는 것도 중요하다고 생각한다.
우리는 링크 공유와 카카오톡 기능을 넣어줬다. 링크 공유야 매번 비슷한 방식이라 따로 추가하지는 않겠다. 카카오톡 공유의 경우는 조금 다르다.
전까지 카카오톡 공유는 클라이언트 이벤트였다. 하지만 우리는 서버 컴포넌트를 사용하고 app router를 사용하기 때문에 다른 방식을 적용시켜줘야한다.
'use client';
import Script from 'next/script';
export default function KakaoScript() {
const onLoad = () => {
window.Kakao.init(process.env.NEXT_PUBLIC_KAKAO_API_KEY);
};
return <Script src="https://developers.kakao.com/sdk/js/kakao.js" async onLoad={onLoad} />;
}
최상위 경로에 KakaoScript.tsx라는 파일을 만들어줘야한다. 기존의 page router의 경우는 client 컴포넌트각 기본인데 app router는 server 컴포넌트가 기본이기 때문이다. 위의 파일을 생성해주고 프로젝트 기본 RootLayout에 카카오 스크립트를 넣어주면 된다.
declare global {
interface Window {
Kakao: any;
}
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ko">
<body>
<NextSSRPlugin routerConfig={extractRouterConfig(ourFileRouter)} />
<Toaster />
{children}
</body>
<KakaoScript />
</html>
);
}
이제 전과 동일하게 원하는 곳에 카카오톡 공유 기능을 넣어주면 된다.
const shareKakao = () => {
const { Kakao } = window;
Kakao.Share.sendDefault({
objectType: 'feed',
content: {
title: 'Gila',
description: '길라와 같이 떠나자!',
imageUrl: shareImage,
link: {
mobileWebUrl: url,
},
},
buttons: [
{
title: '친구와 같이 매듭 묶기',
link: {
mobileWebUrl: url,
},
},
],
});
};
Next.js 14버전 app router에서는 이렇게 해주면 잘 동작한다.
구현하면서 에러상황
우리는 모바일 버전을 구현하고 있는데 크롬의 모바일 디스플레이로 카카오톡 공유하기를 실행하면 동작하지 않는다. 그렇다고 모바일에서 동작안하는 것은 아니다. 데스크탑 모드에서는 잘 동작하고 배포하고 모바일에서 실행해도 잘 된다. 모바일앱은 카카오에써 따로 지원하는 기능을 사용해줘야 하는 것 같다. 다른 사람들은 혼란스러워 하지 않기를...
next 14를 사용하고 server action을 사용할수록 참 편리한 것들이 많다. react-query도 사용할 필요가 없어지면서 데이터 fetching도 쉽고 한눈에 들어온다. 아직 100%이해했는가에 대해서는 확답을 못하겠지만 천천히 알아가는 중이다.
다음은 대시보드 페이지를 구현해보고 간단하게 포스팅할 예정이다. 생각보다 대시보드는 간단하기 때문이다. 나만의 것을 만들어 간다는 것은 생각보다 재미있는 것 같다. 더 완성도 있는 프로젝트를 만들어보고 싶다.