Skip to content

thepsyentist/react-example

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TODO App - React + Express + SQLite

React 클라이언트가 Express 서버와 REST API로 통신하는 구조를 학습하기 위한 예제 앱입니다. 클라이언트는 클린 아키텍처(Clean Architecture) 를 적용하여 레이어별 관심사를 분리합니다.

실행

npm run dev       # 서버 + 클라이언트 동시 실행
npm run server    # 서버만 (http://localhost:3002)
npm run client    # 클라이언트만 (http://localhost:5173)

전체 구조

react-example/
├── server/                        # Express + SQLite 서버
│   └── index.js
└── client/src/                    # React 클라이언트 (Clean Architecture)
    ├── domain/                    # 도메인 레이어
    │   ├── entities/              # Todo, Category, TodoStats
    │   └── repositories/         # 리포지토리 인터페이스
    ├── application/               # 애플리케이션 레이어
    │   └── usecases/             # 13개 유스케이스
    ├── infrastructure/            # 인프라 레이어
    │   ├── api/                  # REST API 구현
    │   ├── localstorage/         # LocalStorage 구현
    │   └── di/                   # 의존성 주입 컨테이너
    └── presentation/              # 프레젠테이션 레이어
        ├── hooks/                # 커스텀 훅
        └── components/           # React 컴포넌트

아키텍처 - Clean Architecture

핵심 원칙: 의존성이 항상 안쪽(도메인) 방향으로만 흐른다.

Presentation → Application → Domain ← Infrastructure

1. Domain Layer (도메인 레이어)

가장 안쪽 레이어로, 외부 라이브러리나 프레임워크에 의존하지 않습니다.

엔티티 (Entities)

domain/entities/Todo.js

export class Todo {
  constructor({ id, text, completed, category_id, category_name, ... }) { ... }

  toggleCompleted() { ... }    // 새 인스턴스 반환 (불변성)
  updateText(newText) { ... }
  assignCategory(categoryId, name, color) { ... }
  matchesSearch(query) { ... } // 도메인 로직: 검색 매칭
}
  • 앱의 핵심 데이터 구조와 비즈니스 규칙을 포함합니다.
  • 불변성(Immutability): 상태를 직접 변경하지 않고 새 인스턴스를 반환합니다.
  • matchesSearch()처럼 엔티티 자체에 도메인 로직을 포함할 수 있습니다.

domain/entities/Category.js — 두 번째 엔티티

export class Category {
  constructor({ id, name, color, created_at }) { ... }
}
  • 엔티티가 여러 개 존재하면 리포지토리도 여러 개 필요해지고, 유스케이스에서 조합하는 패턴이 자연스럽게 등장합니다.

domain/entities/TodoStats.js — 값 객체 (Value Object)

export class TodoStats {
  constructor({ total, completed, pending, completionRate, byCategory }) { ... }
}
  • 식별자(id)가 없는 값 객체로, 통계 데이터를 캡슐화합니다.

리포지토리 인터페이스 (Repository Interfaces)

domain/repositories/TodoRepository.js

export class TodoRepository {
  async getAll(filters) { throw new Error('Not implemented'); }
  async getStats()      { throw new Error('Not implemented'); }
  async create(text, categoryId) { ... }
  async update(id, data) { ... }
  async delete(id) { ... }
  async completeAll() { ... }
  async deleteCompleted() { ... }
}

domain/repositories/CategoryRepository.js

export class CategoryRepository {
  async getAll() { ... }
  async create(name, color) { ... }
  async delete(id) { ... }
}
  • 데이터 접근의 계약(Contract) 만 정의합니다.
  • 실제 구현은 Infrastructure 레이어에서 합니다.
  • 리포지토리가 2개가 됨으로써 유스케이스에서 서로 다른 리포지토리를 조합하는 패턴을 볼 수 있습니다.

2. Application Layer (애플리케이션 레이어)

유스케이스(Use Case) 를 정의합니다. 하나의 유스케이스 = 하나의 사용자 행위입니다.

기본 CRUD 유스케이스

유스케이스 역할 주입받는 리포지토리
AddTodo 새 할일 추가 + 입력 검증 TodoRepository
GetTodos 필터 조건으로 목록 조회 TodoRepository
ToggleTodo 완료/미완료 토글 TodoRepository
UpdateTodo 텍스트 수정 + 검증 TodoRepository
DeleteTodo 삭제 TodoRepository

