재우니의 블로그

 

Build a Full Stack CRUD App with React, .NET, SQLite, and Ant Design | With Search & Validation!

 

 

설치 가이드 설명

# Node.js 버전 확인
C:\Users\lucks>node -v
v20.13.1

# Node Version Manager(NVM)으로 Node.js 버전을 20.x로 변경 (이 경우 v20.4.0)
C:\Users\lucks>nvm use 20
Now using node v20.4.0 (64-bit)

# 새로운 React 프로젝트를 client 폴더에 생성
D:\Source\React>npx create-react-app client

# 생성된 client 폴더로 이동
D:\Source\React>cd client

# React 프로젝트에 antd(UI 라이브러리)와 axios(HTTP 클라이언트) 설치
D:\Source\React\client> npm i antd axios

# 새로운 .NET Core Web API 프로젝트를 UsersAPI라는 이름으로 생성
D:\Source\React>dotnet new webapi -n UsersAPI

# 생성된 UsersAPI 폴더로 이동
D:\Source\React>cd UsersAPI

# Entity Framework Core에서 SQLite를 사용하는 패키지 추가
D:\Source\React\UsersAPI>dotnet add package Microsoft.EntityFrameworkCore.Sqlite

# Entity Framework Core 툴 패키지 추가 (마이그레이션 및 DB 작업을 위해)
D:\Source\React\UsersAPI>dotnet add package Microsoft.EntityFrameworkCore.Tools

# .NET Core Web API 서버 실행
D:\Source\React\UsersAPI>dotnet run

 

 

소스 설명

 

 

Program.cs (ASP.NET CORE API 소스)

 

 

주요 설명:

  1. CORS 정책 설정:
    • AllowAnyOrigin(), AllowAnyMethod(), AllowAnyHeader()로 설정된 기본 CORS 정책을 사용하여 모든 요청 출처, 메서드, 헤더를 허용합니다. 이는 API가 여러 출처에서 접근될 수 있게 하여, 특히 클라이언트와 서버가 다른 도메인에 있을 때 유용합니다.
  2. SQLite 데이터베이스 설정:
    • AddDbContext를 사용해 AppDbContext를 의존성 주입(DI) 컨테이너에 등록하고, SQLite를 사용해 users.db 파일을 데이터베이스로 설정합니다.
  3. 데이터베이스 자동 생성:
    • 앱이 시작될 때 데이터베이스가 존재하지 않으면 EnsureCreated() 메서드를 사용해 자동으로 생성합니다.
  4. CRUD API 엔드포인트:
    • GET, POST, PUT, DELETE 메서드를 통해 사용자를 생성, 조회, 수정, 삭제할 수 있는 간단한 RESTful API를 구성합니다.
      • GET /api/users: 모든 사용자 정보를 반환.
      • GET /api/users/{id}: 특정 ID의 사용자 정보를 반환.
      • POST /api/users: 새로운 사용자를 추가.
      • PUT /api/users/{id}: 특정 ID의 사용자 정보를 업데이트.
      • DELETE /api/users/{id}: 특정 ID의 사용자를 삭제.
  5. User 모델:
    • User 클래스는 데이터베이스의 테이블 구조와 동일하게 설계된 모델로, 각 사용자에 대한 ID, 이름, 이메일, 비밀번호를 저장합니다.
  6. AppDbContext:
    • AppDbContext는 DbContext를 상속하여, Users 테이블과 상호작용할 수 있는 환경을 제공합니다. DbSet<User>는 Entity Framework Core가 테이블과 상호작용할 수 있도록 설정된 테이블을 의미합니다.

 

이 코드는 기본적인 CRUD (생성, 읽기, 업데이트, 삭제) 기능을 제공하는 ASP.NET Core API로, 데이터베이스로 SQLite를 사용하며, CORS 정책을 통해 여러 도메인에서의 접근을 허용합니다.

 

using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args); // 애플리케이션 빌더 생성

// CORS(Cross-Origin Resource Sharing) 정책 설정
builder.Services.AddCors(options =>
    options.AddDefaultPolicy(policy =>
        policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader())); // 모든 Origin, Method, Header를 허용하는 기본 CORS 정책 추가

