관리자 전용 로그인 기능을 구현했다.
나만 사용하는 페이지 이긴 하지만, 수정이 필요할때마다 재배포 하는 작업이 불필요하다고 생각했고, 로그인 관련 기능을 구현해보고 싶다고도 생각했다.
로그인 기능이 필요해 -> JWT를 사용해보자 -> NextJs 에서 JWT를 쉽게 사용하려면 jose를 많이 쓴다더라 -> jose를 이용해 JWT 인증 로직을 구현하자 의 방식으로 생각이 흘러갔던것 같다.
Edge Runtime과의 호환성
Next.js의 Middleware는 Edge Runtime에서 동작한다. 기존에 흔히 쓰이던 jsonwebtoken은 Node.js 표준 API를 사용하기 때문에 Edge 환경에서 에러가 발생할 확률이 높은데, jose는 Web Crypto API를 기반으로 설계되어 Edge 환경에서도 가볍고 빠르게 동작한다고 한다.
XSS 공격 방어
토큰을 LocalStorage에 저장하면 자바스크립트로 탈취가 가능하다. 이를 방지하기 위해 서버에서만 읽을 수 있는 httpOnly 옵션이 필요하기도 했고 vercel은 https를 배포시 적용 해 주기에 설정이 용이할꺼라 생각했다.
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
try {
const { username = "", password = "" } = await req.json();
if (
username === process.env.NEXT_ID &&
password === process.env.NEXT_SECRET
) {
return NextResponse.json(
{ ok: true, message: "Login successful" },
{ status: 200 },
);
}
return NextResponse.json(
{ ok: false, message: "아이디 또는 비밀번호 오류입니다." },
{ status: 401 },
);
} catch (error) {
console.error("Login API error:", error);
return NextResponse.json(
{ ok: false, message: "Internal server error" },
{ status: 500 },
);
}
}
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_BASE_URL}/api/login`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
},
);
if (!response.ok) {
redirect("/login?error=invalid");
}
const token = await new SignJWT({ username })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("1h")
.sign(new TextEncoder().encode(process.env.JWT_SECRET_KEY));
const cookiesStore = await cookies();
cookiesStore.set("access_token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
maxAge: 3600,
});
} catch (error) {
console.error("Login SeverAction error:", error);
redirect("/login?error=server");
}
로그인 기능을 만들면서 가장 많이 고민하고 수정했던 세 가지 핵심 포인트이다.
🚩 Trouble 1: API Route와 Server Action 사이의 쿠키 설정 혼선
처음에는 클라이언트 사이드에서 fetch로 로그인 API를 호출하고, API 핸들러에서 쿠키를 저장하려고 했다.
하지만 Next.js 15에서는 Server Action을 활용하는 것이 클라이언트와의 통신 비용을 줄이고 유지보수가 간편하겠다고 생각이 들었고 로그인을 하는 로직과 JWT토큰을 발급하는 로직을 나눴다.
문제: API Route에서 NextResponse.cookies.set을 사용할 때와 Server Action에서 next/headers의 cookies()를 사용할 때의 일관성 문제.
해결: ItsMe 프로젝트에서는 보다 현대적인 방식인 Server Action으로 로그인 로직을 일원화했습니다. cookies().set()을 사용하여 서버 사이드에서 직접 쿠키를 기록하도록 변경했다.
🚩 Trouble 2: Middleware의 필요성과 설정
처음엔 미들웨어 자체에 대한 이해도도 많이 낮았고, 그래서 중요성을 몰랐다. 그러다가 Admin page의 접근 권한 제어를 middleware를 통해 할 수 있다는 걸 알았고 추가하게 되었다. 그 과정에서 config 설정의 문제로 login 페이지를 수동 접근하는 페이지 조차 홈으로 리다이렉트 되었고, 이를 해결하고자 했다.
해결: middleware.ts의 config 설정을 통해 /admin 에 관한 모든 경로에 한해 인증이 없다면 /login 으로 리다이렉트 하도록 설정했다.
인강에서와는 달리 직접 문서들을 찾아봐야 했다.
npm 라이브러리에 들어가서 jose에 관해 찾아보고, 공식문서를 찾아가 인증방식에 대해 찾아봤다.
추상화의 중요성: 단순한 로그인 구현이라고 생각했는데, 코드를 작성하면서 로그인 자체의 기능과 JWT를 생성/검증 로직을 분리하는게 확실히 나은것 같다.
middleware, serverAction, login 기능등에 대해 어떻게 하는 거네... 하고 알고 있다가 직접 써보면서 조금 더 알게 되었던 것 같다. 다음에 개발할땐 조금 덜 헤매고 개발 할 수 있을 것 같다.