복합 유스케이스 — 비즈니스 규칙이 드러나는 케이스

CompleteAllTodos — 일괄 완료 처리

export class CompleteAllTodos {
  constructor(todoRepository) { this.todoRepository = todoRepository; }

  async execute() {
    return this.todoRepository.completeAll();
  }
}
  • 단순 CRUD를 넘어 "모든 미완료 항목을 완료로 변경" 이라는 비즈니스 규칙을 캡슐화합니다.

DeleteCompletedTodos — 완료된 항목 일괄 삭제

  • "완료된 것만 삭제"라는 조건부 로직을 유스케이스로 분리합니다.

GetTodoStats — 통계 조회

  • 완료율, 카테고리별 분포 등 집계 로직을 유스케이스로 표현합니다.

검색 & 필터링 유스케이스

SearchTodos — 서버 사이드 검색

export class SearchTodos {
  constructor(todoRepository) { ... }

  async execute(query, filters = {}) {
    return this.todoRepository.getAll({ ...filters, search: query });
  }
}
  • 검색어와 기존 필터를 조합하는 것이 이 유스케이스의 역할입니다.

GetTodosByCategory — 카테고리별 조회

export class GetTodosByCategory {
  constructor(todoRepository) { ... }

  async execute(categoryId) {
    return this.todoRepository.getAll({ category_id: categoryId });
  }
}
  • 두 도메인(Todo + Category)을 연결하는 유스케이스입니다.

카테고리 유스케이스

유스케이스 역할 주입받는 리포지토리
GetCategories 카테고리 목록 조회 CategoryRepository
AddCategory 카테고리 생성 + 검증 CategoryRepository
DeleteCategory 카테고리 삭제 CategoryRepository
  • TodoRepository와 별도의 CategoryRepository를 주입받는 것에 주목하세요.
  • 각 유스케이스가 필요한 리포지토리만 의존합니다.

3. Infrastructure Layer (인프라 레이어)

도메인에서 정의한 인터페이스를 실제로 구현하는 레이어입니다.

API 구현체 — infrastructure/api/

TodoApiRepository.js — REST API로 서버와 통신

export class TodoApiRepository {
  async getAll(filters = {}) {
    const params = new URLSearchParams();
    if (filters.search) params.set('search', filters.search);
    // ... 필터 파라미터를 쿼리스트링으로 변환
    const res = await fetch(url);
    return data.map(item => new Todo(item));  // JSON → 도메인 엔티티 변환
  }
}

CategoryApiRepository.js — 카테고리 CRUD API

LocalStorage 구현체 — infrastructure/localstorage/

TodoLocalStorageRepository.js — 브라우저 저장소 사용

export class TodoLocalStorageRepository {
  _read()  { return JSON.parse(localStorage.getItem('todos') || '[]'); }
  _write(items) { localStorage.setItem('todos', JSON.stringify(items)); }

  async getAll(filters = {}) {
    let items = this._read();
    if (filters.search) { /* 클라이언트 사이드 필터링 */ }
    return items.map(i => new Todo(i));
  }
  // ... 동일한 인터페이스의 모든 메서드 구현
}

핵심 포인트: 리포지토리 패턴의 가치

두 구현체는 완전히 동일한 인터페이스를 가집니다. DI 컨테이너에서 한 줄만 변경하면 서버 없이도 앱이 동작합니다:

// container.js
// 서버 통신 모드 (기본)
const todoRepository = new TodoApiRepository();

// LocalStorage 모드 (서버 불필요) — 이 줄로 교체
// const todoRepository = new TodoLocalStorageRepository();

유스케이스, 컴포넌트, 훅은 아무것도 변경하지 않아도 됩니다. 이것이 리포지토리 패턴과 의존성 주입의 핵심 가치입니다.

DI 컨테이너 — infrastructure/di/container.js

const todoRepository = new TodoApiRepository();
const categoryRepository = new CategoryApiRepository();