// SQLite 데이터베이스 설정 및 의존성 주입 등록
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite("Data Source=users.db")); // SQLite를 사용하여 "users.db" 데이터베이스 설정

// Swagger/OpenAPI 설정 (주석 처리됨)
// Swagger는 API 문서화를 위해 사용되는 도구
//builder.Services.AddEndpointsApiExplorer();
//builder.Services.AddSwaggerGen();

var app = builder.Build(); // 애플리케이션 빌드

// CORS 미들웨어 사용
app.UseCors();

// 데이터베이스 생성 (초기화)
// 앱이 실행될 때 데이터베이스가 존재하지 않으면 자동으로 생성
using (var scope = app.Services.CreateScope()) // 새 서비스 범위 생성
{
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); // DbContext 가져오기
    db.Database.EnsureCreated(); // 데이터베이스가 없으면 생성
}

// GET: /api/users
// 전체 사용자 목록을 반환하는 엔드포인트
app.MapGet("/api/users", async (AppDbContext db) => 
    await db.Users.ToListAsync()); // 데이터베이스에서 모든 사용자 데이터를 비동기적으로 가져와 반환

// GET: /api/users/{id}
// 특정 id에 해당하는 사용자 정보 반환
app.MapGet("/api/users/{id:int}", async (int id, AppDbContext db) =>
    await db.Users.FindAsync(id) is User user ? Results.Ok(user) : Results.NotFound()); // id로 사용자를 찾고, 있으면 OK 응답, 없으면 NotFound 응답

// POST: /api/users
// 새로운 사용자를 추가하는 엔드포인트
app.MapPost("/api/users", async (User newUser, AppDbContext db) =>
{
    db.Users.Add(newUser); // 새로운 사용자 추가
    await db.SaveChangesAsync(); // 데이터베이스에 변경 사항 저장
    return Results.Created($"/api/users/{newUser.Id}", newUser); // Created 응답 반환 (201 상태 코드)
});

// PUT: /api/users/{id}
// 기존 사용자의 정보를 수정하는 엔드포인트
app.MapPut("/api/users/{id:int}", async (int id, User updatedUser, AppDbContext db) =>
{
    var user = await db.Users.FindAsync(id); // 주어진 id로 사용자를 찾음
    if (user is null) return Results.NotFound(); // 사용자가 없으면 NotFound 반환

    // 사용자 정보 업데이트
    user.Name = updatedUser.Name;
    user.Email = updatedUser.Email;
    user.Password = updatedUser.Password;

    await db.SaveChangesAsync(); // 데이터베이스에 변경 사항 저장
    return Results.NoContent(); // NoContent 응답 반환 (204 상태 코드)
});

// DELETE: /api/users/{id}
// 특정 id의 사용자를 삭제하는 엔드포인트
app.MapDelete("/api/users/{id:int}", async (int id, AppDbContext db) =>
{
    var user = await db.Users.FindAsync(id); // 주어진 id로 사용자를 찾음
    if (user is null) return Results.NotFound(); // 사용자가 없으면 NotFound 반환

    db.Users.Remove(user); // 사용자 삭제
    await db.SaveChangesAsync(); // 데이터베이스에 변경 사항 저장
    return Results.NoContent(); // NoContent 응답 반환 (204 상태 코드)
});

app.Run(); // 애플리케이션 실행

// User 모델 정의
public class User
{
    public int Id { get; set; } // 사용자 고유 ID (Primary Key)
    public string Name { get; set; } = string.Empty; // 사용자 이름
    public string Email { get; set; } = string.Empty; // 사용자 이메일
    public string Password { get; set; } = string.Empty; // 사용자 비밀번호
}

// AppDbContext: Entity Framework Core의 DbContext 클래스
// 데이터베이스와의 상호작용을 담당하는 클래스
public class AppDbContext : DbContext
{
    public DbSet<User> Users => Set<User>(); // Users 테이블 정의
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } // DbContext 생성자
}

 

 

App.js (React.js 소스)

 

