< Next.js 에서 Axios GET 으로 API 조회하기 >

https://rakaso598.github.io/items/items.json 파일을 조회

 

items.json은 깃허브에 배포한 제 블로그의 게시물 데이터 파일입니다.

 

제 items.json 또한 공개된 장소에 누구나 접근할 수 있으므로 Nest.js에서 Axios로 데이터를 요청해 보았습니다.

 

-- Next.js 설치
npx create-next-app@latest

-- 프로젝트 디렉토리로 이동
cd my-nextjs-project

-- 프로젝트 실행
npm run dev

 


 

< 구현해보기 >

/app/request/page.tsx

 

Next.js에서는 `/app` 하위에 있는 `page.tsx`가 기본 화면으로 동작합니다.

 

그래서 `/app/page.tsx`가 `localhost:3000/` 에 접속했을때 보이는 기본 화면이 됩니다.

 

저는 `/app` 하위에 `/request` 를 생성한 후, `page.tsx`를 생성했습니다.

 

이 `/reqeust/page.tsx` 화면을 보려면, `localhost:3000/request` 로 접근하면 렌더링된 화면을 볼 수 있습니다.

 

 

여기에 접속하면 제 게시물 데이터들이 담긴 JSON 파일을 조회할 수 있는데요,

 

이것을 Axios를 통해 GET 요청하여 데이터를 응답받을 것입니다.

const Request: React.FC = () => {

  return (
    <div>
      <h1>Request</h1>
      <p>This is the request page of the application.</p>
      
      // 문자열로 변환
      <pre>{JSON.stringify(data, null, 2)}</pre>
      
    </div>
  );
};

export default Request;

 

`<pre>` 태그는 HTML에서 "preformatted text" 를 의미하는 태그입니다.

 

이 태그는 텍스트를 포맷팅된 형태로 표시할 때 사용됩니다.

 

`<pre>` 태그 안에 있는 텍스트는 공백과 줄바꿈이 유지되며, 고정 폭 폰트로 표시됩니다.

 

이 상태에서 API 요청을 위해 fetchData() 를 작성합니다.

  const [data, setData] = useState<any | null>(null); // 가져온 데이터를 저장
  const [loading, setLoading] = useState(true); // 로딩중 상태, 기본값: true
  const [error, setError] = useState<string | null>(null); // 에러를 저장

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get('https://rakaso598.github.io/items/items.json');
        setData(response.data);
        setLoading(false);

      } catch (err: unknown) {
        if (err instanceof Error) {
          setError(err.message); // err를 Error 타입으로 체크한 후 message 사용
        } else {
          setError('An unknown error occurred'); // 알 수 없는 에러 처리
        }
      }
    };

    fetchData();
  }, []);

 

▶ useState :

  • const [data, setData] = useState<any | null>(null);: 상태 변수 data를 생성하며 초기값은 null입니다. setData 함수는 data를 업데이트하는 데 사용됩니다.
  • const [loading, setLoading] = useState(true);: loading 상태를 true로 초기화합니다. setLoading 함수는 이 값을 변경하는 데 사용됩니다.
  • const [error, setError] = useState<string | null>(null);: error 상태를 설정하며 초기값은 null입니다. setError 함수는 error를 업데이트하는 데 사용됩니다.

 

▶ useEffect :

  • useEffect 훅은 함수 컴포넌트에서 사이드 이펙트를 수행하는 데 사용됩니다. fetchData 함수는 useEffect 내부에서 정의되어 있으며, API에서 데이터를 가져오기 위해 즉시 호출됩니다.
  • 컴포넌트가 마운트되면 fetchData가 호출되어 API에서 데이터를 가져옵니다.
  • 데이터를 성공적으로 가져오면, 응답 데이터를 사용하여 data를 업데이트하고 loading을 false로 설정합니다.
  • 가져오는 동안 오류가 발생하면, 오류가 Error 인스턴스인지 확인한 후 error 상태를 적절히 설정합니다.

 

`useState`를 사용하면 컴포넌트의 상태를 관리할 수 있으며, `useEffect`를 사용하면 컴포넌트의 라이프사이클의 특정 시점(여기서는 빈 의존성 배열 [] 덕분에 컴포넌트가 마운트될 때)에 사이드 이펙트를 수행할 수 있습니다.

 


 

  // 조건부 렌더링
  if (loading) {
    return <div>Loading...</div>;
  }

  // 에러
  if (error) {
    return <div>Error: {error}</div>;
  }

 

