이번 포스팅에서는 Next-Auth
의 세션을 사용할 때 마주할 수 있는 이슈에 대해 간단한 경험을 공유하고자 합니다.
개발을 진행하던 중 위와 같이 '깜빡임 (ui 불일치)' 현상이 발생했습니다.
아래는 문제가 발생한 부분의 코드입니다.
"use client";
import { useSession } from "next-auth/react";
// import ...
export const MainHeader = () => {
const { data: session, status } = useSession();
return (
<Header
leftSlot={
<Link href="/">
<Image src={IMAGES.LOGO} alt="로고" width={100} height={30} />
</Link>
}
rightSlot={
<Flex align="center" justify="center" className="sm-max:gap-0 gap-3">
// ...
{status === "authenticated" ? (
<Link href={`/user/info/${session?.user?.id}`}>
<Avatar className="sm-max:hidden border border-border border-solid">
<AvatarImage src={session?.user?.image!} />
<AvatarFallback>{session?.user?.name}</AvatarFallback>
</Avatar>
</Link>
) : (
<div className="sm-max:hidden">
<LoginModal />
</div>
)}
</Flex>
}
>
{null}
</Header>
);
};
여러분들은 문제의 원인을 찾으셨나요? 해결 방법을 알아보기 전에 CSR
과 SSR
에 대해서 간단히 살펴보고 넘어가고자 합니다.
CSR (Client Side Rendering) 은 말 그대로 클라이언트 (브라우저) 단에서 렌더링을 처리하는 방식으로 동작 방식은 다음과 같습니다.
1. 사용자가 웹 사이트를 방문하면, 브라우저가 서버에 콘텐츠를 요청합니다.
2. 서버는 빈 뼈대만 있는 HTML을 응답합니다.
3. 브라우저가 연결된 JavaScript 링크를 통해 서버로부터 다시 JS 파일을 다운로드 합니다.
4. 받아온 JS를 통해 페이지를 만들어 렌더링 합니다.
SSR (Server Side Rendering) 은 렌더링을 서버 단에서 처리하는 방식으로 아래와 같이 동작합니다.
1. 사용자가 웹 사이트를 방문하면, 브라우저가 서버에 콘텐츠를 요청합니다.
2. 서버는 페이지에 필요한 데이터를 즉시 로드해 HTML을 생성하고, CSS를 적용하여 완성된 HTML과 JS를 브라우저에 반환합니다.
3. 브라우저는 이를 받아 렌더링 합니다. (JS를 읽기 전 까진 상호작용이 불가능 합니다.)
4. 브라우저가 JS 파일을 읽습니다.
5. JS를 실행하여 페이지를 상호작용 가능하게 만듭니다.
위 과정에서 3 - 5번 즉, 렌더링 된 HTML DOM 요소에 JS 파일을 덮어씌우는 과정을 hydration
이라고 합니다.
위의 문제 코드를 보면 useSession
훅을 통해 세션 정보에 접근하고 있습니다. 이 때 해당 컴포넌트는 use client
지시어를 사용했기 때문에 서버 측에서 렌더링 된 후, 다시 클라이언트 측에서 렌더링 됩니다.
이 과정에서 문제가 발생하는데요, 서버 측에서 렌더링 할 땐 useSession 훅의 세션 정보에 접근할 수 없기 때문에 로그인 버튼을 보여주었다가, 클라이언트 측에서 렌더링 할 때 세션 정보를 주입하여 ui가 달라지면서 깜빡이는 것 처럼 보이게 됩니다.
해결 방법은 간단합니다. 클라이언트 컴포넌트에서 발생하는 문제이기 때문에 이를 서버 컴포넌트로 바꿔주면 되는데요,
Next-Auth 에서는 서버 단에서 세션 정보에 접근할 수 있도록 getServerSession
함수를 제공하고 있습니다.
사용방법 또한 간단합니다. 우선 authOptions
를 작성해줍니다. 이는 Next-Auth를 사용한다면 이미 아래와 비슷하게 작성되어 있을겁니다.
// import ...
export const authOptions = {
secret: process.env.NEXTAUTH_SECRET,
providers: [
KakaoProvider({
clientId: process.env.KAKAO_CLIENT_ID as string,
clientSecret: process.env.KAKAO_CLIENT_SECRET as string,
}),
],
adapter: PrismaAdapter(prisma),
session: {
strategy: "jwt" as const,
},
callbacks: {
session: ({ session, token }: { session: Session; token: JWT }) => ({
...session,
user: {
...session.user,
id: token.sub,
},
}),
},
};
authOptions를 getServerSession 함수의 인자로 전달할 수 있습니다. 저는 세션 정보를 사용하는 곳이 많아 별도의 유틸 함수로 분리했습니다.
// app/shared/utils/getServerSession.ts
"use server";
import { getServerSession as getSession } from "next-auth/next";
import { authOptions } from "@/app/shared/lib/next-auth";
import { AuthOptions, Session } from "next-auth";
export const getServerSession = async (): Promise<Session | null> => {
const session = await getSession(authOptions as AuthOptions);
return session;
};
그 후 위의 클라이언트 컴포넌트에서 함수를 사용해 세션 정보를 받아올 수 있습니다.
// use client를 제거합니다.
import { getServerSession } from "@/app/shared/utils";
// import ...
export const MainHeader = async () => {
const session = await getServerSession();
// getServerSession 함수는 useSession에서 제공하는 `status` 값이 담겨 있지 않습니다.
// 따라서 session이 정상적으로 반환 되었을 경우 'authenticated' 로 설정해줍니다.
const status = session ? "authenticated" : "unauthenticated";
return (
<Header
leftSlot={
<Link href="/">
<Image src={IMAGES.LOGO} alt="로고" width={100} height={30} />
</Link>
}
rightSlot={
<Flex align="center" justify="center" className="sm-max:gap-0 gap-3">
// ...
{status === "authenticated" ? (
<Link href={`/user/info/${session?.user?.id}`}>
<Avatar className="sm-max:hidden border border-border border-solid">
<AvatarImage src={session?.user?.image!} />
<AvatarFallback>{session?.user?.name}</AvatarFallback>
</Avatar>
</Link>
) : (
<div className="sm-max:hidden">
<LoginModal />
</div>
)}
</Flex>
}
>
{null}
</Header>
);
};
로그인 버튼이 렌더링 되지 않고, 정상적으로 사용자 정보가 렌더링 되는 것을 확인할 수 있습니다.