Next.js와 TypeScript로 웹개발 공부를 하면서 정말 간단하게 로그인 기능을 구현하여 main페이지로 라우팅하는 부분을 연습해보았다.
이번 글에서는 로그인 페이지부터 인증 후 메인 페이지로 넘어가는 과정까지, 전체 흐름을 코드와 함께 상세하게 설명하려고 한다.
특히 각 기술과 개념에 대한 설명을 충분히 제공하여 나와 같이 웹 개발을 배우는 학생들이 이해하기 쉽도록 작성했다.
프로젝트 구조 살펴보기
Next.js 프로젝트의 구조를 보면 다음과 같다.
├── app
│ ├── globals.css # 전역 스타일
│ ├── layout.tsx # 앱 레이아웃
│ ├── page.tsx # 로그인 페이지 (/)
│ └── main
│ └── page.tsx # 메인 페이지 (/main)
├── middleware.ts # 인증 미들웨어
├── utils
│ └── auth.ts # 인증 관련 유틸리티
└── package.json # 프로젝트 의존성
이 구조는 Next.js의 App Router 방식을 따르고 있다.
해당 부분을 공부하고 싶으면 아랫글을 참고하길 바란다.
[Next.js] App Router vs Pages Router
Pages Router 개념Pages Router는 Next.js의 전통적인 라우팅 시스템이다./pages 디렉토리 내의 파일 구조를 기반으로 라우트가 자동으로 생성되는 방식으로 동작한다.(직관적이고 간단하게 라우팅을 구현
bbin-guuuu.tistory.com
로그인 페이지 구현하기
로그인 페이지는 사용자가 처음 접하는 부분이므로 직관적이고 사용하기 쉽게 만드는 것이 중요하다.
다음은 app/page.tsx 파일에 구현한 로그인 페이지다!!
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// 간단한 유효성 검사
if (!email || !password) {
setError('이메일과 비밀번호를 모두 입력해주세요.');
return;
}
// 여기서는 간단한 예시로 특정 이메일/비밀번호 조합으로 로그인 성공 처리
// 실제로는 API 호출을 통한 인증 로직이 들어갈 것입니다
if (email === 'test@example.com' && password === 'password') {
// 로그인 성공 시 세션/쿠키 저장 (예시)
localStorage.setItem('isLoggedIn', 'true');
// 메인 페이지로 리다이렉트
router.push('/main');
} else {
setError('이메일 또는 비밀번호가 잘못되었습니다.');
}
};
return (
<div className="flex items-center justify-center min-h-screen bg-gray-100">
<div className="w-full max-w-md p-8 space-y-8 bg-white rounded-lg shadow-md">
<div className="text-center">
<h1 className="text-3xl font-extrabold text-gray-900">로그인</h1>
<p className="mt-2 text-sm text-gray-600">
계정에 로그인하세요
</p>
</div>
{error && (
<div className="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg">
{error}
</div>
)}
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
이메일
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
비밀번호
</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
className="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"
/>
<label htmlFor="remember-me" className="block ml-2 text-sm text-gray-900">
로그인 상태 유지
</label>
</div>
<div className="text-sm">
<a href="#" className="font-medium text-indigo-600 hover:text-indigo-500">
비밀번호를 잊으셨나요?
</a>
</div>
</div>
<div>
<button
type="submit"
className="flex justify-center w-full px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
로그인
</button>
</div>
</form>
<div className="text-center mt-4">
<p className="text-sm text-gray-600">
계정이 없으신가요?{' '}
<Link href="/register" className="font-medium text-indigo-600 hover:text-indigo-500">
회원가입
</Link>
</p>
</div>
</div>
</div>
);
}
이 코드에서 주목할 부분이 몇 가지 있다. 우선 'use client' 지시문인데, 이는 해당 컴포넌트가 클라이언트 컴포넌트임을 명시한다.
Next.js의 App Router에서는 기본적으로 모든 컴포넌트가 서버 컴포넌트로 동작하지만, useState와 같은 React 훅을 사용하기 위해서는 클라이언트 컴포넌트로 지정해야 한다.
또한 useRouter 훅을 사용하여 프로그래밍 방식으로 페이지 이동을 구현했다.
로그인이 성공하면 /main 경로로 사용자를 리다이렉트한다.
그리고 로그인 상태를 저장하기 위해 localStorage를 사용했는데, 실제 프로덕션 환경에서는 보안을 위해 HTTP-only 쿠키나 토큰 기반 인증을 사용하는 것이 좋다.
이 코드는 제출 이벤트를 처리하고 유효성 검사를 수행한 후, 간단한 하드코딩된 인증 로직을 실행한다.
실제 애플리케이션에서는 백엔드 API와의 통신을 통해 인증을 처리하게 될 것이다.
메인 페이지 구현하기
인증에 성공한 후 사용자가 접근하게 되는 메인 페이지는 app/main/page.tsx에 구현했다.
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
export default function MainPage() {
const router = useRouter();
const [username, setUsername] = useState('사용자');
useEffect(() => {
// 로그인 상태 확인
const isLoggedIn = localStorage.getItem('isLoggedIn');
if (!isLoggedIn) {
// 로그인되지 않은 상태면 로그인 페이지로 리다이렉트
router.push('/');
}
// 실제 애플리케이션에서는 여기서 사용자 정보를 가져오는 API 호출 등을 수행할 수 있습니다
}, [router]);
const handleLogout = () => {
// 로그아웃 처리
localStorage.removeItem('isLoggedIn');
router.push('/');
};
return (
<div className="min-h-screen bg-gray-100">
{/* 헤더 */}
<nav className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<div className="flex-shrink-0 flex items-center">
<span className="text-xl font-bold text-indigo-600">MyApp</span>
</div>
</div>
<div className="flex items-center">
<span className="mr-4 text-gray-700">안녕하세요, {username}님!</span>
<button
onClick={handleLogout}
className="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
로그아웃
</button>
</div>
</div>
</div>
</nav>
{/* 메인 콘텐츠 */}
<div className="py-10">
<header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold text-gray-900">대시보드</h1>
</div>
</header>
<main>
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="px-4 py-8 sm:px-0">
<div className="border-4 border-dashed border-gray-200 rounded-lg p-8">
<h2 className="text-2xl font-semibold mb-4">환영합니다!</h2>
<p className="text-gray-600">
로그인에 성공하셨습니다. 이 페이지는 로그인 후 접근할 수 있는 메인 페이지입니다.
</p>
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg font-medium text-gray-900">주요 기능 1</h3>
<p className="mt-2 text-sm text-gray-500">
이곳에 애플리케이션의 주요 기능에 대한 설명이 들어갑니다.
</p>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg font-medium text-gray-900">주요 기능 2</h3>
<p className="mt-2 text-sm text-gray-500">
이곳에 애플리케이션의 또 다른 주요 기능에 대한 설명이 들어갑니다.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
);
}
이 코드에서는 useEffect 훅을 사용하여 컴포넌트가 마운트될 때 사용자의 로그인 상태를 확인한다.
만약 로그인되지 않은 상태라면 로그인 페이지로 리다이렉트한다.
이는 인증되지 않은 사용자가 URL을 직접 입력하여 메인 페이지에 접근하는 것을 방지한다.
handleLogout 함수는 로그아웃 버튼 클릭 시 호출되며, localStorage에서 로그인 상태를 제거하고 사용자를 로그인 페이지로 리다이렉트한다.
메인 페이지에서는 간단한 대시보드 UI를 구현하여 사용자가 로그인 후 볼 수 있는 페이지를 보여준다.
이 페이지는 로그인한 사용자만 접근할 수 있어야 하므로, 클라이언트 측에서뿐만 아니라 서버 측에서도 인증 상태를 확인하는 것이 중요하다.
인증 로직 구현하기
인증 관련 로직은 utils/auth.ts 파일에 분리하여 구현했다.
export interface User {
id: string;
email: string;
name: string;
}
// 테스트용 더미 사용자
const DUMMY_USER: User = {
id: '1',
email: 'test@example.com',
name: '테스트 사용자',
};
export async function loginUser(email: string, password: string): Promise<User | null> {
// 실제로는 API 호출을 통해 인증 처리
return new Promise((resolve) => {
setTimeout(() => {
if (email === 'test@example.com' && password === 'password') {
resolve(DUMMY_USER);
} else {
resolve(null);
}
}, 500);
});
}
export function isAuthenticated(): boolean {
if (typeof window === 'undefined') {
return false;
}
return localStorage.getItem('isLoggedIn') === 'true';
}
이 코드에서는 TypeScript의 장점이 잘 드러난다.
User 인터페이스를 정의하여 사용자 객체의 구조를 명확히 했고,
loginUser 함수는 비동기 함수로 정의하여 Promise를 반환한다.
실제 애플리케이션에서는 이 함수가 백엔드 API와 통신하게 될 것이다.
isAuthenticated 함수는 클라이언트 측에서 사용자의 인증 상태를 확인하는 헬퍼 함수다.
서버 렌더링 중에는 window 객체가 없기 때문에, 함수 내에서 이를 체크하여 오류를 방지한다.
이러한 측면은 Next.js에서 서버 사이드 렌더링과 클라이언트 사이드 렌더링을 모두 고려해야 하는 상황에서 중요하다!!!
미들웨어를 활용한 라우트 보호
Next.js에서는 미들웨어를 사용하여 라우트 레벨에서 인증을 처리할 수 있다.
middleware.ts 파일을 활용하여 보호된 라우트에 대한 접근을 제어했다.
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 로그인 페이지와 공개 경로는 항상 접근 가능하도록 설정
const publicPaths = ['/', '/register', '/api/login'];
const isPublicPath = publicPaths.some(path =>
request.nextUrl.pathname === path ||
request.nextUrl.pathname.startsWith('/api/')
);
// 인증 토큰 확인
const authToken = request.cookies.get('authToken')?.value;
// 로그인한 사용자가 로그인/등록 페이지에 접근하려고 할 때
if (authToken && (request.nextUrl.pathname === '/' || request.nextUrl.pathname === '/register')) {
return NextResponse.redirect(new URL('/main', request.url));
}
// 로그인하지 않은 사용자가 보호된 페이지에 접근하려고 할 때
if (!authToken && !isPublicPath) {
return NextResponse.redirect(new URL('/', request.url));
}
return NextResponse.next();
}
미들웨어는 모든 라우트 요청이 처리되기 전에 실행되는 함수로, 인증 상태에 따라 리다이렉션이나 요청 수정을 할 수 있다.
이 코드에서는 쿠키를 통해 인증 상태를 확인하고, 인증되지 않은 사용자가 보호된 페이지에 접근하려고 할 때 로그인 페이지로 리다이렉트한다.
미들웨어는 서버 측에서 실행되기 때문에, 클라이언트에서의 인증 상태 검사와 함께 사용하면 보안을 강화할 수 있다.
이중 검사 방식으로 인증을 처리하는 것이 좋은 습관이다!!!
전체 흐름 이해하기
이제 모든 구성 요소를 이해했으니, 전체 인증 흐름을 살펴보자:
- 사용자가 로그인 페이지(/)에 접속한다.
- 유효한 이메일과 비밀번호를 입력하고 로그인 버튼을 클릭한다.
- handleSubmit 함수가 호출되어 입력 값을 검증한다.
- 인증이 성공하면, localStorage에 로그인 상태를 저장하고 사용자를 메인 페이지(/main)로 리다이렉트한다.
- 메인 페이지에서 useEffect를 통해 로그인 상태를 다시 확인한다.
- 사용자가 로그아웃 버튼을 클릭하면, localStorage에서 로그인 상태가 제거되고 로그인 페이지로 리다이렉트된다.
이 모든 과정에서 미들웨어는 각 라우트 요청을 인터셉트하여 인증 상태를 검증하고, 필요한 경우 리다이렉션을 수행한다.
이 흐름을 이해하면 다양한 인증 시나리오를 처리할 수 있다:
- 인증되지 않은 사용자가 보호된 페이지에 접근하려고 할 때
- 이미 로그인한 사용자가 로그인 페이지에 접근하려고 할 때
- 세션이 만료된 사용자를 다시 로그인 페이지로 리다이렉트할 때
이번 튜토리얼을 통해 Next.js와 TypeScript를 사용하여 기본적인 로그인 시스템을 구현해 보았다.
실제 프로덕션 환경에서는 JWT, OAuth 등의 더 안전한 인증 방식을 사용하는 것이 좋지만, 이 예제를 통해 인증 흐름의 기본 개념을 이해할 수 있을 것이다.
로그인 정보 (테스트용)
- 이메일: test@example.com
- 비밀번호: password
구현화면
로그인화면
메인화면
'Next.js' 카테고리의 다른 글
[Next.js] Next.js + TypeScript에서의 페이지 이동 방법 총정리 (0) | 2025.03.17 |
---|---|
[Next.js] 'use client'와 React Hooks의 관계 - SSR과 CSR의 경계 이해하기 (0) | 2025.03.16 |
[Next.js] App Router vs Pages Router (4) | 2025.03.13 |
[Next.js] SSR이란 무엇인가? - Next.js의 서버 사이드 렌더링 이해하기 (1) | 2025.03.12 |
[Next.js] styled-components(CSS in JS)를 Next.js에서 사용한다고??! 🚨 (1) | 2025.03.11 |