Search

interface 만들어서 response 관리하기

태그
TypeScript
목차

백엔드에서 아래와 같이 Response 를 작성했는데…

아래와 같이 작성한 Response 를 프론트에서 어떻게 받고, 처리해야 할지 난감했습니다.
@GetMapping(value = "/{boardId}") public ResponseEntity<?> displayBoardDetail (@PathVariable("boardId") Long boardId) { try { List<BoardDetailResponse> boardDetailResponses = boardService.displayBoardDetail(boardId); return ResponseEntity.ok(boardDetailResponses); } catch (ApiException apiException) { return ResponseEntity.status(apiException.getErrorType().getStatus()).body(apiException.getErrorType().getMessage()); } } // BoardDetailResponse @Getter @Builder @AllArgsConstructor @NoArgsConstructor public class BoardDetailResponse { private Long taskBoxId; private Long taskBoxOrder; private String taskBoxTitle; private List<Task> taskDetailResponses; } // Task Entity @Entity @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class Task { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private String tag; private LocalDate dueDate; private String workHour; private String createdBy; private Long taskOrder; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "task_box_id") private TaskBox taskBox; ... }
Java
복사
그러던 찰나 interface 를 작성하여 관리하는 법을 알게되었고, 아래와 같이 interface 를 작성하였습니다.

Task.ts

export interface Task { id: number; title: string; tag: string; dueDate: string; workHour: string; createdBy: string; taskOrder: number; }
TypeScript
복사

BoardDetailResponse.ts

위에서 작성한 Task 를 끌고 오는 것이죠.
import {Task} from "./Task.ts"; export interface BoardDetailResponse { taskBoxId: number; taskBoxOrder: number; taskBoxTitle: string; taskDetailResponses: Task[]; }
TypeScript
복사

api 연결하기

이제 실제로 api 를 연결해봅시다. 저는 api 는 이런식으로 따로 분류를 해놓는데요,
해당 api 는 BoardApi.ts 에 작성했으므로, 해당 파일을 까보겠습니다.

BoardApi.ts

export const boardDetail = async (boardId: UnwrapRef<() => string>) => { const response = await axiosInstance.get(`/boards/${boardId}`); return response; }
TypeScript
복사
다음은 이 api 를 불러온 컴포넌트로 가보겠습니다!
<script setup lang="ts"> const boardData = ref<BoardDetailResponse[]>([]); const fetchBoardDetail = async () => { try { const response = await boardDetail(boardId.value); boardData.value = response.data; } catch (error) { console.error(error); } }; </script>
HTML
복사
사용은 어떻게 하냐구요?
<template> <div class="board-detail-container"> <div class="board-detail-icon-container"> <div class="board-detail-title"> {{ boardTitle }} </div> <div> <font-awesome-icon style="margin-right: 15px; font-size: 23px" @click="openCreateModal" icon="fa-solid fa-folder-plus" /> <font-awesome-icon style="margin-right: 10px; font-size: 19px" @click="openMemberSearchModal(boardId)" icon="fa-solid fa-user-plus" /> <font-awesome-icon icon="fa-solid fa-user-minus" style="margin-right: 13px; font-size: 19px" @click="openMemberSettingModal"/> <font-awesome-icon v-if="isMemberHost" icon="fa-solid fa-address-card" style="margin-right: 13px; font-size: 22px" @click="openMemberReportModal"/> <font-awesome-icon v-if="isMemberHost" icon="fa-solid fa-users-rectangle" style="font-size: 21px" @click="openTeamReportModal"/> </div> </div> <div class="kanban-container"> <!-- 늘 하던대로 반복문을 통해 꺼내오면 됩니다 --> <div v-for="taskBox in boardData" :key="taskBox.taskBoxId" class="kanban-column" draggable="true" @dragstart="onDragStartTaskBox($event, taskBox.taskBoxId, taskBox.taskBoxOrder)" @dragover="onDragOver" @drop="onDropTaskBox($event, taskBox.taskBoxId)"> <div class="kanban-column-header"> <p> {{ taskBox.taskBoxTitle }} </p> <div class="board-button-container"> <font-awesome-icon icon="fa-regular fa-pen-to-square" @click="openUpdateModal(taskBox.taskBoxId)" style="margin-right: 15px;" class="board-icon"/> <font-awesome-icon icon="fa-solid fa-file-circle-plus" @click="openTaskCreateModal(taskBox.taskBoxId, boardId)" style="margin-right: 15px;" class="board-icon"/> <font-awesome-icon icon="fa-solid fa-trash-can" @click="handleDeleteTaskBox(taskBox.taskBoxId)" class="board-icon"/> </div> </div> <!-- 늘 하던대로 반복문을 통해 꺼내오면 됩니다 --> <div v-for="(task, taskIndex) in taskBox.taskDetailResponses" :key="task.id" class="kanban-row" draggable="true" @dragstart="onDragStartTask($event, task.id, task.taskOrder, taskBox.taskBoxId)" @dragover="onDragOver" @drop="onDropTaskElement($event, taskBox.taskBoxId, taskIndex)"> <div class="kanban-row-header"> <p> {{ task.title }} </p> <div class="kanban-button-container"> <font-awesome-icon icon="fa-regular fa-pen-to-square" @click="openTaskUpdateModal(task.id)" style="margin-right: 12px;" class="kanban-row-icon"/> <font-awesome-icon icon="fa-solid fa-trash-can" @click="handleDeleteTask(task.id)" class="kanban-row-icon"/> </div> </div> <div class="kanban-row-content" @drop="onDropTaskElement($event, taskBox.taskBoxId, taskIndex)"> <div class="kanban-row-content-icon"> <font-awesome-icon style="margin-right: 5px" icon="fa-regular fa-calendar-check" /> <span>{{ task.dueDate }}</span> </div> <div class="kanban-row-content-icon"> <font-awesome-icon style="margin-right: 5px" icon="fa-regular fa-clock" /> <span>{{ task.workHour }}</span> </div> <div class="kanban-row-footer" style="margin-top: 10px"> <div class="kanban-badge-created"> {{ task.createdBy }} </div> <div class="kanban-badge" :class="getTagClass(task.tag)"> {{ task.tag }} </div> </div> </div> </div> </div> </div> </div> ...
HTML
복사

