본문 바로가기
프로젝트/웹

[React] API 계층 분석 : 한국어 교육 웹 서비스 '재밌는한국어' 프로젝트

by AI읽어주는남자 2025. 10. 16.
반응형

2. API 계층 분석: adminApi.js

이 파일은 백엔드 서버와 통신하는 모든 API 요청을 중앙에서 관리하는 역할을 합니다. axios 라이브러리를 사용하여 HTTP 요청을 생성하고, 기능별로 모듈화하여 내보냅니다.

2.1. 코드 전문 (주석 포함)

// axios 라이브러리를 가져옵니다. HTTP 통신을 쉽게 할 수 있게 도와주는 도구입니다.
import axios from "axios";

// API 요청의 기본 URL을 상수로 정의합니다. 이렇게 하면 서버 주소가 변경될 때 여기만 수정하면 됩니다.
const BASE_URL = "http://localhost:8080/saykorean/admin";

// JSON 데이터를 전송하기 위한 axios 인스턴스(객체)를 생성합니다.
// 대부분의 텍스트 기반 데이터는 이 인스턴스를 통해 전송됩니다.
const api = axios.create({
    baseURL: BASE_URL, // 기본 URL 설정
    headers: {
        "Content-Type": "application/json", // 보내는 데이터의 형식이 JSON임을 명시
    },
});

// 파일(이미지, 음성)을 전송하기 위한 별도의 axios 인스턴스를 생성합니다.
// 파일 데이터는 'multipart/form-data' 형식으로 보내야 합니다.
const apiFormData = axios.create({
    baseURL: BASE_URL,
    headers: {
        "Content-Type": "multipart/form-data", // 보내는 데이터에 파일이 포함되어 있음을 명시
    },
});

// [1] 장르(Genre) 관련 API 함수들을 객체로 묶어서 관리합니다.
export const genreApi = {
    // 1) 모든 장르 목록을 조회하는 함수 (GET 요청)
    getAll: () => api.get("/study/genre"),
    // 2) 새로운 장르를 생성하는 함수 (POST 요청). genreDto 객체를 JSON 형태로 서버에 전송합니다.
    create: (genreDto) => api.post("/study/genre", genreDto),
    // 3) 특정 장르를 삭제하는 함수 (DELETE 요청). URL 파라미터로 genreNo를 전달합니다.
    delete: (genreNo) => api.delete(`/study/genre?genreNo=${genreNo}`),
};

// [2] 교육 주제/해설(Study) 관련 API 함수들을 객체로 묶습니다.
export const studyApi = {
    // 1) 모든 교육 목록 조회
    getAll: () => api.get("/study"),
    // 2) 특정 교육 상세 조회
    getIndi: (studyNo) => api.get(`/study/indi?studyNo=${studyNo}`),
    // 3) 새 교육 생성
    create: (studyDto) => api.post("/study", studyDto),
    // 4) 기존 교육 수정 (PUT 요청). studyDto 객체를 JSON 형태로 전송합니다.
    update: (studyDto) => api.put("/study", studyDto),
    // 5) 특정 교육 삭제
    delete: (studyNo) => api.delete(`/study?studyNo=${studyNo}`),
};

// [3] 예문(Exam) 관련 API 함수들을 객체로 묶습니다.
export const examApi = {
    // 1) 모든 예문 목록 조회
    getAll: () => api.get("/study/exam"),
    // 2) 특정 예문 상세 조회
    getIndi: (examNo) => api.get(`/study/exam/indi?examNo=${examNo}`),
    // 3) 새 예문 생성 (파일 포함 가능성이 있으므로 FormData 사용)
    create: (examDto) => {
        // 파일과 텍스트를 함께 보내기 위해 FormData 객체를 생성합니다.
        const formData = new FormData();

        // 3-1) examDto 객체의 모든 키(key)를 순회하며 FormData에 추가합니다.
        Object.keys(examDto).forEach(key => {
            // 'imageFile'이 아니고, 값이 null이나 undefined가 아닌 경우에만 추가합니다.
            if (key !== "imageFile" && examDto[key] != null && examDto[key] !== undefined) {
                formData.append(key, examDto[key]);
            }
        });

        // 3-2) 이미지 파일이 있으면 'imageFile'이라는 이름으로 FormData에 추가합니다.
        if (examDto.imageFile) {
            formData.append("imageFile", examDto.imageFile);
        }

        // 3-3) 최종적으로 만들어진 FormData를 apiFormData 인스턴스를 통해 서버에 POST 요청으로 보냅니다.
        return apiFormData.post("/study/exam", formData);
    },
    // 4) 기존 예문 수정
    update: (examDto) => {
        const formData = new FormData();

        // 4-1) 텍스트 데이터 변경
        Object.keys(examDto).forEach(key => {
            // 'newImageFile'(새로 업로드할 파일)이 아닌 데이터만 추가
            if (key !== "newImageFile" && examDto[key] != null && examDto[key] !== undefined) {
                formData.append(key, examDto[key]);
            }
        });

        // 4-2) 새로 변경할 이미지 파일이 있으면 'newImageFile' 이름으로 추가
        if (examDto.newImageFile) {
            formData.append('newImageFile', examDto.newImageFile);
        }

        // 4-3) 만들어진 FormData를 PUT 요청으로 보냅니다.
        return apiFormData.put('/study/exam', formData);
    },
    // 5) 특정 예문 삭제
    delete: (examNo) => api.delete(`/study/exam?examNo=${examNo}`),
};