export const useCases = {
  getTodos: new GetTodos(todoRepository),
  addTodo: new AddTodo(todoRepository),
  searchTodos: new SearchTodos(todoRepository),
  getCategories: new GetCategories(categoryRepository),
  // ...
};
  • 의존성 주입(DI): 유스케이스가 구체적 구현체를 직접 생성하지 않고, 외부에서 주입받습니다.
  • 테스트 시 Mock 리포지토리를 주입할 수 있습니다.
  • 리포지토리 구현체 교체가 이 파일 한 곳에서만 일어납니다.

4. Presentation Layer (프레젠테이션 레이어)

사용자에게 보이는 UI를 담당합니다.

커스텀 훅 (Custom Hooks)

useTodos — 메인 상태 관리

export function useTodos() {
  const [filter, setFilter] = useState('all');
  const [searchQuery, setSearchQuery] = useState('');
  const [categoryFilter, setCategoryFilter] = useState(null);

  // 필터가 바뀔 때마다 유스케이스를 다시 호출
  const loadTodos = useCallback(async () => {
    const filters = {};
    if (searchQuery) filters.search = searchQuery;
    if (categoryFilter) filters.category_id = categoryFilter;
    const data = await useCases.getTodos.execute(filters);
    setTodos(data);
  }, [searchQuery, categoryFilter, filter]);
}

useCategories — 카테고리 상태 관리

useTodoStats — 통계 데이터 관리

  • 각 훅이 관련된 유스케이스만 호출하고, 관심사를 분리합니다.

컴포넌트 (Components)

컴포넌트 역할
SearchBar 검색 입력
TodoForm 할일 추가 (카테고리 선택 포함)
FilterBar 상태 필터 (All/Active/Completed) + 카테고리 필터
TodoList 목록 렌더링
TodoItem 개별 항목 (체크, 수정, 삭제, 카테고리 배지)
BulkActions 일괄 완료 / 완료 항목 삭제
TodoStats 통계 대시보드

서버 - Express + SQLite

REST API

Method Endpoint 설명
GET /api/todos?search=&category_id=&completed= 목록 조회 (검색, 필터)
GET /api/todos/stats 통계 (완료율, 카테고리별 수)
POST /api/todos 새 할일 생성
PATCH /api/todos/:id 수정
DELETE /api/todos/:id 삭제
PATCH /api/todos/bulk/complete-all 전체 완료
DELETE /api/todos/bulk/delete-completed 완료 항목 일괄 삭제
GET /api/categories 카테고리 목록
POST /api/categories 카테고리 생성
DELETE /api/categories/:id 카테고리 삭제

SQLite

  • 파일 기반 DB (server/todos.db)로 별도 설치 불필요
  • WAL 모드: 읽기/쓰기 동시 처리 성능 향상
  • todoscategories외래 키(Foreign Key) 관계
  • 서버 시작 시 테이블/기본 카테고리 자동 생성

사용된 주요 패턴 요약

패턴 적용 위치 설명
Clean Architecture 클라이언트 전체 레이어별 관심사 분리, 안쪽 방향 의존성
Repository Pattern domain ↔ infrastructure 데이터 접근을 추상화하여 구현을 교체 가능하게 함
Dependency Injection di/container.js 의존성을 외부에서 주입하여 결합도를 낮춤
Use Case Pattern application/usecases 비즈니스 로직을 독립적 단위로 캡슐화
Custom Hook presentation/hooks React 상태 로직을 컴포넌트에서 분리
REST API 서버 ↔ 클라이언트 HTTP 메서드로 자원을 조작
Immutable Entity domain/entities 상태 변경 시 새 인스턴스를 반환하여 부수효과 방지
Value Object TodoStats 식별자 없이 값으로만 의미를 갖는 객체
Interface Segregation 리포지토리 2개 분리 각 유스케이스가 필요한 인터페이스에만 의존

기능 목록

  • 할일 추가 / 수정 (더블클릭) / 삭제 / 완료 토글
  • 카테고리 지정 (Work, Personal, Shopping + 커스텀)
  • 검색 (서버 사이드)
  • 필터링 (All / Active / Completed + 카테고리별)
  • 일괄 완료 / 완료 항목 일괄 삭제
  • 통계 대시보드 (완료율, 카테고리별 분포)
  • 리포지토리 교체 (API ↔ LocalStorage)

기술 스택

  • Client: React, Vite
  • Server: Express, better-sqlite3
  • Database: SQLite
  • 통신: REST API (fetch)

About

기본 리액트 예제 프로젝트입니다

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors