백엔드 API를 활용하여 사용자가 이용할 '루틴 관리' 관련 프론트엔드 컴포넌트를 작업 한 내용입니다.

컴포넌트 기반 설계

지난 며칠간 구축한 백엔드 API를 기반으로 사용자에게 보여지는  화면을 만드는 프론트엔드 개발의 진행을 시작했습니다.
컴포넌트 기반 개발 방식으로 접근했습니다.

  • 컴포넌트 분리 : '루틴 관리 모달'이라는 큰 기능 단위를 다음과 같은 작은 컴포넌트로 분리하여 개발했습니다. 

RoutineList.tsx : 루틴 목록을 표시합니다. 

RoutineItem.tsx : 개별 루틴 하나하나를 표시합니다.

AddRoutineForm.tsx : 새로운 루틴을 등록합니다.

PhotoUpload.tsx: 사진 인증을 담당합니다.

 

위와 같은 역할 분리로 각 컴포넌트는 자신의 역할에만 관여하며 코드의 복잡성을 낮출 수 있습니다.

React Hooks를 이용한 상태 관리

useStateuseEffect를 통해 컴포넌트를 관리합니다.

  • useState로 상태 정의
    각 컴포넌트가 '기억'해야 할 모든 값들(예: 폼 입력 내용, 로딩 여부, 에러 메시지)을 useState로 명확하게 정의했습니다.
    예를 들어, setLoading(true)로 버튼이 '생성 중...'으로 바뀌고 비활성화되는 로직을 구현했습니다.
  • useEffect로 데이터 흐름 제어
    RoutineModal 컴포넌트에서 모달이 열릴 때useEffect를 사용하여 자동으로 fetchRoutines API를 호출하도록 설계했습니다. 이는 "특정 조건이 충족되었을 때,  실행하라"는 명령을 내리는 것과 같았다.

API 연동 및 비동기 처리

백엔드와 프론트엔드를 연결하는 연결점의 역할을 하는 API 연동 작업을 진행하며, 비동기 처리에 대해 집중햇습니다.

  • axios를 이용한 API 호출 : 미리 설정해둔 axiosInstance를 사용해 백엔드 API(POST /api/routines 등)와 연결했습니다.
  • async/awaittry...catch...finally
    서버 응답을 기다리는 모든 API 호출은 async/await를 사용하여 비동기적으로 처리했습니다.
    네트워크 오류 등과 같은 문제를 처리하기 위해 모든 비동기 로직을 try...catch 구문으로 작성했습니다.
    작업의 성공/실패 여부와 관계없이 항상 실행되어야 하는 로직(예: setLoading(false))은 finally 블록에 넣어 코드의 안정성을 높였습니다.

사용자 경험(UX)을 고려한 디테일 구현

사용자가 더 편리하고 직관적으로 앱을 사용할 수 있도록 디테일을 추가했습니다.

  • 유효성 검증 : 루틴 이름이 비어있거나, 이미지 파일의 크기가 너무 클 경우, 폼을 제출하기 전에 사용자에게 미리 알려주어 불필요한 API 요청을 막습니다.
  • 로딩 및 에러 상태 표시 : API 통신 중에는 스피너 아이콘을 보여주고 버튼을 비활성화하여 사용자가 중복 클릭하는 것을 방지했습니다. 에러 발생 시에는 한글 메시지로 나타내어 사용자가 상황을 인지할 수 있도록 구현했습니다.
  • 컴포넌트 간의 소통 (Props & Callbacks)
    부모 컴포넌트(RoutineModal)가 자식 컴포넌트(AddRoutineForm)에게 데이터를 전달할 때는 props를 사용했습니다.
    자식 컴포넌트가 자신의 작업 완료를 부모에게 알릴 때는 onSuccess, onCancel 같은 콜백 함수를 호출하는 패턴을 사용하여 컴포넌트 간의 의존성을 낮추고 재사용성을 높였습니다.

정리

작성한 백엔드 API를 이용하여 프론트엔드를 구현했습니다. useState를 통해 데이터와 UI가 어떻게 실시간으로 반응하는지, useEffect로 API 호출 시점을 어떻게 제어하는지 이해할 수 있었습니다. 내일은 기능의 작동을 확인하며 추가적인 부분을 구현할 예정입니다.