설명:

  1. useEffect를 사용하여 currentUser 상태가 변경될 때마다 폼을 초기화하거나, 값을 currentUser로 설정하도록 했습니다.
  2. Add User 버튼을 눌렀을 때 setCurrentUser(null)을 먼저 실행하여 폼이 초기화된 상태로 모달이 나타나도록 했습니다.

이 방식은 상태가 비동기적으로 처리되는 문제를 해결하며, Add User 버튼을 클릭할 때마다 폼이 항상 초기화된 상태로 나타나게 합니다.

 

import React, { useState, useEffect } from "react";
import { Layout, Menu, Table, Form, Input, Button, Modal, Space } from "antd";
import { PlusOutlined, EditOutlined, DeleteOutlined } from "@ant-design/icons";
import axios from "axios"; // HTTP 요청을 위한 axios 라이브러리

// Ant Design Layout 컴포넌트에서 사용되는 Header, Content, Footer
const { Header, Content, Footer } = Layout;
const { Search } = Input; // Ant Design의 Input에서 제공하는 Search 컴포넌트

const App = () => {
  // 사용자 데이터 관리용 상태
  const [users, setUsers] = useState([]);
  // 모달(사용자 추가/수정) 표시 여부 관리 상태
  const [isModalVisible, setIsModalVisible] = useState(false);
  // 현재 편집 중인 사용자 정보 관리 상태
  const [currentUser, setCurrentUser] = useState(null);
  // 검색어를 관리하는 상태
  const [searchTerm, setSearchTerm] = useState("");
  const [form] = Form.useForm(); // Ant Design의 Form 인스턴스 생성

  // 서버에서 사용자 데이터를 가져오는 비동기 함수
  const fetchUsers = async () => {
    const response = await axios.get("http://localhost:5209/api/users");
    setUsers(response.data); // 가져온 데이터를 상태에 저장
  };

  // 사용자 추가 또는 편집 처리 함수
  const handleAddOrEditUser = async (values) => {
    if (currentUser) {
      // 편집 모드인 경우 (currentUser가 null이 아님)
      await axios.put(
        `http://localhost:5209/api/users/${currentUser.id}`,
        values
      );
    } else {
      // 추가 모드인 경우 (currentUser가 null인 경우)
      await axios.post(`http://localhost:5209/api/users`, values);
    }
    // 모달 닫기 및 상태 초기화
    setIsModalVisible(false);
    setCurrentUser(null);
    form.resetFields(); // 폼 필드 초기화
    fetchUsers(); // 사용자 목록을 다시 불러옴
  };

  // 사용자 삭제 처리 함수
  const handleDeleteUser = async (id) => {
    await axios.delete(`http://localhost:5209/api/users/${id}`);
    fetchUsers(); // 사용자 목록을 다시 불러옴
  };

  // 컴포넌트가 마운트될 때 사용자 데이터를 불러오는 useEffect
  useEffect(() => {
    return () => {
      fetchUsers(); // 초기 사용자 목록 로딩
    };
  }, []);

  // currentUser 상태가 변경될 때마다 폼을 초기화하거나, 값을 설정
  useEffect(() => {
    if (!currentUser) {
      form.resetFields(); // currentUser가 없으면 폼을 초기화
    } else {
      form.setFieldsValue(currentUser); // 편집 모드일 경우, 폼에 값을 설정
    }
  }, [currentUser, form]);

  // 검색어에 맞게 사용자 목록을 필터링
  const filteredUsers = users.filter(
    (user) =>
      user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
      user.email.toLowerCase().includes(searchTerm.toLowerCase())
  );

  // 테이블 컬럼 정의
  const columns = [
    {
      title: "Name", // 컬럼 제목
      dataIndex: "name", // users 데이터의 필드명
      key: "name", // React에서 사용될 고유 키
    },
    {
      title: "Email",
      dataIndex: "email",
      key: "email",
    },
    {
      title: "Password",
      dataIndex: "password",
      key: "password",
    },
    {
      title: "Action", // 수정/삭제 버튼이 들어갈 컬럼
      key: "action",
      render: (_, record) => (
        <Space>
          <Button
            icon={<EditOutlined />} // 편집 아이콘
            onClick={() => {
              setCurrentUser(record); // 편집할 사용자를 currentUser로 설정
              setIsModalVisible(true); // 모달 표시
            }}
          />
          <Button
            icon={<DeleteOutlined />} // 삭제 아이콘
            danger
            onClick={() => handleDeleteUser(record.id)} // 삭제 함수 호출
          />
        </Space>
      ),
    },
  ];

  // 비밀번호 입력 필드 유효성 검사 규칙
  const passwordRules = [
    { required: true, message: "Password is required" }, // 필수 입력 항목
    { min: 8, message: "Password must be at least 8 characters" }, // 최소 8자 이상
    {
      pattern: /(?=.*[a-z])/,
      message: "Password must contain at least one lowercase letter", // 소문자 포함
    },
    {
      pattern: /(?=.*[A-Z])/,
      message: "Password must contain at least one uppercase letter", // 대문자 포함
    },
    {
      pattern: /(?=.*[0-9])/,
      message: "Password must contain at least one number", // 숫자 포함
    },
    {
      pattern: /(?=.*[!@#$%^&*])/,
      message: "Password must contain at least one special character", // 특수 문자 포함
    },
  ];

  return (
    <Layout style={{ minHeight: "100vh" }}>
      <Header>
        <Menu theme="dark" mode="horizontal">
          <Menu.Item key="home">Home</Menu.Item>
          <Menu.Item key="contact">Contacts</Menu.Item>
        </Menu>
      </Header>
      <Content style={{ padding: "50px" }}>
        {/* 사용자 추가 버튼 */}
        <Space style={{ marginBottom: "20px", float: "left" }}>
          <Button
            type="primary"
            icon={<PlusOutlined />}
            onClick={() => {
              setCurrentUser(null); // 사용자 추가 모드로 전환 (currentUser를 null로 설정)
              setIsModalVisible(true); // 모달 표시
            }}
          >
            Add User
          </Button>
        </Space>
        {/* 검색 필드 */}
        <Space style={{ marginBottom: "20px", float: "right" }}>
          <Search
            placeholder="Search by name or email" // 검색 필드의 placeholder
            onSearch={(value) => setSearchTerm(value)} // 검색어 설정
            style={{ width: 300 }}
          />
        </Space>
        <div style={{ clear: "both" }} />
        {/* 사용자 목록 테이블 */}
        <Table dataSource={filteredUsers} columns={columns} rowKey="id" />

        {/* 사용자 추가/편집 모달 */}
        <Modal
          title={currentUser ? "Edit User" : "Add User"} // 편집/추가 모드에 따라 모달 제목 변경
          visible={isModalVisible} // 모달 표시 여부
          onCancel={() => {
            setIsModalVisible(false); // 모달 닫기
            setCurrentUser(null); // currentUser 초기화
            form.resetFields(); // 폼 필드 초기화
          }}
          footer={null} // 모달 하단에 버튼을 표시하지 않음
        >
          <Form
            form={form} // 폼 인스턴스
            onFinish={handleAddOrEditUser} // 제출 시 호출될 함수
            layout="vertical" // 세로 레이아웃
          >
            {/* 이름 입력 필드 */}
            <Form.Item name="name" label="Name" rules={[{ required: true }]}>
              <Input />
            </Form.Item>

            {/* 이메일 입력 필드 */}
            <Form.Item
              name="email"
              label="Email"
              rules={[{ required: true, type: "email" }]}
            >
              <Input />
            </Form.Item>

            {/* 비밀번호 입력 필드 */}
            <Form.Item name="password" label="Password" rules={passwordRules}>
              <Input.Password />
            </Form.Item>

            {/* 제출 버튼 */}
            <Form.Item>
              <Button type="primary" htmlType="submit">
                Submit
              </Button>
            </Form.Item>
          </Form>
        </Modal>
      </Content>

      {/* 페이지 하단에 푸터 */}
      <Footer
        style={{
          textAlign: "center",
          backgroundColor: "#001529",
          color: "#fff",
          position: "sticky",
          bottom: 0,
        }}
      >
        © {new Date().getFullYear()} Your Company Name. All Rights Reserved.
      </Footer>
    </Layout>
  );
};

export default App;

 

 

 

https://www.youtube.com/watch?v=7YBXKEQCfLw