시간이 지날 수록 학습량이 많아지다보니 이해하지 못하고 넘어가는 부분들도 많아지는 것 같다
아무리 급해도 제대로 이해하고 그 부분에서 어떤 기술이 왜 어떤 방식으로 쓰였는지에 대해서 더 자세히 알아 가는 과정들이 필요할 것 같다
그러니 차근차근 천천히 학습을 진행해보자
이번에 학습한 내용들은 본격적으로 로그인을 하고나서 서비스가 운영되는 페이지의 사이드바를 구현하는 것이 주요한 목적이었다.
과거에 이러한 방식은 간단하게 구현할 수 있었는데 이 프로젝트에서는 다양한 기술들이 사용되다보니 어려움을 느꼈다.
AuthContext
먼저 AuthContext.tsx파일을 만들어서 인증 기능을 추가했다.
이 파일에서 사용된 SessionProvider는 next-auth의 세션 관리를 제공하는 역할을 한다. 또한 현재 사용자의 전체 앱에서 사용할 수 있도록 만들어 줄 수 있다. 따라서 이 파일은 인증 상태를 전역으로 제공하여 하위 컴포넌트들이 인증 정보를 사용할 수 있도록 지원하는 컨텍스트 역할을 하는 것이다.
export default function AuthContext({ children }: AuthContextProps) {
return <SessionProvider>{children}</SessionProvider>;
}
그리고 이에 대한 해당 내용을 밖에 있는 layout.tsx 에 추가해서 ToasterContext를 감싸준다.
<AuthContext>
<ToasterContext />
{children}
</AuthContext>
이러한 이유는 알림기능을 전역에서 관리하기 위해 ToasterContext를 AuthContext와 함께 상위 컴포넌트에서 제공하기 위함이다. 이 설정은 메시지 알림을 사용하는 하위 컴포넌트들에게 손쉽게 전급할 수 있게 해주고, 알림의 상태와 생성을 전역에서 일관되게 관리할 수 있또록 도와준다.
useSession과 useRoute
1. useSession
useSession은 NextAuth에서 제공하는 훅으로 현재 사용자의 세션 정보를 가져와 상태를 확인할 수 있다. 여기서는 session객체를 통해 사용자의 인증 상태를 확인한다. session.status가 "authenticated"일 경우 사용자가 로그인된 상태임을 의미하며, 이 상태가 되면 사용자 페이지로 이동하게 된다.
const session = useSession();
useEffect(() => {
if (session?.status === "authenticated") {
router.push("/users");
}
}, [session?.status, router]);
2. useRouter
useRouter는 Next.js의 내장 라우팅 훅으로, 클라이언트 측에서 페이지 이동이나 라우팅을 제어할 수 있게 한다.
여기서는 세션 상태에 따라 users페이지로 리다이렉트하는 데 사용된다. 사용자가 로그인에 성공하거나 소셜로그인을 통해 인증되면, /users로 이동하여 해당 사용자가 볼 수 있는 메인 페이지로 안내된다.
const router = useRouter();
router.push("/users");
아참 자동으로 불러오면 next/router의 useRouter가 불러와지는데 최신 버전은 next/navigation이므로 주의해야한다.
회원 가입 후 바로 로그인
const onSubmit: SubmitHandler<FieldValues> = (data) => {
setIsLoading(true);
if (variant === "REGISTER") {
axios
.post("/api/register", data)
.then(() => signIn("credentials", data))
.catch(() => toast.error("Something went wrong"))
.finally(() => setIsLoading(false));
}
이번 시간에서는 사용자 경험을 개선한 것이 있었는데 ".then(()=>signIn("credentials",data))"구문을 추가했다.
이것은 회원가입을 한 후 다시 로그인 페이지로 돌아가서 로그인을 하는 것이 아니라 회원가입을 했을 때 바로 로그인이 되어 페이지가 넘어갈 수 있도록 하였다.
미들웨어
middleware.ts파일은 Next.js의 미들웨어 기능을 활용해 특정 페이지에 대한 접근 권한을 관리하는 역할을 한다. next-auth의 withAuth함수를 사용하여 사용자가 인증되지 않은 경우 로그인 페이지로 리디렉션하도록 설정한다.
1. withAuth
- withAuth는 next-auth라이브러리에서 제공하는 인증 미들웨어로, 사용자가 특정 페이지에 접근할 때 인증 상태를 확인하여 인증되지 않은 사용자는 지정된 페이지로 리디렉션할 수 있도록 설정해준다.
- withAuth함수는 미들웨어로서 특정 경로에 대한 요청을 가로채고 인증된 사용자만 접근을 허용하는 역할을 한다.
2. 페이지 리디렉션 설정
pages: {
signIn: "/",
},
- pages객체에서 signIn속성을 사용하여 인증되지 않은 사용자가 접근할 경우 리디렉션할 페이지를 지정한다. 여기서 signIn:"/"로 설정하였기 때문에 사용자가 로그인이 필요한 페이지에 접근하려고 하면 / 경로로 이동된다.
- 이는 일반적으로 로그인 페이지를 의미하며, 사용자가 인증 없이 접근하려고 할 때 로그인을 유도하는 역할을 한다.
3. config설정
export const config = {
matcher: ["/users/:path*"],
};
- config객체에서 matcher속성을 사용하여 미들웨어로 적용할 경로 패턴을 지정한다.
- "/users/:path*"는 users/뒤에 오는 모든 하위 경로를 포함하므로, /users, /users/profile, /users/setting등 /users로 시작하는 모든 경로가 미들웨어의 적용대상이된다.
- 따라서 /users관련 페이지에 접근하려는 사용자가 인증되지 않았다면 , /로그인 페이지로 리디렉션된다.
이 미들웨어가 작용되는 흐름을 살펴보면 사용자가 /user로 시작하는 페이지에 접근하레되면 이 미들웨어가 실행되고 withAuth가 사용자 인증 상태를 확인하고 인증된 경우에는 정상작동 아니면 signIn에서 지정한 "/"경로로 리디렉션되어 로그인 페이지로 이동. 로그인 후 사용자는 인증 상태에 따라 /users 페이지에 다시 접근 가능 후후 !
UserPage
이제 본격적으로 Userpage를 만들어보자
app/users라는 폴더를 새롭게 만들어서 그 안에 layout.tsx와 pages.tsx를 마련한다.
page.tsx에는 <Emptystate/>라는 컴포넌트를 만들어서 빈공간을 채워준다.
import Emptystate from "../components/EmptyState";
const Users = () => {
return (
<div className="hidden lg:block lg:pl-80 h-full">
<Emptystate />
</div>
);
};
export default Users;
<emptystate>는 특별한 건 없으니깐 굳이 설명은 안하겠음
그리고 user폴더 안에 layout.tsx를 만든다.
layout은 컴포넌트는 해당 경로 아래에 있는 페이지들의 기본구조로 사용된다. 이 코드에서는 Sidebar컴포넌트를 사용해 사이드바 메뉴와 메인컨텐츠(children)를 감싸는 레이아웃을 만들어준다.
import Sidebar from "../components/sidebar/Sidebar";
export default async function UserLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
// @ts-expect-error Server Component
<Sidebar>
<div className="h-full">{children}</div>
</Sidebar>
);
}
userlayout
- 이 컴포넌트는 비동기 함수로 정의되어 있다. next.js에서는 레이아웃이나 페이지에서 데이터를 서버에서 미리 가져오는 기능을 지원하는데, 이 비동기 함수가 그 목적을 위해 비동기 처리를 사용할 수있다.
- userlayout은 기본적으로 모든 user페이지의 레이아웃으로 사용된다. 이 파일의 기능은 export default로 내보내지므로, 해당 경로에 접근할 때 자동으로 이 레이아웃이 적용된다.
children 속성
- children속성은 이 레이아웃을 사용하는 페이지가 렌더링하는 콘텐츠를 의미한다. next.js에서는 layout.tsx파일이 해당 경로의 모든 자식 페이지의 커테이너가 되며, 자식 페이지들이 children으로 전달된다.
- @ts-expect-error Server Component이 주석은 타입스크립트 컴파일러에게 특정 에러를 무시하라고 지시한다.
- server component관련 에러를 무시하도록 설정되어 있는데 이는 넥스트13버전에서 도입된 기능으로, 서버 컴포넌트와 클라이언트 컴포넌트를 구분해 렌더링 효율을 높일 수 있다.
Hooks
본격적으로 사이드바를 구성하기 전에 커스텀훅을 만들었다.
app/hooks폴더를 만들어서 그 안에 useConversation.ts와 UseRoutes.ts를 구성하였다.
useConversation
useConversation은 특정대화에 대한 ID를 추출하고 대화가 열려있는지 여부를 확인하는 커스텀훅이다. 이 훅은 useParams를 통해 URL의 파라미터에서 대화 ID를 얻고, 해당ID가 있는지 여부를 통해 대화가 열려있는지를 확인한다.
1. const params = useParams()
- useParams는 next.js의 next/navigation에서 제공하는 훅으로, URL의 파라미터 값을 가져온다. 이 경우 URL에 conversationId라는 파라미터가 있다면 이를 가져온다.
- 예를 들어 URL이 /conversation/123이면, params.conversationId는 123이된다.
2. conversationId변수 생성
const conversationId=useMemo(()=>{...}, [params?.conversationId]);
- conversationId는 params에서 conversationId가 존재하는지 확인하고, 존재하지 않는다면 빈 문자열 "" 를 반환한다.
- useMemo를 사용해 params?.conversationID가 변경될 때만 conversationId를 새로 계산하여 최적화한다.
- 최종적으로 conversationId는 string타입으로 강제된다.
3. isOpen변수 생성
const isOpen = useMemo(()=>!!conversationId, [conversationId]);
- isOpen은 대화가 열려있는 지 확인하는 변수. conversationId가 빈 문자열이 아니라면 isOpen은 true가 된다.
- !!conversationId를 사용하여 conversationId가 존재할때 true, 없을 때 false로 변환한다.
4. 변환값 최적화
return useMemo(() => ({ isOpen, conversationId }), [isOpen, conversationId]);
- 최종적으로 isOpen과 conversationId를 객체로 묶어 반환한다.
- useMemo를 사용해 반환값이 isOpen이나 conversationId가 변경될 때만 새 객체로 계산되도록 하여 불필요한 재렌더링을 방지한다.
useRoute
useRoute는 여러 페이지나 기능(예:채팅, 사용자 목록, 로그아웃)에 대한 라우트 데이터를 생성하는 커스텀 훅이다. 각 라우트는 경로, 아이콘, 활성상태 및 클릭 시 동작을 포함하며, 이를 통해 사용자가 현재 위치에 따라 동적으로 활성화된 상태를 표시할 수 있다.
1. 현재 경로 및 대화 ID 가져오기:
- const pathname = usePathname();: 현재 URL 경로를 가져옵니다. 이 경로는 사용자가 어디에 있는지를 확인하여 해당 라우트를 활성화하는 데 사용됩니다.
- const { conversationId } = useConversation();: useConversation 훅을 통해 현재 대화의 ID를 가져옵니다. conversationId가 존재하면 Chat 라우트를 활성화하는 데 사용됩니다.
2. routes 배열 생성:
- useMemo를 사용해 routes 배열을 구성하여 성능을 최적화합니다. pathname이나 conversationId가 변경될 때만 배열이 다시 생성됩니다.
- routes 배열에는 각 페이지나 기능에 대한 정보가 포함됩니다.
각 라우트 객체에는 다음 속성이 있습니다:
- label: 라우트의 이름 또는 설명 텍스트입니다. 예를 들어 "Chat", "Users", "Logout" 등으로 설정되어 있습니다.
- href: 라우트의 경로입니다.
- "Chat": /conversation
- "Users": /users
- "Logout": 로그아웃 버튼으로, href는 "#"로 설정되어 있어 페이지 이동이 아닌 클릭 시 로그아웃을 수행합니다.
- icon: react-icons 라이브러리에서 가져온 아이콘으로, 각각의 라우트와 시각적으로 연관됩니다.
- HiChat은 "Chat" 아이콘, HiUsers는 "Users" 아이콘, HiArrowLeftOnRectangle는 "Logout" 아이콘으로 지정되어 있습니다.
- active: 현재 라우트가 활성화 상태인지 나타내는 속성으로, 이를 통해 사용자가 현재 위치에 따라 해당 라우트를 강조할 수 있습니다.
- "Chat": 현재 pathname이 /conversations이거나 conversationId가 존재할 때 활성화됩니다.
- "Users": 현재 pathname이 /users일 때 활성화됩니다.
3. 로그아웃 라우트의 onClick 이벤트:
- "Logout" 라우트에는 onClick 속성이 추가되어 있어 클릭 시 signOut()을 호출해 로그아웃이 수행됩니다.
4. 훅의 반환값:
- routes 배열을 반환하여 다른 컴포넌트에서 사용될 수 있도록 합니다. 이 배열을 통해 현재 위치에 따라 라우트가 동적으로 활성화되거나 비활성화된 상태로 렌더링될 수 있습니다.
SideBar
위에서 보여진것 처럼 User페이지에서 Sidebar는 레이아웃으로 고정되어 있는 형태로 나타나게 된다.
컴포넌트에 Sidebar 폴더를 만들어서 그 안에 sidebar를 구성하는 여러 컴포넌트들을 만들었다.
async function Sidebar({ children }: { children: React.ReactNode }) {
const currentUser = await getCurrentUser();
return (
<div className="h-full">
<DesktopSidebar currentUser={currentUser!} />
<MobileFooter />
<main className="lg:pl-20 h-full">{children}</main>
</div>
);
}
- Sidebar는 비동기 함수로 사용자 정보를 받아와 컴포넌트에 전달하고 화면 구조를 정의한다.
- children속성에는 메인 콘텐츠가 들어가면, 이 콘텐츠는 사이드바와 푸터 외에 Sidebar내에서 렌더링 된다.
- 나중에 화면의 동적 움직임에 따라서 <DesktopSidebar>와 <MobileFooter>가 움직이게 된다.
getCurrentUser
sidebar에서 사용된 getCurrentUser는 현재 로그인한 사용자의 세션 정보를 확인하고, 해당 사용자의 상세 정보를 데이터베이스에서 가져오는 역할을 한다. 이 함수는 사용자가 로그인된 상태인지 확인하고, 로그인된 상태라면 데이터베이스에서 이메일을 기반으로 해당 사용자의 정보를 조회하여 반환한다. 주요 기능은 prisma와 getSession을 활용해 현재 사용자를 식별하고 정보를 가져온다.
세션정보 확인
const session = await getSession();
if (!session?.user?.email) {
return null;
}
- getsession을 호출하여 사용자 세션 정보를 session변수에 할당한다.
- 세션 객체가 없거나 user정보 안에 email이 없다면 로그인 하지 않은 상태로 간주하고 null을 반환한다.
사용자 조회
const currentUser = await prisma.user.findUnique({
where: {
email: session.user.email as string,
},
});
- session.user.email을 통해 데이터베이스에서 해당 이메일을 가진 사용자를 조회한다.
- findUnique메소드는 where절을 사용하여 특정 조건을 만족하는 한 개의 레코드를 검색한다.
- 이메일을 기반으로 사용자를 찾는 방식이므로, 이메일이 데이터베이스에 없을 경우 null을 반환한다.
오류 처리
catch (error: any) {
return null;
}
- 이 하뭇는 오류가 발생할 경우 null을 반환하도록 처리되어 있어, 에러 상황에서도 null을 반환함으로써 프로그램이 중단되지 않게 한다.
DesktopSidebar
데스크탑 사이드바를 만들어보자
1. 첫번째 네비게이션
<ul role="list" className="flex flex-col items-center space-y-1">
{routes.map((item) => (
<DesktopItem
key={item.label}
href={item.href}
label={item.label}
icon={item.icon}
active={item.active}
onClick={item.onClick}
/>
))}
</ul>
- routes에 담긴 각 메뉴 항목을 순회하면서 DesktopItem컴포넌트를 렌더링한다.
- key, href, label, icon, active, onClick등의 속성을 전달해 개별 메뉴 항목을 정의한다.
- active속성은 현재 페이지 위치와 일치하는 메뉴에 강조 표시를 해준다.
2. 두번째 네비게이션 (아바타)
<div
onClick={() => setIsOpen(true)}
className="cursor-pointer hover:opacity-75 transition"
>
<Avatar user={currentUser} />
</div>
- 사이드바 하단에는 사용자의 Avatar컴포넌트를 표시하며, currentUser를 전달해 사용자의 프로필 이미지를 렌더링한다.
- 아바트를 클릭시 isOpen상태를 true로 변경해 추가적인 사용자 인터페이스를 열 수 있게 한다.
- hover:opacity-75로 마우스 오버 시 살짝 투명해지는 효과가 작동한다.
MobileFooter
이 컴포넌트는 모바일 화면 하단에 고정된 푸터 네비게이션을 렌더링 하며 useRoutes와 useConversation훅을 사용해 네비게이션 항목과 현재 대화 상태를 기반으로 동작을 결정한다. 사용자가 현재 대화중이라면 푸터 네비게이션을 숨긴다.
const MobileFooter = () => {
const routes = useRoutes();
const { isOpen } = useConversation();
if (isOpen) {
return null;
}
routes와 isOpen상태 가져오기
- routes: useRoutes훅에서 반환된 메뉴 항목 배열이다. href, active, icon, onClick등 필요한 정보가 포함되어 있다.
- isOpen: 현재 대화 창이 열려있는지 여부를 나타낸다. is Open이 true이면 푸터를 렌더링하지 않고 null을 반환해 숨긴다.