세션 기반의 로그인 프로젝트를 만들기 위한 프론트엔드, 백엔드의 구조를 정리해보았다.
1. 프론트엔드
/src
├── api # API 요청 관련 코드
│ ├── authApi.js # 로그인 및 로그아웃 API 요청
│ ├── userApi.js # 사용자 정보 조회 API 요청
│
├── components # 공통 UI 컴포넌트
│ ├── InputField.js # 입력 필드 컴포넌트
│
├── context # 전역 상태 관리
│ ├── AuthContext.js # 로그인 상태 관리
│
├── pages # 주요 페이지
│ ├── LoginPage.js # 로그인 페이지
│ ├── HomePage.js # 로그인 후 이동할 메인 페이지
│
├── utils # 공통 유틸 함수
│ ├── axiosInstance.js # Axios 설정 (API 기본값 설정)
│
├── App.js # 메인 애플리케이션 (라우터 포함)
├── index.js # React 진입점
| 폴더명 | 역할 |
| api/ | API 요청을 담당하는 함수들을 모아둠 - 백엔드에 요청할 api를 엔드포인트에 따라 분류 |
| components/ | 공통 UI 컴포넌트 (입력 필드, 버튼, 상단바 등) - 이후에 pages/ 에서 import 후 호출에 사용 |
| pages/ | 로그인 및 메인 페이지와 같은 주요 화면 - App.js 에서 Router를 사용해서 각 엔드포인트 주소마다 렌더링되는 페이지를 정해서 사용 |
| context/ | AuthContext를 이용해 로그인 상태를 전역적으로 관리 - 로그인 상태 등의 전역 정보를 localStorge에서 저장하여 처리하는 것도 가능하지만, creatContext 와 useContext 를 이용해 pages/에서 전역 정보를 불러올 수 있음 |
| utils/ | Axios 인스턴스 및 공통 유틸 함수 정리 - baseURL: "http://localhost:8080", withCredentials: true 와 같이 api/에서 공통적으로 사용되는 함수 정리 |
- context/AuthContext.js (전역 로그인 상태 관리)
import { createContext, useState, useEffect } from "react";
// AuthContext 생성
export const AuthContext = createContext(null); // 초기값을 null로 하여 AuthContext 선언
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
// 애플리케이션 시작 시 localStorage에서 사용자 정보 로드
useEffect(() => {
const storedUsername = localStorage.getItem("username");
if (storedUsername) {
setUser({ username: storedUsername });
}
}, []);
return (
<AuthContext.Provider value={{ user, setUser }}>
{children}
</AuthContext.Provider>
);
};
createContext란?
createContext는 React의 Context API를 활용하여 전역 상태를 관리할 수 있도록 하는 기능으로 여러 컴포넌트에서 공유할 수 있는 전역적인 상태(보통 로그인 상태와 같은 상태 저장에 사용)를 제공한다.
AuthProvider란?
AuthContext.Provider를 통해 모든 하위 컴포넌트에서 AuthContext에 접근할 수 있도록 한다. 앱이 실행되면 로컬 저장소에서 사용자 정보를 불러와 user 상태를 설정한다.
- App.js
import React from "react";
import { AuthProvider } from "./context/AuthContext";
import LoginPage from "./pages/LoginPage";
import HomePage from "./pages/HomePage";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
function App() {
return (
<AuthProvider>
<Router>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/home" element={<HomePage />} />
</Routes>
</Router>
</AuthProvider>
);
}
export default App;
App.js 에서 앱 전체를 AuthProvider로 감싸고, user 상태를 관리함.
이제 AuthProvider 내부의 모든 컴포넌트가 AuthContext를 사용할 수 있음.
- utils/axiosInstance.js (Axios 설정)
import axios from "axios";
const instance = axios.create({
baseURL: "http://localhost:8080", // 백엔드 API 주소
withCredentials: true, // 세션 기반 인증을 위해 필요
});
export default instance;
API 요청 시 세션 인증을 유지하도록 설정.
- components/InputField.js (입력 필드 컴포넌트)
import React from "react";
const InputField = ({ label, type, name, value, onChange }) => (
<div>
<label>{label}</label>
<input type={type} name={name} value={value} onChange={onChange} required />
</div>
);
export default InputField;
로그인 입력 필드를 재사용 가능하도록 구현. 이후 pages/에서 입력 필드를 재사용
- pages/LoginPage.js
import React, { useState, useContext } from "react";
import { useNavigate } from "react-router-dom";
import { login } from "../api/authApi";
import { AuthContext } from "../context/AuthContext";
import InputField from "../components/InputField";
const LoginPage = () => {
const navigate = useNavigate();
const { setUser } = useContext(AuthContext); // AuthContext에서 상태 관리
const [formData, setFormData] = useState({ username: "", password: "" });
const [message, setMessage] = useState("");
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
const result = await login(formData.username, formData.password);
setUser(result.data); // 전역 상태 업데이트
localStorage.setItem("username", result.data.username); // 로컬 저장
navigate("/home"); // 로그인 성공 후 이동
} catch (error) {
setMessage("로그인 실패: " + error.message);
}
};
return (
<form onSubmit={handleSubmit}>
<h2>로그인</h2>
<InputField label="아이디" type="text" name="username" value={formData.username} onChange={handleChange} />
<InputField label="비밀번호" type="password" name="password" value={formData.password} onChange={handleChange} />
<button type="submit">로그인</button>
{message && <p style={{ color: "red" }}>{message}</p>}
</form>
);
};
export default LoginPage;
const { setUser } = useContext(AuthContext); 를 통해 전역에서 사용자 로그인 상태를 관리.
api/ 의 로그인 요청 await login( formData.username, formData.password ) 로 로그인을 성공하면 사용자의 아이디를 localStorage에 저장(이전에 CreateContext에서 사용자 아이디로 로그인 상태를 관리)
- api/authApi.js (로그인 및 로그아웃 API)
import axios from "../utils/axiosInstance";
// 로그인 요청
export const login = async (username, password) => {
const response = await axios.post("/auth/login", { username, password });
return response.data; // { message, data: { userId, username, name } }
};
// 로그아웃 요청
export const logout = async () => {
await axios.post("/auth/logout");
};
pages/ 나 components/ 에서 요청하는 함수를 정의. login 함수를 요청하면 /auth/login 에 post 요청을 보냄. 이후에 백엔드에서 /auth/login 의 post 요청에서 로그인을 수행하고 json 형식의 사용자의 로그인 정보를 반환.
{
"message": "로그인 성공",
"data": {
"userId": 1,
"username": "testUser",
"name": "홍길동"
}
}
- pages/HomePage.js (홈 페이지)
import React, { useContext } from "react";
import { AuthContext } from "../context/AuthContext";
import { logout } from "../api/authApi";
import { useNavigate } from "react-router-dom";
const HomePage = () => {
const { user, setUser } = useContext(AuthContext);
const navigate = useNavigate();
const handleLogout = async () => {
await logout();
localStorage.removeItem("username"); // 로컬 저장소에서 제거
setUser(null); // 상태 초기화
navigate("/login"); // 로그인 페이지로 이동
};
return (
<div>
<h1>환영합니다, {user ? user.username : "Guest"}!</h1>
<button onClick={handleLogout}>로그아웃</button>
</div>
);
};
export default HomePage;
HomePage에서 로그인 이후에 사용자의 로그인 정보를 확인하고, 로그아웃 기능도 제공. 로그아웃시 logout 요청을 보내고, localStorage에서 사용자의 이름을 제거함.
2. 백엔드
/src/main/java/com/example/auth
├── controller # API 요청 처리 (프론트엔드와 직접 통신)
│ ├── AuthController.java
│
├── dto # 요청 및 응답 데이터 전송 객체 (Request/Response DTO)
│ ├── LoginRequestDto.java
│ ├── UserDto.java
│
├── entity # 데이터베이스 테이블과 매핑되는 클래스 (JPA 엔티티)
│ ├── User.java
│
├── repository # 데이터베이스 접근 (JPA 사용)
│ ├── UserRepository.java
│
├── service # 비즈니스 로직 처리 (핵심 기능)
│ ├── AuthService.java
│ ├── AuthServiceImpl.java
│
├── config # 보안, 세션 설정 및 전역 환경 설정
│ ├── SecurityConfig.java
│
├── exception # 예외 처리 및 전역 에러 핸들링
│ ├── GlobalExceptionHandler.java
│
├── Application.java # Spring Boot 메인 실행 파일
| 폴더명 | 역할 |
| controller/ | 프론트엔드와의 HTTP 요청을 처리하는 API 엔드포인트 - 프론트엔드와 직접 통신하는 API 엔드포인트 제공.(/auth/login의 post 요청이 들어오면 여기서 처리) - 요청을 받아 service/에 로직을 위임하고 결과를 반환. |
| dto/ | 프론트엔드와 데이터 송수신을 위한 데이터 전송 객체 (DTO) - entity/와 다르게 DTO는 비밀번호를 포함하지 않음. |
| entity/ | 데이터베이스의 테이블과 매핑되는 클래스 (JPA 엔티티) - JPA를 통해 데이터베이스 테이블과 1:1 매핑되는 클래스. - @Entity 와 update 설정을 사용하여 데이터베이스 클래스를 자동으로 생성되게 할 수 있음. |
| repository/ | 데이터베이스와의 직접적인 CRUD 연산을 수행하는 JPA 인터페이스 - JpaRepository를 사용하여 CRUD 기능을 자동으로 제공 - DB에 직접 접근하는 코드 없이 간단한 메서드 호출만으로 CRUD 처리 가능. |
| service/ | 비즈니스 로직을 처리하는 계층 (컨트롤러와 리포지토리 중간 역할) - repository/ 에서 데이터를 가져와서 로직을 수행하는 계층 - 보통 Service 와 SeviceImpl 을 두고 상속받게하여 구성 |
| config/ | 보안, 세션, CORS 등 전역 환경 설정 - Spring Security 설정, CORS 설정, 세션 유지 설정 등 전역적인 설정을 관리 - CORS 설정에서 프론트와 백의 localhost 주소가 3000과 8080으로 다른 것을 연결 |
| exception/ | 예외를 처리하는 핸들러 클래스 (전역 에러 핸들링) - 일관된 에러 응답을 제공 가능 |
| Application.java | Spring Boot 애플리케이션의 실행 시작점 |
- controller/AuthController.java
package com.example.auth.controller;
import com.example.auth.dto.LoginRequestDto;
import com.example.auth.dto.UserDto;
import com.example.auth.service.AuthService;
import jakarta.servlet.http.HttpSession;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/login")
public ResponseEntity<UserDto> login(@RequestBody LoginRequestDto requestDto, HttpSession session) {
LoginResponseDto user = authService.login(requestDto);
session.setAttribute("user", user); // 세션에 사용자 정보 저장
return ResponseEntity.ok(user);
}
@PostMapping("/logout")
public ResponseEntity<String> logout(HttpSession session) {
session.invalidate(); // 세션 삭제
return ResponseEntity.ok("로그아웃 성공");
}
}
@RequestBody LoginRequestDto requestDto 에서 프론트엔드 받은 로그인 정보를 Dto 인 LoginRequestDto로 변환해서 받음.
HttpSession session 은 로그인 성공 시 세션을 생성하고 유지하는 역할. authServcie에서 requestdto와 함께 login 요청을 보내 LoginResponseDto를 응답 받음.
인증 성공하면 session.setAttribute("user", user);를 호출하여 세션에 사용자 정보를 저장. 이후, 세션 쿠키(JSESSIONID)가 자동으로 생성되어 브라우저에 저장됨.
- dto/LoginRequestDto.java , dto/LoginResponseDto.java
@Getter
@Setter
public class LoginRequestDto {
private String username;
private String password;
}
프론트엔드에서 username과 password를 보내면 이를 받아주는 DTO.
@Getter
@Setter
@AllArgsConstructor
public class LoginResponseDto {
private Long id;
private String username;
private String name;
}
사용자 정보 응답 시 비밀번호를 제외하고 id, username, name만 반환하는 DTO.
- entity/User.java
@Entity
@Table(name = "users")
@Getter
@Setter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
private String name;
}
데이터베이스의 users 테이블과 매핑되는 entity. properties 설정에서
spring.jpa.hibernate.ddl-auto=update
를 추가하면 필요할 때 entity에 맞게 자동으로 테이블을 생성해줌.
- respository/UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
사용자를 username으로 조회할 수 있도록 findByUsername() 메서드 추가
/service에서 로그인 로직을 수행할 때, findByUsername() 메서드로 사용자의 아이디로 비밀번호를 데이터베이스에서 찾아서 비교 후 로그인 승인
- service/ AuthServiceImpl.java, service/ AuthService.java
@Service
public class AuthServiceImpl implements AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public AuthServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public LoginResponseDto login(LoginRequestDto requestDto) {
User user = userRepository.findByUsername(requestDto.getUsername())
.orElseThrow(() -> new RuntimeException("아이디 또는 비밀번호가 잘못되었습니다."));
if (!passwordEncoder.matches(requestDto.getPassword(), user.getPassword())) {
throw new RuntimeException("아이디 또는 비밀번호가 잘못되었습니다.");
}
return new LoginResponseDto(user.getId(), user.getUsername(), user.getName());
}
}
비밀번호를 검증한 후, LoginResponseDto로 변환하여 반환. private final PasswordEncoder passwordEncoder 를 통해 해싱된 비밀번호를 matches 메소드를 이용하여 비교한 후 로그인.
public interface AuthService {
UserLoginResponseDto login(UserLoginRequestDto dto, HttpSession session);
}
AuthServiceImpl는 AuthService을 상속 받음.