
이번 포스팅에서는
Spring Boot에서 JPA Entity와 Repository를 생성하고,
MySQL에 테이블을 만들고 더미 데이터를 넣으며,
React에서 API를 호출해 실제 데이터를 화면에 뿌리는 흐름을 한 번 실제로 경험해보겠다.
먼저, ERD에 맞게 각 테이블과 매핑되는 Entity 클래스를 만든다.
예를 들어 users 테이블을 위한 User Entity는 이렇게 작성할 수 있다.
package com.study.shop.template.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
@Entity
@Table(name = "users")
public class User implements UserDetails { // UserDetails 인터페이스 구현
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 30)
private String username;
@Column(nullable = false, length = 255)
private String password;
@Column(nullable = false, unique = true, length = 100)
private String email;
@Column(length = 255)
private String address;
@Column(length = 255)
private String role;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
public User() {
}
public User(Long id, String username, String password, String email, String address, String role, LocalDateTime createdAt) {
this.id = id;
this.username = username;
this.password = password;
this.email = email;
this.address = address;
this.role = role;
this.createdAt = createdAt;
}
// UserDetails에서 요구하는 메서드 구현
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// role 컬럼에 저장된 문자열로부터 권한 생성
return List.of(new SimpleGrantedAuthority(role));
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
// 계정 만료 여부 여부 (필요에 따라 로직 추가 가능. 여기선 모두 true)
@Override
public boolean isAccountNonExpired() {
return true;
}
// 계정 잠금 여부 (필요에 따라 로직 추가 가능)
@Override
public boolean isAccountNonLocked() {
return true;
}
// 자격증명 만료 여부 (필요에 따라 로직 추가 가능)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 계정 활성화 여부 (필요에 따라 로직 추가 가능)
@Override
public boolean isEnabled() {
return true;
}
// 기존 엔티티 getter/setter
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", email='" + email + '\'' +
", address='" + address + '\'' +
", role='" + role + '\'' +
", createdAt=" + createdAt +
'}';
}
}
그리고 JPA Repository 인터페이스를 만들어 기본 CRUD 기능을 바로 사용할 수 있도록 한다.
package com.study.shop.template.repository;
import com.study.shop.template.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
boolean existsByUsername(String username);
boolean existsByEmail(String email);
Optional<User> findByUsername(String username);
}
마찬가지로 Product, Order, OrderItem 등도 Entity를 정의한다.
DB 테이블은 gui 툴을 사용하였으며 나는 데이터 그립을 통해 shopDB라는 테이블을 생성하였다.
그리고 로그인 및 상품 리스트 테스트를 위해 더미 데이터들을 입력하였다.
이번에는 실제로 React 컴포넌트에서 백엔드 API를 호출해 로그인 기능을 구현하는 법을 살펴보자.
앞서 우리는 백엔드에 로그인용 API /api/users/login을 만들었고, 프론트에서는 이 API를 호출해 인증 정보를 처리해야 한다.
먼저, API 호출 로직은 중복을 줄이기 위해 공통 함수로 빼는 게 좋다.
이 함수가 있으면 모든 API 요청을 이 함수를 통해 할 수 있다.
const BASE_URL = import.meta.env.VITE_API_URL
console.log('▶︎ VITE_API_URL =', import.meta.env.VITE_API_URL)
// JSON 응답 처리용 공통 함수
export async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
const res = await fetch(`${BASE_URL}${path}`, {
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // 쿠키 전송 등 세션 유지용
...options,
})
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`)
if (res.status === 204) {
// 204 No Content일 때 void 반환
return undefined as unknown as T
}
return res.json()
}
// 텍스트 응답 처리용 (예: 단순 텍스트 메시지 받을 때)
export async function requestText(path: string, options: RequestInit = {}): Promise<string> {
const res = await fetch(`${BASE_URL}${path}`, {
credentials: 'include',
...options,
})
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`)
return res.text()
}
BASE_URL은 .env에 미리 설정한 API 베이스 URL이다.credentials: 'include'는 쿠키를 자동으로 포함시켜 세션 관리를 돕는다.위 공통 함수를 활용해 실제 API 호출 함수들은 이렇게 관리한다.
// userApi.ts
import { request } from './client'
import type { User } from '../models/User'
import type { LoginRequest, SignUpRequest } from '../models/SignUpRequest'
/** 로그인 API 호출 */
export function login(data: LoginRequest): Promise<User> {
return request<User>('/api/users/login', {
method: 'POST',
body: JSON.stringify(data),
})
}
login 함수는 백엔드 로그인 API를 호출하고, 성공 시 User 객체를 반환한다.
이제 로그인 컴포넌트에서 위 API 함수 login을 호출해서 실제 로그인 처리를 한다.
import React, { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { login } from '../../api/userApi'
const Login: React.FC = () => {
const navigate = useNavigate()
const [id, setId] = useState('')
const [password, setPassword] = useState('')
const [showPw, setShowPw] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError(null)
try {
const user = await login({ username: id, password })
alert(`로그인 성공! 환영합니다, ${user.username} 님.`)
window.location.href = '/'
} catch (err: any) {
setError(err?.message || '로그인에 실패했습니다. 다시 시도하세요.')
setLoading(false)
}
}
return (
<div style={styles.container}>
<form onSubmit={handleLogin}>
<div style={styles.inputWrap}>
<input
type="text"
placeholder="ID"
value={id}
onChange={e => setId(e.target.value)}
style={styles.input}
autoComplete="username"
disabled={loading}
required
/>
</div>
<div style={styles.inputWrap}>
<input
type={showPw ? 'text' : 'password'}
placeholder="PASSWORD"
value={password}
onChange={e => setPassword(e.target.value)}
style={styles.input}
autoComplete="current-password"
disabled={loading}
required
/>
</div>
{error && <div style={styles.errorMsg}>{error}</div>}
<button style={styles.loginBtn} type="submit" disabled={loading}>
{loading ? '로그인 중...' : 'LOGIN'}
</button>
</form>
</div>
)
}
export default Login
request 유틸 함수 덕분에 HTTP 요청 관련 코드를 중앙에서 관리할 수 있어 편하고,
컴포넌트에서는 오직 보여줄 UI와 동작에 집중할 수 있다.
이후 프론트의 모든 API는 위 모듈과 화면 컴포넌트를 분리해 나갈 예정
지금까지 설계한 DB와 API를 실제 코드와 데이터로 구현하고,
React에서 이를 불러와 보여 주는 단계로 넘어가는 과정은
운영 가능한 서비스를 만드는 데 있어서 매우 중요한 전환점이다.
이 단계를 통해 내가 만든 구조가 실제 동작하며, 확장도 문제없음을 직접 손에 느낄 수 있다.
다음 글에서는 이 과정을 차근차근 실제 코드 위주로 설명하며,
하나씩 기능을 꾸준히 추가해 나가는 방식으로 진행해보자.
머라는지모르겟어염 ♡