▶ 코드 흐름 :

  • 로딩 중: loading이 true면 "Loading..." 메시지를 표시합니다.
  • 에러 발생 시: loading이 false가 되면 에러 상태를 확인하고, 에러가 있으면 에러 메시지를 표시합니다.
  • 데이터 표시: loading이 false이고 에러가 없는 경우에만 실제 데이터를 포함한 UI를 렌더링합니다.

 


 

< 전체 코드 >

"use client";

import React, { useEffect, useState } from 'react';
import axios from 'axios';

const Request: React.FC = () => {

  const [data, setData] = useState<any | null>(null); // 가져온 데이터를 저장
  const [loading, setLoading] = useState(true); // 로딩중인상태, 기본값: true
  const [error, setError] = useState<string | null>(null); // 에러를 저장

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get('https://rakaso598.github.io/items/items.json');
        setData(response.data);
        setLoading(false);

      } catch (err: unknown) {
        if (err instanceof Error) {
          setError(err.message); // err를 Error 타입으로 체크한 후 message 사용
        } else {
          setError('An unknown error occurred'); // 알 수 없는 에러 처리
        }
      }
    };

    fetchData();
  }, []);

  // 조건부 렌더링
  if (loading) {
    return <div>Loading...</div>;
  }

  // 에러
  if (error) {
    return <div>Error: {error}</div>;
  }

  // 데이터 표시
  return (
    <div>
      <h1>Request</h1>
      <p>This is the request page of the application.</p>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
};

export default Request;

 

이렇게 `app/request/page.tsx` 를 작성 완료했습니다.

 

그리고 `npm run dev` 를 통해 Next.js 프로젝트를 실행시키면...

 

localhost:3000/request

 

`localhost:3000/request` 에서 렌더링이 완료됩니다.

 


< 참고 문서 >

 

기본 예제 | Axios Docs

 

기본 예제 | Axios Docs

기본 예제 Axios를 사용하기 위한 기본 예제 참고: CommonJS 사용법 require()를 이용한 CommonJS를 사용하는 동안 TypeScript 타이핑(인텔리센스 / 자동 완성)을 사용하려면, 다음 방법을 쓰세요. const axios = r

axios-http.com

Request - Web API | MDN

 

Request - Web API | MDN

Fetch API의 Request 인터페이스는 리소스 요청을 나타냅니다.

developer.mozilla.org

 

npm run start -> successfully started
'localhost:3000'에서 문구 출력 -> 어플리케이션 실행 성공
MongoDB 실행중
수입/지출관리 기능을 처리할 'income-expense' 파일들


NestJS를 사용해 수입/지출 관리를 위한 기본 REST API를 구축하고 실행한 것입니다.
주요 작업 내용과 코드를 기록한 문서입니다.

 


1. NestJS 프로젝트 생성

  • NestJS는 백엔드 애플리케이션 프레임워크로, 모듈 기반 구조와 데코레이터를 활용한 선언적 코딩 스타일을 제공합니다.
  • 초기 프로젝트 설정과 IncomeExpenseModule 생성은 애플리케이션의 구조를 정리하는 작업입니다.

 


2. Mongoose 및 MongoDB 연결

  • Mongoose를 통해 MongoDB와 통신할 수 있도록 설정했습니다.
  • MongoDB는 NoSQL 데이터베이스로, 수입/지출 데이터를 저장할 데이터베이스로 사용되었습니다.
  • MongooseModule.forFeature를 사용해 IncomeExpense라는 Mongoose 모델을 등록했습니다.

 


3. REST API 구성

  • NestJS에서 컨트롤러와 서비스 계층을 구현했습니다:
    1. Controller (컨트롤러):
      • API의 엔드포인트를 정의하고, 클라이언트의 요청을 처리합니다.
      • 예: POST /income-expenses로 수입/지출 데이터를 생성.
    2. Service (서비스 계층):
      • 비즈니스 로직을 처리합니다.
      • 컨트롤러에서 받은 요청 데이터를 Mongoose 모델을 통해 MongoDB에 저장하거나, 조회/수정/삭제합니다.
    3. DTO (Data Transfer Object):
      • 클라이언트가 전달하는 데이터를 구조화하고, 유효성 검사를 쉽게 하기 위해 정의된 객체입니다.

 