728x90

의존성 역전 원칙(DIP) 고수

클린 아키텍처 구조 중 중요한 원칙 중 하나는의존성 역전 원칙(DIP) 입니다. "상위 모듈은 하위 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다."는 내용을 담고 있습니다.

Before: 직접적인 의존성

// 잘못된 방식: 비즈니스 로직이 Prisma라는 '구현체'에 직접 의존한다.
class RoutineService {
  private prisma = new PrismaClient();

  async getRoutine(id: number) {
    // 만약 Prisma를 다른 ORM으로 바꾸면 이 코드는 전부 수정되어야 한다.
    return this.prisma.routine.findUnique({ where: { id } });
  }
}

After: 추상화에 대한 의존

IRoutinesRepository라는 인터페이스(추상화)를 domains 계층에 정의했습니다. 이 인터페이스는 "루틴을 어떻게 가져올 것인가(How)"가 아닌, "루틴을 가져올 수 있다(What)"는 약속만을 담고 있습니다.

// domains/repositories/IRoutinesRepository.ts
export interface IRoutinesRepository {
  findById(routineId: number): Promise<Routine | null>;
  // ...
}

// application/usecases/GetRoutineByIdUseCase.ts
export class GetRoutineByIdUseCase {
  // 생성자를 통해 '설계도(인터페이스)'를 주입받는다.
  // 실제 Prisma 구현체가 들어오는지, 다른 것이 들어오는지 전혀 신경 쓰지 않는다.
  constructor(private readonly routinesRepository: IRoutinesRepository) {}

  async execute(request: GetRoutineByIdRequestDto): Promise<ReadRoutineResponseDto> {
    // 약속된 기능만을 호출한다.
    const routine = await this.routinesRepository.findById(request.routineId);
    // ...
  }
}

 

비즈니스 로직(UseCase)은 이제 Prisma의 존재를 알지 못합니다. 오직 IRoutinesRepository라는 '약속'에만 의존합니다. 따라서, 데이터베이스 기술이 바뀌어도 비즈니스 로직은 수정할 필요가 없어졌으며 Repository를 가짜 객체로 대체하여 손쉽게 단위 테스트를 작성할 수 있습니다. 이것이 DIP가 주는 유연성과 테스트 용이성입니다.

백엔드 기능 구현

오늘 하루  다음과 같은 성과를 이뤄냈습니다.

  • 확장 가능한 API 설계 : RESTful 표준을 준수하고, 일관된 응답 형식과 명확한 HTTP 상태 코드를 적용하여 총 10개의 API 엔드포인트를 구현했습니다.
  • 테스트 완료 : API 통합 테스트 결과, 테스트를 모두 통과 했습니다.

정리

IRoutinesRepository라는 추상적인 인터페이스에서 시작하여 Next.js의 NextResponse로 데이터가 반환되기까지의 과정을 거쳤습니다. 제가 맡은 "메인 대시보드 및 루틴 수행" 기능의 백엔드는 모두 구현이 완료되었으며. GET /api/routines?userId={id} POST /api/routines/{id}/complete API를 이용하여 프론트엔드단 개발을 진행할 예정입니다. 완성된 API를 호출하여 메인 페이지에 기능을 구현하고 사용자가 실제로 루틴을 완료하는 인터랙션을 구현하는 데 집중할 계획입니다.

728x90

클린 아키텍처 백엔드 구조 검증 및 논의

오늘 아침 회의 후, 어제까지 구현했던 클린 아키텍처 백엔드 구조에 대한 검증을 했습니다. 특히 domains/entities domains/repositories 하위의 추상화된 인터페이스(Repository 인터페이스)가 올바르게 설계된 것을 확인했습니다. 이제 이 추상화를 바탕으로 실제 Use Case와 HTTP API 엔드포인트를 구현했습니다.

Use Case (비즈니스 로직) 정의 및 구현

