이번 포스팅에서는 서버단에서 이 페이지 ( 프로필 )를 어떻게 렌더링할지 알아보겠습니다. 그리고 getServerSideProps함수에서 어덯게 인증 기능을 구현할지 알아보도록 하겠습니다.
그리고 우리는 withApiSession이라는 Helper함수를 iron-session에서 제공하는 함수를 통해 구현했었습니다. 이는 쿠키의 일종의 암호화를 푼 다음 request안에 그 쿠키 내용들을 넣어주는 역할을 합니다.
그리고 우리는 이 외에도 withSsrSession이라는 함수를 만들도록 하겠습니다. iron session을 이용하면 API Route안에서 인증 할 수도 있고, getServerSideProps안에서도 할 수 있습니다.
iron-session은 암호화된 쿠키를 이용하는 Node.js stateless session utility입니다. JWT와 비슷하지만, 전송되는 토큰 정보(Payload)를 암호화 한다는 점에서 차이가 있습니다.
동작 방식은 JWT와 굉장히 흡사합니다. 다만 메타데이터를 완벽히 토큰에 저장하는 JWT와 약간 다르게 iron-session은 고유의 session을 가지고 있습니다.
1. 브라우저가 서버에 요청을 보냄
2. 서버에서 정보를 암호화한 쿠키를 만듦
3. 브라우저에게 응답을 보낼때 쿠키를 같이 보냄
4. 브라우저는 암호화된 쿠키를 가지고, 이후 요청에 쿠키를 같이 보냄
5. 암호화된 쿠키를 받은 서버는 이를 복호화해 정보를 활용함
여기에서 쿠키를 만들고, 브라우저에게 보내는 등의 작업을 저희가 직접 할 필요가 없습니다. 이 작업들을 도와주는 helper function들을 Iron-session에서 제공하기 떄문입니다.
즉 withSsrSession함수가 있으면 이제 getServerSideProps안에서 인증 기능을 사용할 수 있다는 겁니다.
/pages/profile/index.tsx
import type { NextPage, NextPageContext } from "next";
import Layout from "@components/layout";
import Link from "next/link";
import useUser from "@libs/client/useUser";
import { Review, User } from "@prisma/client";
import useSWR, { SWRConfig } from "swr";
import { cls } from "@libs/client/utils";
import { withSsrSession } from "@libs/server/withSession";
interface ReviewWithUser extends Review {
createdBy: User;
}
interface ReviewsResponse {
ok: boolean;
reviews: ReviewWithUser[];
}
const Profile: NextPage = () => {
const { user } = useUser();
const { data } = useSWR<ReviewsResponse>("/api/reviews");
return (
<Layout title="나의 계정" hasTabBar>
<div className="py-3 px-4">
<div className="flex items-center space-x-3">
{user?.avatar ? (
<img
src={`https://imagedelivery.net/_SMYXsMOOEvTYhYAAKoRCQ/${user.avatar}/avatar`}
className="h-16 w-16 rounded-full bg-slate-500"
/>
) : (
<div className="h-16 w-16 rounded-full bg-slate-500" />
)}
<div className="flex flex-col">
<span className="font-medium text-gray-800">
{user?.name}
</span>
<Link href="/profile/edit">
<a className="font-sm text-gray-700">
Edit profile →
</a>
</Link>
</div>
</div>
<div className="mt-10 flex justify-around">
<Link href="/profile/sold">
<a className="flex flex-col items-center">
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-orange-500 text-white">
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
></path>
</svg>
</div>
<span className="mt-2 text-sm font-medium text-gray-700">
판매내역
</span>
</a>
</Link>
<Link href="/profile/bought">
<a className="flex flex-col items-center">
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-orange-500 text-white">
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
></path>
</svg>
</div>
<span className="mt-2 text-sm font-medium text-gray-700">
구매내역
</span>
</a>
</Link>
<Link href="/profile/loved">
<a className="flex flex-col items-center">
<div className="aligned-center flex h-14 w-14 items-center justify-center rounded-full bg-orange-500 text-white">
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
></path>
</svg>
</div>
<span className="mt-2 text-sm font-medium text-gray-700">
관심목록
</span>
</a>
</Link>
</div>
{data?.reviews.map((review) => (
<div key={review.id} className="mt-12">
<div className="flex items-center space-x-4">
<div className="h-12 w-12 rounded-full bg-slate-400" />
<div>
<h4 className="text-sm font-bold text-gray-800">
{review?.createdBy?.name}
</h4>
<div className="flex items-center">
{[1, 2, 3, 4, 5].map((star) => (
<svg
key={star}
className={cls(
"h-5 w-5",
review.score >= star
? "text-yellow-400"
: "text-gray-400"
)}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
))}
</div>
</div>
</div>
<div className="mt-4 text-sm text-gray-600">
<p>{review?.review}</p>
</div>
</div>
))}
</div>
</Layout>
);
};
const Page: NextPage = () => {
return (
<SWRConfig>
<Profile />
</SWRConfig>
);
};
export const getServerSideProps = withSsrSession(
async function ({ req }: NextPageContext) {
console.log(req?.session.user);
return {
props: {},
};
}
);
export default Page;
이렇게 profile페이지를 구성해 보았습니다. 이전에 만든 withSsrSession을 불러와서 getServerSideProps에 감싸주었습니다. 그리고 서버단의 console에 ctx의 Iron-session이 담아준 session의 user정보를 찍어보도록 하겠습니다.
withSsrSession이 하는 일은 iron-session에게 req오브젝트를 제공해서, iron-session은 쿠키를 가져오고, 쿠키를 해독한 다음, 그 쿠키의 결과를 req?.session.user 내부에 넣어주는 것입니다.
그리고 이제 서버사이드에서 User정보를 불러와서 진짜로 주입해보도록 하겠습니다.
/pages/profile/index.tsx
import type { NextPage, NextPageContext } from "next";
import Layout from "@components/layout";
import Link from "next/link";
import useUser from "@libs/client/useUser";
import { Review, User } from "@prisma/client";
import useSWR, { SWRConfig } from "swr";
import { cls } from "@libs/client/utils";
import { withSsrSession } from "@libs/server/withSession";
import client from "@libs/client/client";
interface ReviewWithUser extends Review {
createdBy: User;
}
interface ReviewsResponse {
ok: boolean;
reviews: ReviewWithUser[];
}
const Profile: NextPage = () => {
const { user } = useUser();
const { data } = useSWR<ReviewsResponse>("/api/reviews");
return (
<Layout title="나의 계정" hasTabBar>
<div className="py-3 px-4">
<div className="flex items-center space-x-3">
{user?.avatar ? (
<img
src={`https://imagedelivery.net/_SMYXsMOOEvTYhYAAKoRCQ/${user.avatar}/avatar`}
className="h-16 w-16 rounded-full bg-slate-500"
/>
) : (
<div className="h-16 w-16 rounded-full bg-slate-500" />
)}
<div className="flex flex-col">
<span className="font-medium text-gray-800">
{user?.name}
</span>
<Link href="/profile/edit">
<a className="font-sm text-gray-700">
Edit profile →
</a>
</Link>
</div>
</div>
<div className="mt-10 flex justify-around">
<Link href="/profile/sold">
<a className="flex flex-col items-center">
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-orange-500 text-white">
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
></path>
</svg>
</div>
<span className="mt-2 text-sm font-medium text-gray-700">
판매내역
</span>
</a>
</Link>
<Link href="/profile/bought">
<a className="flex flex-col items-center">
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-orange-500 text-white">
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
></path>
</svg>
</div>
<span className="mt-2 text-sm font-medium text-gray-700">
구매내역
</span>
</a>
</Link>
<Link href="/profile/loved">
<a className="flex flex-col items-center">
<div className="aligned-center flex h-14 w-14 items-center justify-center rounded-full bg-orange-500 text-white">
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
></path>
</svg>
</div>
<span className="mt-2 text-sm font-medium text-gray-700">
관심목록
</span>
</a>
</Link>
</div>
{data?.reviews.map((review) => (
<div key={review.id} className="mt-12">
<div className="flex items-center space-x-4">
<div className="h-12 w-12 rounded-full bg-slate-400" />
<div>
<h4 className="text-sm font-bold text-gray-800">
{review?.createdBy?.name}
</h4>
<div className="flex items-center">
{[1, 2, 3, 4, 5].map((star) => (
<svg
key={star}
className={cls(
"h-5 w-5",
review.score >= star
? "text-yellow-400"
: "text-gray-400"
)}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
))}
</div>
</div>
</div>
<div className="mt-4 text-sm text-gray-600">
<p>{review?.review}</p>
</div>
</div>
))}
</div>
</Layout>
);
};
const Page: NextPage<{ profile: User }> = ({ profile }) => {
return (
<SWRConfig
value={{
fallback: {
"api/users/me": { ok: true, profile },
},
}}
>
<Profile />
</SWRConfig>
);
};
export const getServerSideProps = withSsrSession(
async function ({ req }: NextPageContext) {
const profile = await client.user.findUnique({
where: {
id: req?.session.user?.id,
},
});
console.log("profile serverside >> ", profile);
return {
props: {
profile: JSON.parse(JSON.stringify(profile)),
},
};
}
);
export default Page;
상품 목록과 같은 것을 보여주는 경우는 사용자 인증해줄 게 없기 때문에 getServerSideProps안에서 인증 요청은 일어나지 않고 있습니다. 하지만 프로필 페이지에서는 필요했습니다. 그래서 유저 정보를 서버사이드에서 렌더링 해 준것입니다. 게다가 useSWR을 이용해서 fallback을 이용해서 fallback안에 컴포넌트의 캐시 초기값도 제공해 주고 있습니다.
그리고 useUser훅의 useEffect코드는 백그라운드에서 실행되기 때문에 유저는 로딩 상태를 보여주지 않아도 됩니다.
'Web > NextJs' 카테고리의 다른 글
[ Next.js - DeepDive ] - getStaticProps (0) | 2022.08.06 |
---|---|
[ Next.js - DeepDive ] - Blog Section (0) | 2022.08.06 |
[ Next.js - DeepDive ] - SSR + SWR (0) | 2022.08.06 |
[ Next.js - DeepDive ] - getServerSideProps (0) | 2022.08.06 |
[ Next.js - DeepDive ] - Script Component (0) | 2022.08.06 |