[React + Spring Boot] 쇼핑몰 클론 코딩 : Part 3. Entity/Repository 생성 및 React API 연동

이지연·2025년 8월 23일
post-thumbnail

개요

이번 포스팅에서는
Spring Boot에서 JPA Entity와 Repository를 생성하고,
MySQL에 테이블을 만들고 더미 데이터를 넣으며,
React에서 API를 호출해 실제 데이터를 화면에 뿌리는 흐름을 한 번 실제로 경험해보겠다.


엔티티 및 레퍼지토리 생성 및 프론트 API 연동

Spring Boot Entity & Repository 생성

먼저, 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 +
                '}';
    }
}
  • Spring Security의 UserDetails 인터페이스를 구현해, 사용자 인증/권한 관리에 활용할 수 있다.
  • 주요 필드로는 id, username, password, email, address, role, createdAt이 포함되어 있다.
  • role 필드를 권한 정보로 사용하며, getAuthorities() 메서드에서 이를 권한 객체로 변환해 반환한다.
  • 계정 만료, 잠금, 자격증명 만료, 활성화 여부 관련 메서드는 모두 활성(true) 상태로 기본 구현되어 있다.
  • 각 필드에 대한 표준 getter/setter와 편의를 위한 생성자, toString() 메서드도 포함되어 있다.

그리고 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);
}
  • JpaRepository<User, Long>를 상속받아 기본 CRUD 및 페이징, 정렬 기능을 사용할 수 있다.
  • 추가로 사용자명(username)과 이메일(email)의 중복 여부를 확인하는 메서드(existsByUsername, existsByEmail)가 선언되어 있다.
  • 사용자명을 기준으로 Optional를 반환하는 findByUsername 메서드를 통해 해당 사용자를 조회할 수 있다.

마찬가지로 Product, Order, OrderItem 등도 Entity를 정의한다.


테이블 생성 및 더미 데이터 입력

DB 테이블은 gui 툴을 사용하였으며 나는 데이터 그립을 통해 shopDB라는 테이블을 생성하였다.
그리고 로그인 및 상품 리스트 테스트를 위해 더미 데이터들을 입력하였다.


React에서 API 호출해 데이터 받아오기 - 로그인 구현

이번에는 실제로 React 컴포넌트에서 백엔드 API를 호출해 로그인 기능을 구현하는 법을 살펴보자.
앞서 우리는 백엔드에 로그인용 API /api/users/login을 만들었고, 프론트에서는 이 API를 호출해 인증 정보를 처리해야 한다.

1. 공통 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'는 쿠키를 자동으로 포함시켜 세션 관리를 돕는다.
  • JSON 응답이 없을 경우 204 상태 코드를 처리해준다.

2. API 호출 함수 분리하기

위 공통 함수를 활용해 실제 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 객체를 반환한다.


3. React 로그인 컴포넌트 구현

이제 로그인 컴포넌트에서 위 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에서 이를 불러와 보여 주는 단계로 넘어가는 과정은
운영 가능한 서비스를 만드는 데 있어서 매우 중요한 전환점이다.

이 단계를 통해 내가 만든 구조가 실제 동작하며, 확장도 문제없음을 직접 손에 느낄 수 있다.

다음 글에서는 이 과정을 차근차근 실제 코드 위주로 설명하며,
하나씩 기능을 꾸준히 추가해 나가는 방식으로 진행해보자.

profile
Eazy하게

2개의 댓글

comment-user-thumbnail
2025년 8월 25일

머라는지모르겟어염 ♡

1개의 답글