핵심 비즈니스 규칙을 담당하는 Usecase 계층을 명확하게 정의하고, 각 Usecase가 데이터베이스에서 데이터를 가져와 비즈니스 규칙을 적용하여 알맞은 형태로 변환하여 반환하는 역할을 수행하도록 구현했습니다.

  • 구현된 Use Case 목록
    GetRoutinesUseCase : 특정 사용자의 루틴 목록을 가져오는 로직
    GetRoutineByIdUseCase : 특정 루틴 하나의 상세 정보를 조회하는 로직
    CompleteRoutineUseCase: 사용자가 루틴을 완료 처리하는 비즈니스 로직
    GetRoutineCompletionsUseCase: 특정 루틴의 완료를 조회하는 로직.

REST API 엔드포인트 구현 (Next.js App Router)

정의된 Use Case들을 이용하기 위하여 REST API 엔드포인트를 Next.js의 App Router 구조에 맞춰 구현했습니다. 각 API는 명확한 HTTP 상태 코드를 반환하며 오류 발생 시에는  한글 에러 메시지를 제공하도록 설계했습니다.

  • 구현된 API 목록
    루틴 관리
    POST /api/routines : 루틴 생성하기
    GET /api/routines : 모든 루틴 목록 보기(필터링 기능 포함)
    GET /api/routines/[routineId] : 특정 루틴 상세 정보 보기
    PATCH /api/routines/[routineId] : 루틴 정보 수정하기
    DELETE /api/routines/[routineId] : 루틴 삭제하기

    루틴 완료
    POST /api/routines/[routineId]/complete : 루틴 완료 기록하기
    GET /api/routines/[routineId]/completions: 특정 루틴의 완료 기록 조회하기

기술적 문제 해결 

개발 과정에서 기술적인 문제들을 해결하며 코드의 안정성을 높였습니다.

  • TypeScript 타입 이슈 해결 : Prisma의 null 타입 처리와 Next.js 15의 새로운 타입 요구사항에 맞춰 TypeScript 관련 오류들을 해결하며 타입 일관성을 확보했습니다.

클린 아키텍처 완성 및 테스트 준비

오늘 작업의 핵심은 이론적으로만 이해했던 클린 아키텍처를 실제로 동작하는 API로 구현했다는 점입니다.

  • 명확한 계층 분리
    API 레이어 : HTTP 요청/응답 처리만을 담당합니다.
    Use Case 레이어 : 순수한 비즈니스 로직만을 담당합니다.
    Repository 레이어 : 데이터 접근(Prisma 구현)만을 담당합니다.

  • 의존성 역전 원칙 준수 : 상위 레이어가 하위 레이어의 구체적인 구현(Prisma)에 의존하지 않고, 오직 인터페이스에만 의존하도록 설계하여 코드의 유연성과 테스트 용이성을 인지하며 설계했습니다.

정리

오늘 클린 아키텍처 기반으로 '루틴' 기능에 대한 백엔드와 API의 구현을 작업했습니다. 앞으로 다른 기능들을 개발할 때도 동일한 패턴을 적용한다면, 이를 기반으로 작업할 수 있습니다. 내일은 구현된 API들의 정확성 확인을 진행할 예정입니다. 이 과정을 통하여 프론트엔드 연동을 위한 백엔드 API를 최종적으로 구현하는 것에 노력을 하겠습니다.

728x90

클린 아키텍처 구조 적용

프로젝트의 유지보수성과 확장성을 높이기 위해 도입한 클린 아키텍처의 개념을 프로젝트 구조에 적용했습니다.

  • 핵심 개념
    도메인(Domain) :  서비스의 순수한 비즈니스 로직. 외부 라이브러리(Prisma 등)에 의존하지 않는다.
    인프라(Infrastructure) : 데이터베이스, 외부 API 등 외부 세계와 소통하는 기술적인 세부 구현.
    관심사의 분리 (SoC) : 무엇을 할 것인가(도메인)와 어떻게 할 것인가(인프라)를 명확히 분리하여 한쪽의 변경이 다른 쪽에 영향을 미치지 않도록 설계한다.
    의존성 규칙 : 모든 의존성은 항상 바깥쪽(인프라)에서 안쪽(도메인)으로 향해야 한다는 원칙을 이해한다.

  • 리포지토리 패턴(Repository Pattern) 적용
    인터페이스 정의 (/domains) : 먼저 RoutinesRepository 인터페이스를 만들어 데이터베이스로 수행해야 할 작업(생성, 조회, 수정, 삭제)을 추상적으로 정의했습니다. 이는 도메인 계층이 특정 DB 기술에 종속되지 않도록 합니다.
    구현체 작성 (/infrastructures) : 위에서 정의한 인터페이스를 PrRoutinesRepository 클래스가 구현(implements) 하도록 작업할 예정 입니다. 이 클래스 안에서만 실제 Prisma 코드를 사용하여 데이터베이스와 통신하는 로직을 작성해야 합니다.

