FrontEnd Develop/Project : Team Nova MJ Search

백엔드에서 요구한 데이터 구조에 맞춘 API 타입 설계

Frisbeen 2025. 7. 6. 21:38

"기존 백엔드에서 요구한 데이터 구조를 기반으로, 프론트엔드 협업을 위한 API 데이터 타입을 어떻게 정의하고 왜 그렇게 구성하는가?"

스스로에게 정말 많이 던졌던 질문이다.

한번 파헤쳐보자!


1. 실제 백엔드에서 주는 /notices API 응답 구조

  • HTTP Method: GET
  • Body 없음 (GET 요청이기 때문에 일반적으로 body에 데이터를 담지 않음)
  • 모든 데이터는 query parameter로 전달됨
{
  "content": [
    {
      "title": "[공지] ...",
      "date": "2025.04.08",
      "category": "career",
      "link": "https://..."
    }
  ],
  "pageable": {
    "pageNumber": 0,
    "pageSize": 15,
    "sort": { ... },
    ...
  },
  "last": false,
  "totalElements": 670,
  ...
}

2. 타입을 구조적으로 나누는 관점.

 가독성 향상

  • 중첩된 구조를 한눈에 보기 어렵고, 접근도 복잡해짐 (ex. data.content[0].title)
  • 타입을 나누면 필요한 구조만 집중해서 읽기 쉬움

 재사용성 높음

  • NoticeItem은 리스트 카드 컴포넌트, 상세 페이지, 북마크 등 다양한 UI에서 독립적으로 사용 가능
  • Pageable은 pagination 컴포넌트에서 그대로 재사용 가능

 유지보수 효율성

  • 특정 구조 변경 시 해당 타입만 수정하면 됨 (단일 책임 원칙)
  • 테스트용 mock 데이터 생성도 훨씬 편해짐

3. 실제 타입 분기 (noticeInfo.ts)

먼저, 각 공지사항 자체의 데이터 타입을 NoticeItem으로 정의한다. 

그 이후 페이지네이션 기능의 대한 타입을 정의한 후

최종적으로 API 통신을 할때 클라이언트가 가져가는 데이터의 타입을 둘을 합쳐서 정의한다.

export interface NoticeItem {
  title: string;
  date: string;
  category: 'general' | 'academic' | 'scholarship' | 'career' | 'activity' | 'rule';
  link: string;
}

export interface Pageable {
  pageNumber: number;
  pageSize: number;
  sort: {
    empty: boolean;
    sorted: boolean;
    unsorted: boolean;
  };
  offset: number;
  paged: boolean;
  unpaged: boolean;
}

export interface NoticeResponse {
  content: NoticeItem[];
  pageable: Pageable;
  last: boolean;
  totalElements: number;
  totalPages: number;
  first: boolean;
  numberOfElements: number;
  empty: boolean;
}

4. 실제 타입 기반 API 호출

1. 백엔드에서 category, year, page, size를 request query parameter로 설정했다.

실제 fetchNoticeInfo는 NotionSection.tsx라는 컴포넌트에서 실행되기에 실제 주입할 값을 매개변수로 처리한다.

 

2. 물론 여기서 Default Argument인 Page, Size는 기획과 디자인 대로 설정할 수 있다.

 

3. noticeApi.ts는 오직 API 통신을 위한 셋업만을 책임으로 지니기에  get함수에 두번째 매개변수 (config)에 params 속성에 값만 

요구된 query parameter만 채워준다.

 

4. 참고로, API 함수의 리턴타입은 비동기 함수이기에 Promise<T>를 반환해야하는데, 이 함수에 끝단에는 return 할 것이 없다.

.try에서만 return 하므로, 따라서 catch문으로 갈 경우의 return 값을 반환을 못하기에 에러를 던지는 것이다.

 

-> 이는 에러 상황에서 undefined가 반환되는 것을 방지하고, try-catch의 의도를 명확히 하기 위함이다.

import apiClient from '../apiClient';
import type { NoticeResponse } from '../../types/notice/noticeInfo';

export const fetchNotionInfo = async (
  category: string,
  year = new Date().getFullYear(),
  page = 0,
  size = 5,
  sort: 'asc' | 'desc' = 'desc',
): Promise<NoticeResponse> => {
  try {
    const response = await apiClient.get('/notices', {
      params: { category, year, page, size, sort },
    });
    return response.data;
  } catch (e) {
    console.log('공지사항 api fetching 간 오류 발생', e);
    throw e;
  }
};

5. 실제 렌더링

위에서 선언한 함수를 실제 함수형 컴포넌트에서 활용한다.

'use client';

import { useEffect, useState } from 'react';
import TabComponent from '../../molecules/mainpage/TabComponent';
import { fetchNotionInfo } from '../../../api/notice/noticeApi';
import type { NoticeItem } from '../../../types/notice/noticeInfo';

const NoticeSection = () => {
  const [selectedTab, setSelectedTab] = useState('general');
  const [selectedInfo, setSelectedInfo] = useState<NoticeItem[]>([]);
  const selectedCategory = selectedTab;
  const recentYear = new Date().getFullYear();

  useEffect(() => {
    const fetchingData = async () => {
      try {
        const fetchedNoticeData = await fetchNotionInfo(selectedCategory, recentYear);
        setSelectedInfo(fetchedNoticeData.content);
        console.log(fetchedNoticeData);
      } catch (e) {
        console.log('FC 내부 NoticeData Fetching 실패', e);
      }
    };
    fetchingData();
  }, [selectedTab]);
  return (
    <div>
      <h1 className='text-xl mt-1.5 font-bold text-mju-primary mb-4'>공지사항</h1>
      <TabComponent currentTab={selectedTab} setCurrentTab={setSelectedTab}></TabComponent>
      <div className='grid grid-cols-1 gap-2 mt-5 '>
        {selectedInfo.map((data, idx) => (
          <div className='bg-grey-05 '>
            <a
              href={data.link}
              target='_blank'
              rel='noopener noreferrer'
              key={idx}
              className='block p-4 border border-grey-10 rounded-xl shadow-sm hover:shadow-md transition-shadow'
            >
              <h3 className='text-lg font-semibold text-blue-800 truncate'>{data.title}</h3>
              <div className='mt-2 text-sm text-gray-600'>
                <p>
                  <span className='font-medium'>{new Date(data.date).toLocaleDateString()}</span>
                </p>
              </div>
            </a>
          </div>
        ))}
      </div>
    </div>
  );
};

export default NoticeSection;

6. 뭐가 좋은가? 

이 과정은 단순히 ‘타입을 나눈다’는 차원을 넘어서, 다음과 같은 실질적인 이점을 제공한다.

 

  • 예측 가능한 API 구조 덕분에 요청/응답 형식에서 발생할 수 있는 에러를 줄이고, 안정성을 높일 수 있었다.
  • 타입 분리를 통해 로직과 UI 컴포넌트의 결합도를 낮추고, 코드의 응집도를 높였다.
  • mock 데이터 생성이 용이해져 테스트 및 개발 속도가 눈에 띄게 향상되었다.
  • 무엇보다 백엔드와의 협업 커뮤니케이션이 명확해졌고, 기획 변경에도 유연하게 대응할 수 있는 구조를 확보할 수 있다.

 

프론트엔드에서 API 통신은 단순히 데이터를 받아오는 과정을 넘어서, 데이터의 신뢰성, 협업의 효율성, 그리고 코드 유지보수성에 직결된다.

이번 프로젝트에서 우리는 백엔드에서 정의한 Swagger 기반 스펙을 바탕으로, 응답 데이터를 의미 단위로 타입 분리하는 것은