// [4] 음성(Audio) 관련 API 함수들을 객체로 묶습니다.
export const audioApi = {
    // 1) 모든 음성 목록 조회
    getAll: () => api.get('/audio'),
    // 2) 특정 음성 상세 조회
    getIndi: (audioNo) => api.get(`/audio/indi?audioNo=${audioNo}`),
    // 3) 새 음성 생성 (파일이므로 FormData 사용)
    create: (audioDto) => {
        const formData = new FormData();

        // 3-1) 텍스트 데이터(lang, examNo 등) 추가
        Object.keys(audioDto).forEach(key => {
            if (key !== "audioFile" && audioDto[key] !== null && audioDto[key] !== undefined) {
                formData.append(key, audioDto[key]);
            }
        });

        // 3-2) 음성 파일이 있으면 'audioFile' 이름으로 추가
        if (audioDto.audioFile) {
            formData.append('audioFile', audioDto.audioFile);
        }

        // 3-3) 만들어진 FormData를 POST 요청으로 보냅니다.
        return apiFormData.post('/audio', formData);
    },
    // 4) 기존 음성 수정
    update: (audioDto) => {
        const formData = new FormData();

        // 4-1) 텍스트 데이터 추가
        Object.keys(audioDto).forEach(key => {
            if (key !== 'newAudioFile' && audioDto[key] !== null && audioDto[key] !== undefined) {
                formData.append(key, audioDto[key]);
            }
        });

        // 4-2) 새로 변경할 음성 파일이 있으면 'newAudioFile' 이름으로 추가
        if (audioDto.newAudioFile) {
            formData.append('newAudioFile', audioDto.newAudioFile);
        }

        // 4-3) 만들어진 FormData를 PUT 요청으로 보냅니다.
        return apiFormData.put('/audio', formData);
    },
    // 5) 특정 음성 삭제
    delete: (audioNo) => api.delete(`/audio?audioNo=${audioNo}`),
};

// 기본 axios 인스턴스도 내보냅니다. (현재 코드에서는 사용되지 않음)
export default api;

2.2. 핵심 분석

  1. 두 개의 Axios 인스턴스: 왜 apiapiFormData 두 개를 만들었을까요?
    • 서버는 요청 헤더의 Content-Type을 보고 데이터를 어떻게 해석할지 결정합니다.
    • 일반적인 텍스트 데이터는 application/json 형식으로 보내는 것이 표준적이고 효율적입니다.
    • 하지만 이미지나 오디오 파일은 텍스트가 아니므로, multipart/form-data라는 다른 형식으로 감싸서 보내야 합니다.
    • 따라서, 데이터의 종류에 따라 적절한 인스턴스를 사용하기 위해 두 개를 만들어 둔 것입니다.
  2. API 모듈화: genreApi, studyApi 등으로 API를 기능별로 묶었습니다.
    • 이렇게 하면 코드가 훨씬 깔끔해지고, 어떤 컴포넌트에서든 import { studyApi } from '../api/adminApi' 와 같이 필요한 API만 쉽게 가져다 쓸 수 있습니다.
    • 각 함수는 getAll, create, delete 등 역할이 명확한 이름을 가지고 있어 가독성이 좋습니다.
  3. FormData 동적 생성: examApi.createaudioApi.create를 보면 FormData 객체를 사용하는 것을 볼 수 있습니다.
    • FormData는 HTML 폼(form) 데이터를 쉽게 만들고 전송하게 해주는 Web API입니다.
    • Object.keys(examDto).forEach(...) 루프를 통해 examDto 객체에 있는 모든 텍스트 데이터를 formData.append(key, value) 형태로 추가합니다.
    • 그 다음, 파일 데이터(imageFile 또는 audioFile)를 별도로 추가합니다.
    • 이 과정을 통해 텍스트와 파일을 하나의 요청에 담아 서버로 보낼 수 있습니다.
반응형