복잡해보이는데, 어떻게 매핑이 되는걸까?

제가 생성한 칸반보드에 들어가면, 다음과 같이 칸반보드 상세 데이터를 받을 수 있습니다. 지금이야 데이터가 적지만, 사람들이 일 하다 보면 많아지겠죠?  
interface 로 처리하면 아래와같이 복잡해보이는 응답받은 데이터를 쉽게 처리할 수 있다는 건 알겠습니다. 하지만 어떤 원리로 interface 와 response 가 매핑이 되는 걸까요?
[ { "boardTitle": "프로젝트 1 수정", "boardId": 29, "members": [ { "memberId": 36, "username": "시은", "role": "Host", "pending": false }, { "memberId": 42, "username": "듀밍", "role": "Member", "pending": true }, { "memberId": 43, "username": "태훈", "role": "Member", "pending": true }, { "memberId": 45, "username": "혜빈", "role": "Member", "pending": true } ] }, { "boardTitle": "프로젝트 2", "boardId": 30, "members": [ { "memberId": 37, "username": "시은", "role": "Host", "pending": false } ] }, { "boardTitle": "프로젝트 3", "boardId": 31, "members": [ { "memberId": 38, "username": "시은", "role": "Host", "pending": false } ] }, { "boardTitle": "프로젝트 4", "boardId": 32, "members": [ { "memberId": 39, "username": "시은", "role": "Host", "pending": false } ] }, { "boardTitle": "프로젝트 5", "boardId": 33, "members": [ { "memberId": 40, "username": "시은", "role": "Host", "pending": false } ] }, { "boardTitle": "프로젝트 6", "boardId": 34, "members": [ { "memberId": 41, "username": "시은", "role": "Host", "pending": false } ] } ]
JSON
복사

API 응답 데이터를 TypeScript 인터페이스로 매핑 하는 원리 이해하기

TypeScript의 타입 시스템

TypeScript 는 JavaScript 에 정적 타입 시스템을 추가합니다. 이는 변수, 매개변수, 반환값 등이 특정 타입을 가진다고 명시적으로 선언할 수 있게 해주는데요 예를 들어, 어떤 함수가 특정 interface(예시에서는 Task, BoardDetailResponse 입니다.) 를 따르는 객체를 반환한다고 선언하면 TypeScript 컴파일러는 이 함수가 해당 인터페이스에 정의된 모든 필드와 타입을 준수하는 객체를 반환하도록 강제합니다.

TypeScript의 구조적 타이핑

TypeScript 는 구조적 타이핑(Structural Typing) 을 사용합니다. 이는 객체의 실제 구조가 인터페이스에 정의된 구조와 일치하는 한, 해당 객체가 인터페이스를 준수한다고 간주한다는 의미입니다. 예를 들어, 인터페이스에 정의된 필드가 객체에 모두 존재하면 TypeScript 는 이 객체가 해당 인터페이스를 구현한다고 간주합니다.

타입을 동적으로 변경할 수 있는 JavaScript

JavaScript 는 동적 타입 언어로, 변수의 타입은 런타임에 결정되고 변경될 수 있습니다. TypeScript 는 이러한 JavaScript 의 특성을 그대로 유지하면서, 컴파일 시점에 타입 검사를 수행해 프로그램의 안정성을 높입니다.

TypeScript 는 API 응답을 어떻게 매핑하는가?

API 로부터 받은 데이터는 일반적으로 JSON 형식이며, JavaScript 객체로 자동 변환됩니다. TypeScript 는 이 객체가 특정 인터페이스를 준수하는지 컴파일 시점에 검사합니다. API 응답 객체가 인터페이스에 정의된 필드와 타입을 모두 갖추고 있다면, TypeScript 는 이 객체를 해당 인터페이스의 인스턴스로 간주합니다.
이렇게 TypeScript 와 api 응답에 대한 지식이 한층 더 상승했습니다.