4. REST API CRUD 작업

다음 CRUD 기능을 API로 구현했습니다:

  1. Create:
    • 수입/지출 데이터를 생성.
    • 예: POST /income-expenses로 데이터 전송 → MongoDB에 저장.
  2. Read (All):
    • 모든 수입/지출 데이터를 조회.
    • 예: GET /income-expenses.
  3. Read (One):
    • 특정 수입/지출 데이터를 조회.
    • 예: GET /income-expenses/:id.
  4. Update:
    • 기존 데이터를 수정.
    • 예: PUT /income-expenses/:id로 데이터 업데이트.
  5. Delete:
    • 데이터를 삭제.
    • 예: DELETE /income-expenses/:id.

 


5. 에러 해결

구현 중 다음과 같은 문제를 해결했습니다:

  • 타입 선언 문제: IncomeExpense 타입이 값으로 사용될 때 발생한 오류.
    • 해결: Mongoose 모델 이름을 문자열('IncomeExpense')로 명시.
  • NestJS 종속성 문제: IncomeExpenseService가 IncomeExpenseModel을 주입받지 못할 때 발생한 오류.
    • 해결: MongooseModule.forFeature를 통해 올바르게 모델 등록.

 


6. Hello, World 실행 성공

  • 프로젝트 실행 후, REST API 엔드포인트에 정상적으로 접근하여 데이터를 생성/조회할 수 있었습니다. 
    1. NestJS 애플리케이션이 성공적으로 실행됨.
    2. MongoDB와 연결된 Mongoose 모델이 정상적으로 작동.
    3. REST API가 클라이언트 요청을 처리할 준비가 완료됨.

 


■ 작업의 목적과 결과

  • 목적: 개인 재정 관리를 위한 백엔드 시스템 구축.
    • MongoDB에 데이터를 저장하고, 필요한 데이터를 클라이언트에 제공하는 RESTful API.
  • 결과: NestJS 기반의 API 서버가 성공적으로 실행되었으며, 데이터베이스와 상호작용 가능한 CRUD 기능이 구현됨.

 


다음 작업

  1. API 테스트:
    • Postman 또는 cURL을 사용해 모든 엔드포인트를 테스트합니다.
    • 예: 수입/지출 데이터 생성, 수정, 삭제 요청을 보내고 데이터베이스에서 결과 확인.
  2. 프론트엔드 연결:
    • Next.js와 같은 프레임워크를 사용해 사용자 인터페이스를 개발.
    • 이 API를 통해 데이터를 시각화하고 사용자와 상호작용할 수 있도록 설계.
  3. 기능 확장:
    • 인증/인가 추가 (예: JWT).
    • 더 복잡한 데이터 분석 기능 (예: 월별 통계).
    • 데이터 유효성 검사 강화.

지금까지의 작업은 백엔드 애플리케이션의 기본 뼈대를 완성한 상태이며, 다음 단계에서는 이를 기반으로 추가 기능과 UI를 통합해 나가면 됩니다.

 


 

NestJS 프로젝트를 시작하고 실행하기 위해 콘솔에서 사용한 명령어들을 정리

 

1. NestJS 프로젝트 생성

# 새로운 NestJS 프로젝트 생성
npx @nestjs/cli new personal-finance-backend
  • personal-finance-backend: 프로젝트 이름.
  • 생성 과정에서 패키지 매니저를 선택 (예: npm 또는 yarn).

 


2. 프로젝트 폴더로 이동

cd personal-finance-backend

 


3. Mongoose 및 MongoDB 설정

# Mongoose와 관련 패키지 설치
npm install @nestjs/mongoose mongoose
  • @nestjs/mongoose: NestJS에서 Mongoose를 쉽게 사용할 수 있도록 지원.
  • mongoose: MongoDB와 통신하는 ODM(Object Document Mapper) 라이브러리.

 


4. IncomeExpense 모듈 및 리소스 생성

NestJS CLI로 모듈과 기본 파일 생성:

# 수입/지출 모듈 생성
npx nest generate module income-expense
# 수입/지출 컨트롤러 및 서비스 생성
npx nest generate controller income-expense npx nest generate service income-expense
  • income-expense.module.ts, income-expense.controller.ts, income-expense.service.ts 파일이 자동으로 생성됨.

 


5. 애플리케이션 실행

NestJS 서버 실행:

npm run start
  • 서버는 기본적으로 http://localhost:3000에서 실행됩니다.
  • 실행 중 코드 변경 시 자동으로 서버가 재시작되도록 npm run start:dev를 사용할 수도 있습니다.

 


6. MongoDB 연결 테스트

  • .env 파일이나 설정 파일에 MongoDB 연결 URI 추가:
DATABASE_URI=mongodb://localhost:27017/finance
  • app.module.ts에서 MongooseModule로 연결 설정:
# typescript
imports: [ MongooseModule.forRoot(process.env.DATABASE_URI), ],

 


7. 수동으로 DTO 파일 생성 (선택 사항)

# src/income-expense/dto 폴더 생성
mkdir src/income-expense/dto

# DTO 파일 생성 (예: CreateIncomeExpenseDto 및 UpdateIncomeExpenseDto)

 


8. Docker로 MongoDB 실행 (로컬 환경에서 필요한 경우에 / 선택 사항)

# Docker로 MongoDB 실행
docker run --name mongodb -d -p 27017:27017 mongo
  • Docker를 통해 MongoDB 컨테이너를 실행합니다.
  • MongoDB가 이미 설치되어 있다면 로컬 MongoDB 서비스만 시작하면 됩니다.

 


실행 결과

  1. NestJS 애플리케이션이 MongoDB와 연결됨.
  2. IncomeExpenseModule을 통해 기본적인 REST API CRUD 기능이 작동.

 


구현에 사용한 income-expense 파일 내용
// src/income-expense/models/income-expense.controller.ts
import { Controller, Post, Get, Body, Param, Put, Delete } from '@nestjs/common';
import { IncomeExpenseService } from './income-expense.service';
import { CreateIncomeExpenseDto, UpdateIncomeExpenseDto } from './dto/income-expense.dto';



@Controller('income-expenses')
export class IncomeExpenseController {
    constructor(private readonly incomeExpenseService: IncomeExpenseService) { }

    @Post()
    create(@Body() createIncomeExpenseDto: CreateIncomeExpenseDto) {
        return this.incomeExpenseService.create(createIncomeExpenseDto);
    }

    @Get()
    findAll() {
        return this.incomeExpenseService.findAll();
    }

    @Get(':id')
    findOne(@Param('id') id: string) {
        return this.incomeExpenseService.findOne(id);
    }

    @Put(':id')
    update(@Param('id') id: string, @Body() updateIncomeExpenseDto: UpdateIncomeExpenseDto) {
        return this.incomeExpenseService.update(id, updateIncomeExpenseDto);
    }

    @Delete(':id')
    delete(@Param('id') id: string) {
        return this.incomeExpenseService.delete(id);
    }
}
// src/income-expense/models/income-expense.model.ts
import { Schema, Document } from 'mongoose';

export const IncomeExpenseSchema = new Schema({
    type: { type: String, required: true },
    amount: { type: Number, required: true },
    description: { type: String, required: false },
});

// Document를 확장한 인터페이스 정의
export interface IncomeExpense extends Document {
    type: string;
    amount: number;
    description?: string;
}
// src/income-expense/income-expense.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { IncomeExpenseService } from './income-expense.service';
import { IncomeExpenseController } from './income-expense.controller';
import { IncomeExpenseSchema } from './income-expense.model';

@Module({
    imports: [
        MongooseModule.forFeature([{ name: 'IncomeExpense', schema: IncomeExpenseSchema }]),
    ],
    controllers: [IncomeExpenseController],
    providers: [IncomeExpenseService],
    exports: [IncomeExpenseService], // 필요 시 다른 모듈에서 사용 가능하도록 export
})
export class IncomeExpenseModule { }
// src/income-expense/income-expense.schema.ts
import { Schema, Document } from 'mongoose';

export const IncomeExpenseSchema = new Schema({
    type: { type: String, enum: ['income', 'expense'], required: true },
    amount: { type: Number, required: true },
    description: { type: String },
    date: { type: Date, default: Date.now },
});

export interface IncomeExpense extends Document {
    type: string;
    amount: number;
    description?: string;
    date: Date;
}
// src/income-expense/income-expense.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { IncomeExpense } from './income-expense.model'; // 모델 타입 임포트
import { CreateIncomeExpenseDto } from './dto/income-expense.dto'; // DTO 임포트

