recoil은 최상위 컴포넌트를 RecoilRoot
로 감싸주면 되는데, Next.js는 SSR을 지원하기 때문에 layout.tsx에서 바로 RecoilRoot
로 감싸면 아래와 같은 오류가 발생한다.
createContext 는 오직 client component에서만 사용가능하므로, 파일의 최상단에 'use client'를 작성하여 사용하라는 에러이다.
여기서, createContext는 문자 그대로 Context를 생성한다는 의미로 추론된다. 즉, RecoilRoot를 사용하면, 무엇인지 모를 Context라는 것이 생성되는데, 여기서 Context(컨텍스트)는 자바스크립트의 실행 컨텍스트와 같은 역할을 한다. 실행 컨텍스트는 자바스크립트의 소스 코드를 실행하기 위한 정보를 담고 있는 공간을 말한다. 즉, RecoilRoot는 Context를 생성하며, 이 Context는 Recoil을 사용하기 위한 정보가 담긴 공간이라고 볼 수 있다.
결국 'use client'를 파일의 최상단에 위치시키면 해결되는데, 이렇게 하게 되면 사실 Next.js를 사용하는 의미가 퇴색된다. 왜냐하면, 우리는 RecoilRoot로 최상위 컴포넌트를 감싸줌으로써 Recoil을 적용하려 하기 때문에, 최상위 컴포넌트가 작성된 layout.tsx에 'use client'를 작성하게 되면, 사실상 모든 하위의 컴포넌트가 클라이언트 컴포넌트가 되기 때문이다.
또한, layout.tsx에 'use client'를 작성하게 되면, layout.tsx는 서버 사이드 컴포넌트이기 때문에 또 오류가 뜬다.
따라서, Next.js에서 recoil을 사용하기 위해서는 recoil 상태를 사용하는 컴포넌트의 최상위 부모트리에 RecoilRoot
로 감싼 RecoilWrapper.tsx를 만들어서 use client를 작성해주고, layout.tsx에서 RecoilWrapper로 감싸주면 해결된다.
"use client";
import { RecoilRoot } from "recoil";
interface RecoilRootWrapperProps {
children: React.ReactNode;
}
export default function RecoilRootWrapper({
children,
}: RecoilRootWrapperProps) {
return <RecoilRoot>{children}</RecoilRoot>;
}
// 수정금지
import type { Metadata } from "next";
import "../globals.css";
import Container from "../components/Container";
import StyledComponentsRegistry from "@/lib/registry";
import ThemeProviderWrapper from "./ThemeProviderWrapper";
import RecoilRootWrapper from "./RecoilWrapper";
export const metadata: Metadata = {
title: "MoZip",
description: "Making club management and recruiting more convenient",
icons: {
icon: "/logo_TapImg.png",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<RecoilRootWrapper>
<StyledComponentsRegistry>
<ThemeProviderWrapper>
<Container>{children}</Container>
</ThemeProviderWrapper>
</StyledComponentsRegistry>
</RecoilRootWrapper>
</body>
</html>
);
}
이렇게해서 잘 해결이 된 줄로만 알았는데 Next.js에서 recoil로 로그인/로그아웃 전역상태관리를 구현 코드를 작성하고 났더니, 아래와 같은 오류가 발생했다.. (나한테 왜글애..)
이 오류는 보통 아래 4가지 이유로 발생한다고 한다..
- React 버전 불일치
- React와 Next.js의 use client 문제
- 의존성 설치 문제
- Recoil의 RecoilRoot가 제대로 설정되지 않았을 때 발생
나의 경우, 1번과 3번은 아닌 것 같고 2번과 4번은 아까 문제상황1에서 해결했다고 생각을 했다. 더 찾아보니 Next.js에서 클라이언트 컴포넌트("use client")와 서버 컴포넌트가 섞여 있을 때 발생할 가능성도 높고,
RecoilRootWrapper 같은 클라이언트 전용 컴포넌트가 layout.tsx처럼 서버 컴포넌트에서 호출되면 충돌이 발생할 수 있다고 했다.
useEffect를 활용해 Next.js의 클라이언트 상태 오류 방지하는 방법을 써봤는데도 계속해서 해결이 되지 않았고,
recoil이 Next.js와 호환이 좋지 않기도 하고 위의 문제상황1에서도 충분히 겪었다고 생각하여..
결국 상태관리 라이브러리를 바꾸기로 결정했다...😭
빠른 시일 내에 구현을 했어야 했기 때문에 Next.js, TS와 호환도 좋고, 쉽게 적용할 수 있는 Zustand를 선택했다.
import { create } from 'zustand'
interface LoginState {
isLoggedIn: boolean
userId: string | null
setLogin: (userId: string, accessToken: string) => void
logout: () => void
}
export const useLoginStore = create<LoginState>((set) => ({
isLoggedIn: false,
userId: null,
setLogin: (userId, accessToken) => {
localStorage.setItem('userId', userId);
localStorage.setItem('accessToken', accessToken);
set({ isLoggedIn: true, userId });
},
logout: () => {
localStorage.removeItem('userId');
localStorage.removeItem('accessToken');
set({ isLoggedIn: false, userId: null });
}
}))
const handleLogin = async () => {
console.log('로그인 시도:', { email, password }); // 입력값 확인
try {
const response = await postLogin(userData);
console.log('로그인 성공 응답:', response); // 성공 응답 확인
// Zustand store 업데이트
setLogin(response.userId, response.accessToken);
console.log('로그인 상태 업데이트 완료');
router.push('/');
} catch (error: any) {
console.log('로그인 실패 응답:', error.response); // 실패 응답 확인
console.log('에러 메시지:', error.response?.data); // 에러 메시지 확인
setErrormessage('아이디 또는 비밀번호가 일치하지 않습니다.');
}
};
export default function DefaultLogin({ setCurrentView, setNextView }: DefaultLoginProps) {
const router = useRouter();
const setLogin = useLoginStore(state => state.setLogin); //zustand
const handleLogin = async () => {
if (email === '') {
alert('아이디를 입력하세요.');
return;
}
if (password === '') {
alert('비밀번호를 입력하세요.');
return;
}
const userData = {
email: email,
password: password,
};
try {
const { userId, accessToken } = await postLogin(userData);
// Zustand store로 로그인 상태 관리
setLogin(userId, accessToken);
router.push('/');
} catch (error: any) {
console.error('에러: ', error.response);
setErrormessage('아이디 또는 비밀번호가 일치하지 않습니다.');
alert('로그인에 실패했습니다.');
}
};
export default function HomeHeader({ setActiveTab }: HomeHeaderProps) {
const { isLoggedIn, logout } = useLoginStore();
const handleAuthClick = () => {
if (isLoggedIn) {
logout();
} else {
router.push("/memberlogin");
}
};
return (
// ... JSX ...
<CustomFont>{isLoggedIn ? '로그아웃' : '로그인'}</CustomFont>
// ... 나머지 JSX
);
}
export default function Club({ setActiveTab }: ClubProps) {
const { isLoggedIn, userId } = useLoginStore();
useEffect(() => {
if (isLoggedIn) {
// 클럽 데이터 fetch...
}
}, [isLoggedIn]);
if (!isLoggedIn) {
return <MessageContainer>로그인 후 이용 가능합니다</MessageContainer>;
}
// ... 나머지 컴포넌트 로직
}
참고자료