'루틴' 기능 풀스택 설계 및 백엔드 구현

제가 담당한 '메인 대시보드 및 루틴 수행' 기능의 핵심인 '루틴' CRUD(생성, 조회, 수정, 삭제) 로직을 클린 아키텍처 구조에 따라 완성해야 합니다.

  • DTO (Data Transfer Object) 정의
    클라이언트(프론트엔드)와 서버 간에 데이터를 주고받기 위한 규격인 CreateRoutineDto, UpdateRoutineDto를 먼저 정의했습니다.
  • 도메인 엔티티(Domain Entity) 정의
    시스템의 핵심 객체인 Routine 엔티티를 순수한 TypeScript 인터페이스로 정의했습니다.
  • 서비스(Service) 계층 구현
    Routines 클래스를 만들어 실제 비즈니스 로직을 처리하도록 했습니다.
  • 의존성 주입(Dependency Injection) 
    레포지토리 패턴을 적용하여 
    RoutinesRepository 인터페이스에만 의존하도록 설계했습니다. 서비스 계층은 데이터베이스가 Prisma인지 다른 것인지 알 필요가 없습니다.
  • 컨트롤러(Controller) 계층 구현
    Next.js API Routes를 사용하여 실제 API 엔드포인트(GET, PATCH, DELETE 등)를 구현해야 합니다.
    이 컨트롤러는 HTTP 요청을 받아 DTO로 변환하고, Routines를 호출한 뒤, 그 결과를 다시 HTTP 응답으로 변환해주는 역할을 수행하도록 해야 합니다.

네트워크 및 서버 환경 이해

로컬 개발 환경을 넘어서 실제 배포 환경을 고려한 네트워크 개념에 대하여 학습했습니다.

사설 IP와 공인 IP : 내부 네트워크에서 사용하는 주소와 외부 인터넷에서 사용하는 주소의 차이점을 이해했습니다.
포트 포워딩 : 외부에서 들어온 요청을 내부 서버로 연결해주는 공유기의 핵심 설정 방법을 이해했습니다. 이는 추후 개인 서버나 테스트 환경을 구축할 때 필수적인 지식입니다.


정리

추상적인 아키텍처 개념을 실제 동작하는 코드로 구현해내는 경험을 했습니다. 리포지토리 패턴과 의존성 주입을 통해 각 코드의 역할과 책임을 명확히 분리하여 클린 아키텍처가 유지보수와 테스트에 왜 유리한지 느낄 수 있었습니다.


이후 완성된 루틴 기능의 백엔드 API를 기반으로 프론트엔드 UI 개발을 해야합니다. UI 컴포넌트를 완성한 후, TanStack Query를 이용해 실제 API와 연동하여 메인 대시보드 기능을 최종적으로 완성할 예정입니다.

728x90

 

웹 통신(HTTP)의 가장 큰 특징은 '무상태(Stateless)'입니다. 서버는 요청을 보낸 사용자가 이전에 어떤 요청을 했는지 기억하지 못합니다.

이런 '무상태' 환경에서는 다음과 같은 문제가 발생할 수 있습니다.

  • 로그인: 페이지를 이동할 때마다 다시 로그인해야 합니다.
  • 장바구니: 상품을 담고 다른 페이지로 가면 장바구니에 담은 물품이 사라집니다.

이러한 문제를 해결하기 위해 세션(Session)을 사용합니다.

 

세션(Session)은 웹 사이트와 사용자(브라우저) 간의 지속적인 대화 상태를 유지하는 창구와 같은 역할을 합니다.

 