@Injectable()
export class IncomeExpenseService {
    constructor(
        @InjectModel('IncomeExpense') private readonly incomeExpenseModel: Model<IncomeExpense>, // 모델 이름을 문자열로 사용
    ) { }

    async create(createIncomeExpenseDto: CreateIncomeExpenseDto): Promise<IncomeExpense> {
        const createdIncomeExpense = new this.incomeExpenseModel(createIncomeExpenseDto);
        return createdIncomeExpense.save();
    }

    async findAll(): Promise<IncomeExpense[]> {
        return this.incomeExpenseModel.find().exec();
    }

    async findOne(id: string): Promise<IncomeExpense> {
        return this.incomeExpenseModel.findById(id).exec();
    }

    async update(id: string, updateIncomeExpenseDto: any): Promise<IncomeExpense> {
        return this.incomeExpenseModel.findByIdAndUpdate(id, updateIncomeExpenseDto, { new: true }).exec();
    }

    async delete(id: string): Promise<void> {
        await this.incomeExpenseModel.findByIdAndDelete(id).exec();
    }
}
// src/income-expense/dto/income-expense.dto.ts
export class CreateIncomeExpenseDto {
    type: string;
    amount: number;
    description?: string;
}

export class UpdateIncomeExpenseDto {
    amount: number;
    description?: string;
}

 

Node.js와 npm이 컴퓨터에 올바르게 설치되어 있어야 합니다.

 

프로젝트를 설치할 폴더로 이동 후, 다음 코드를 작성합니다.

 


 

/*	
	터미널에서 입력
*/

// 프로젝트 초기화
mkdir rest-api-server
cd rest-api-server
npm init -y

// Express 설치
npm install express
/*
	server.js를 루트 경로에 생성하고, 아래 코드 작성
*/

const express = require('express');
const app = express();
const port = 3000;

// JSON 요청을 파싱하기 위한 미들웨어
app.use(express.json());

// 간단한 라우트 예제
app.get('/', (req, res) => {
  res.send('Hello, REST API Server!');
});

// 서버 실행
app.listen(port, () => {
  console.log(`Server is running at http://localhost:${port}`);
});
/*
	server.js 아래 이어서 작성
	API 예제 및 CRUD 기능	
*/

// 데이터 예제
let users = [
  { id: 1, name: 'John Doe' },
  { id: 2, name: 'Jane Doe' }
];

// 1. 모든 사용자 조회 (GET)
app.get('/api/users', (req, res) => {
  res.json(users);
});

// 2. 특정 사용자 조회 (GET)
app.get('/api/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ message: 'User not found' });
  res.json(user);
});

// 3. 사용자 추가 (POST)
app.post('/api/users', (req, res) => {
  const newUser = {
    id: users.length + 1,
    name: req.body.name
  };
  users.push(newUser);
  res.status(201).json(newUser);
});

// 4. 사용자 정보 수정 (PUT)
app.put('/api/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ message: 'User not found' });

  user.name = req.body.name;
  res.json(user);
});

// 5. 사용자 삭제 (DELETE)
app.delete('/api/users/:id', (req, res) => {
  const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));
  if (userIndex === -1) return res.status(404).json({ message: 'User not found' });

  const deletedUser = users.splice(userIndex, 1);
  res.json(deletedUser);
});
/*
	터미널에 입력
*/

// 코드 자동 반영 (선택사항)
npm install -g nodemon
nodemon server.js

 


 

브라우저에서 localhost:3000를 테스트합니다.

 

 

localhost:3000 접속 시

 

localhost:3000/api/users 테스트

 


 

Node.js와 Express 프레임워크를 사용하여 간단한 API 서버를 구현했습니다.

 

REST API를 빠르게 요청하고 응답받을 수 있는 경량 서버를 구축할 수 있었습니다.

 

Node.js는 초기 개발 속도가 빠르고 설정이 간단해 빠른 개발과 배포가 가능합니다.

 

스프링부트는 복잡한 비즈니스 로직과 안정성, 확장성이 중요한 대규모 시스템에 적합하지만,

빠른 응답과 실시간 처리가 필요한 경량 API 서버에는 Node.js도 활용성이 매우 높습니다.

 

또한, 간단한 API 서버나 RESTful API 서버 구축에 효율적이며,

AWS Lambda나 Google Cloud Functions와 같은 서버리스 플랫폼과의 연동도 용이합니다.

 

+ Recent posts