서버는 각 사용자를 위한 '전용 창구'를 만들어두고, 사이트를 이용하는 동안 특정 사용자를 기억할 수 있게 됩니다. 따라서 서버는 사용자의 로그인 상태, 장바구니 내용 등을 유지할 수 있습니다.

세션의 동작 방식 : 개인 사물함과 열쇠

세션은 보통 세션 ID쿠키를 이용해 동작합니다. 이 과정을 '개인 사물함과 열쇠'에 비유하면 쉽게 이해할 수 있습니다.

  1. 최초 접속 (손님의 첫 방문)
    사용자가 웹사이트에 처음 접속합니다.
  2. 세션 생성 및 세션 ID 발급 (개인 사물함과 열쇠 제공)
    서버는 이 사용자를 위한 고유한 저장 공간(세션)을 서버 메모리나 데이터베이스에 만듭니다. 이것이 바로 '개인 사물함'입니다.
    그리고 서버는 이 '개인 사물함'을 열 수 있는 유일한 '열쇠'인 세션 ID를 생성합니다.
  3. 세션 ID 전달 (열쇠를 사용자에게 전달)
    서버는 생성된 세션 ID를 HTTP 응답 헤더에 담아 사용자 브라우저에게 전달합니다.
  4. 쿠키에 저장 (열쇠를 주머니에 보관)
    사용자 브라우저는 서버로부터 받은 세션 ID를 쿠키에 저장합니다. 이제 사용자는 사이트를 이용하는 동안 이 '열쇠'를 자신의 주머니에 가지고 다니는 셈입니다.
  5. 재접속 및 인증 (열쇠 확인)
    사용자가 같은 웹사이트 내에서 다른 페이지로 이동하거나 다시 요청을 보낼 때, 브라우저는 요청 헤더의 쿠키에 저장된 세션 ID(열쇠)를 자동으로 꺼내어 서버에 보여줍니다.
  6. 상태 유지 (개인 사물함 정보 확인)
    서버는 사용자에게 받은 '열쇠'를 보고, 서버에 있는 수많은 개인 사물함 중에서 그 열쇠에 맞는 사물함을 찾아냅니다.
    사물함과 열쇠가 일치한다면, 그 안에 저장된 정보(로그인 상태, 장바구니 내용 등)를 바탕으로 응답합니다.
  7. 세션 종료 (개인 사물함 비우기)
    사용자가 로그아웃하거나, 브라우저를 닫거나, 서버에서 설정한 일정 시간 동안 아무런 활동이 없으면(세션 만료), 서버는 해당 세션 정보('개인 사물함')를 삭제합니다.

세션과 쿠키의 차이점

구분 쿠키 (Cookie) 세션 (Session)
저장 위치 사용자 브라우저 (클라이언트 측) 웹 서버
저장 내용 간단한 텍스트 데이터 (Key-Value)
세션 ID 같은 열쇠를 저장
객체를 포함한 모든 유형의 데이터<br>로그인 정보, 장바구니 등
중요한 내용물
보안 상대적으로 낮음 (클라이언트에서 수정 가능) 상대적으로 높음 (서버에 저장되어 안전)
생명 주기 설정한 만료 시간까지 유지 (브라우저를 닫아도 유지 가능) 브라우저 종료 시 또는 서버 만료 시 삭제됨
속도/부하 서버에 부담을 주지 않음 사용자가 많아지면 서버에 부하를 줄 수 있음

정리

쿠키는 사용자의 컴퓨터에 남겨진 '열쇠'와 같고, 세션은 그 열쇠를 보고 서버가 관리하는 '개인 사물함'과 같습니다.
중요한 정보는 안전한 사물함(세션)에 보관하고, 그 사물함을 열 수 있는 열쇠(세션 ID)만 사용자(쿠키)에게 맡기는 방식입니다.

이러한 세션 메커니즘을 이용하여 '상태 없는' 웹 환경에서도 편리하게 로그인 상태를 유지합니다.

728x90

'CS' 카테고리의 다른 글

Prisma ORM에 대하여  (0) 2025.08.02
클린 아키텍쳐(Clean Architecture)란?  (0) 2025.07.12
객체 지향이란?  (0) 2025.06.29
데이터 정규화(Normalization)란?  (0) 2025.06.26
동기화와 비동기화  (0) 2025.06.20