맥 터미널에서 아래 명령어 실행하기

 

1. python3 --version

파이썬이 설치되어 있지 않다면 파이썬부터 설치해야 한다.

파이썬을 설치하면 pip3도 함께 설치된다.

 

2. pip3 --version

3. pip3 install --upgrade pip

4. pip install jupyter

 

터미널에서 jupyter notebook 실행하면 아래와 같은 화면이 나온다.

 

View - Open JupyterLab를 클릭하면 JupyterLab을 사용할 수 있다.

JupyterLab는 Jupyter Notebook의 업그레이드 버전으로, 더 강력한 기능과 개선된 UI를 제공하는 웹 기반 인터페이스이다.

여러 개의 노트북, 터미널, 텍스트 편집을 한 화면에서 동시에 다룰 수 있는 IDE(통합 개발 환경)이다.

 

Python, Markdown, HTML, JavaScript 등 편집이 가능하고, 다양한 확장 기능(플러그인, 다크 모드, Vim 등)이 추가가 가능하다.

값 수정하기

from pandas import *

data = [1000, 2000, 3000]
index = ["메로나", "구구콘", "하겐다즈"]
s = Series(data=data, index=index)

print(s)

# 값 수정
s.iloc[0] = 0
s.loc['구구콘'] = 0
s['하겐다즈'] = 0

print(s)

 

값 삭제하기

drop 메서드는 원본 데이터가 변경되는 것을 방지하기 때문에 시리즈 원본 데이터를 제거하지 않고, 새로운 시리즈 객체를 반환한다.

따라서 변수에 다시 바인딩해야 한다.

# 값 삭제
s = s.drop('메로나')
s = s.drop(['구구콘', '하겐다즈']) # 여러 개 삭제

print(s)

 

값 추가하기

loc[]나 concat() 방법을 가장 많이 사용한다.

# 값 추가
s['new1'] = 1
s.at["new2"] = 2
s.loc["new3"] = 3
s = concat([s, Series([4], index=["new4"])])

print(s)

iloc (정수 위치 기반 인덱싱)

정수 기반으로 데이터를 선택한다.

리스트처럼 0부터 시작하는 정수 인덱스를 사용한다.

슬라이싱(:)을 활용하여 여러 개의 값을 선택할 수 있다.

 

loc (라벨 기반 인덱싱)

인덱스(라벨) 값을 기준으로 데이터를 선택한다.

숫자가 아닌 문자열 등 사용자 지정 인덱스를 사용한다.

범위를 지정할 때 끝값까지 포함한다. (iloc와 차이점)

 

인덱싱(indexing)과 슬라이싱(slicing)의 차이

pandas.Series나 pandas.DataFrame에서 데이터를 선택하는 방법은 크게 두 가지가 있다.

  1. 인덱싱: 하나의 값 또는 여러 개의 개별 값을 선택
  2. 슬라이싱: 연속된 범위의 값을 선택
import pandas as pd

data = [100, 200, 300]
index = ["월", "화", "수"]
s = pd.Series(data, index)

print(s.iloc[0]) # 100
print(s.iloc[1]) # 200
print(s.iloc[2]) # 300
print(s.iloc[3]) # IndexError

print(s.iloc[-1]) # 300
print(s.iloc[-2]) # 200
print(s.iloc[-3]) # 200
print(s.iloc[-4]) # IndexError

print(s.loc["월"]) # 100
print(s.loc["화"]) # 200
print(s.loc["수"]) # 300

# 인덱싱 (개별 값)
print(s.iloc[[0, 2]]) # 월 100 수 300
print(s.loc[["월", "수"]]) # 월 100 수 300

# 슬라이싱 (범위)
print(s.iloc[0:2]) # 월 100 화 200
print(s.loc["월":"화"]) # 월 100 화 200

Series

pandas.Series는 1차원 배열 형태의 데이터 구조고, 인덱스(index)와 값(value)으로 구성된 자료형이다.

 

주요 특징

  • 인덱스와 값으로 구성된다. 기본적으로 0부터 시작하는 정수형 인덱스가 자동 할당되지만, 직접 지정할 수도 있다.
  • 동일한 데이터 타입을 가진다. NumPy 배열(numpy.ndarray)과 유사하지만, 인덱스를 활용할 수 있다.
  • 딕셔너리와 비슷한 구조이다. 키(key) = 인덱스, 값(value) = 데이터 라고 생각하면 이해하기 쉽다.

 

기본 시리즈 생성 방법

import pandas as pd

data = ['가', '나', '다', '라'] # type = list
s = pd.Series(data) # type = pandas.core.series.Series

print(s)

 

자동 생성되는 정수형 인덱스가 아닌 별도로 지정할 수 있다.

from pandas import Series

data = [100, 200, 300]
index = ["월", "화", "수"]
s = Series(data, index)

print(s)

 

인덱스를 별도의 변수명으로 지정해도 된다.

from pandas import Series

name = ["메로나", "누가바", "빠삐코"] # data = name
price = [500, 800, 200] # index = price
menu = Series(name, price)

print(menu)

브로드캐스트 (Broadcasting)

파이썬에서 브로드캐스트는 주로 NumPy 배열에서 사용되는 개념으로, 다양한 크기의 배열들 간에 연산을 가능하게 만든다.

크기가 다른 배열끼리 연산할 때, 작은 배열이 큰 배열의 크기게 맞게 자동으로 확장되어 연산이 이루어지도록 도와준다.

즉, 작은 배열이 큰 배열에 맞게 확장되거나 "브로드캐스트" 되어 연산을 수행한다.

 

기본 연산

import numpy as np

a = np.array([10, 20, 30])
b = np.array([1, 2, 3])
c = np.array([1, 2, 3, 4, 5]) # a와 c를 연산하면 ValueError가 발생

print(a + b) # [11 22 33]
print(a - b) # [9 18 27]
print(a * b) # [10 40 90]
print(a / b) # [10. 10. 10.]
print(a % b) # [0 0 0]
print(a + 5) # [15, 25, 35]
data1 = np.array([1, 2, 3]) # 1D 배열
data2 = np.array([
    [10, 20, 30],
    [40, 50, 60],
    [70, 80, 90]
]) # 2D 배열

print(data1[0]) # 1
print(data2[0]) # [10 20 30]

print(data2[0, 2]) # 30
print(data2[0][2]) # 30

print(data1[1] * 10) # 20
print(data2[1] * 10) # [400 500 600]

print(data1 + data2) # [[11 22 33] [41 52 63] [71 82 93]]

 

조건문

arr = np.array([10, 20, 30, 40, 50])
cond1 = arr > 10
cond2 = arr < 40

print(arr > 20) # [False False  True  True  True]

print(cond1) # [False  True  True  True  True]
print(cond2) # [ True  True  True False False]

print(arr[cond1 & cond2]) # [20 30]
print(arr[cond1 & cond2] / 10) # [2. 3.]

 

함수와 메서드

  • axis=0: 열 방향(세로) 계산
  • axis=1: 행 방향(가로) 계산
arr = np.array([10, 20, 30, 40, 50])

print("합계:", arr.sum())  # 150
print("최솟값:", arr.min())  # 10
print("최댓값:", arr.max())  # 50
print("평균:", arr.mean())  # 30.0
print("표준편차:", arr.std())  # 14.142135623730951
print("분산:", arr.var())  # 200.0
arr_2d = np.array([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
])

print("전체 합계:", arr_2d.sum())  # 45

print("열 방향 합계:", arr_2d.sum(axis=0))  # [12 15 18]
print("행 방향 합계:", arr_2d.sum(axis=1))  # [ 6 15 24]

print("열 방향 최댓값:", arr_2d.max(axis=0))  # [7 8 9]
print("행 방향 최댓값:", arr_2d.max(axis=1))  # [3 6 9]​

NumPy

NumPy는 Numerical Python의 약자로, 수치 연산을 효율적으로 처리할 수 있는 파이썬의 핵심 라이브러리이다.

배열(Array), 행렬(Matrix), 고차원 배열 및 그에 대한 연산을 지원하고, 수학적·과학적 계산을 할 때 필수적으로 사용한다.

 

주요 특징

  • 고속 연산:  Numpy는 C로 구현되어 있어 파이썬의 기본 리스트보다 훨씬 빠르게 수치 연산을 수행할 수 있다.
  • 다차원 배열 지원: 파이썬의 기본 리스트는 1차원 배열만 지원하지만, Numpy는 다차원 배열을 지원해 다양한 차원의 데이터를 처리할 수 있다.
  • 백터화 연산: 반복문을 사용하지 않고, 배열에 대한 연산을 백터화하여 빠르게 처리한다.
  • 다양한 수학 함수: 수학 함수, 선형 대수 연산, 푸리에 변환 등 수치적 계산을 위한 함수들을 제공한다.

 

NumPy Install

pip install numpy

 

NumPy Import

from numpy import *
또는
import numpy as np

 

NumPy Type

List (리스트)

  • 리스트는 파이썬의 기본 자료형으로, 순서가 있는 집합이다.
  • 리스트는 여러 데이터를 순차적으로 저장하고, 다양한 타입의 요소를 포함한다.
  • 인덱싱, 슬라이싱 등을 통해 요소에 접근할 수 있다.
  • 리스트의 요소를 추가, 수정, 삭제할 수 있다.

 

numpy.ndarray (NumPy 배열)

  • 고정 크기 배열이고, 메모리 효율적이다.
  • 다차원 배열 객체로, 고속 연산이 가능하다.
  • 백터화 연산 덕분에 반복문 없이 배열에 대한 연산이 가능하다.
  • 동일한 타입의 데이터만 저장할 수 있다. 즉, 하나의 배열 안에는 모든 원소의 데이터 타입이 동일하다.
import numpy as np

data = [1, 2, 3, 4] # type = list
np_data = np.array(data) # type = numpy.ndarray

print(data) # [1, 2, 3, 4]
print(np_data) # [1 2 3 4]

 

import numpy as np

data = [1, 2, 3, 4]

list_arr = data * 10
np_arr = np.array(data) * 10

print(list_arr)
print(np_arr)

 

multi_arr = np.array([
    [1, 2, 3, 4],
    [10, 20, 30, 40],
    [100, 200, 300, 400]
])

# 행 > 열 순서로 접근
print(multi_arr[0]) # [1 2 3 4]
print(multi_arr[1][2]) # 30
print(multi_arr[1, 2]) # 30
print(multi_arr[:, 3]) # [4 40 400]

왕초보를 위한 React Native 101

https://nomadcoders.co/react-native-for-beginners

 

노마드코더 강의를 참고하고 있습니다.

 


리액트 네이티브란?

리액트 네이티브(React Native)는 페이스북이 개발한 오픈 소스 모바일 애플리케이션 프레임워크로서, React와 JavaScript를 사용하여 Android 및 IOS 모바일 애플리케이션을 개발할 수 있다.

 

특징

  • 리액트를 기반으로 하고 있어 컴포넌트를 사용해 재사용성이 높은 UI 구성요소를 만들 수 있다.
  • 앱이 실행 중일 때 코드를 변경하면 핫 리로딩을 통해 변경 사항을 즉시 반영할 수 있다.
  • 네이티브 모듈을 통해 카메라, 위치 정보, 파일 시스템 등 네이티브 기능에 직접 액세스 할 수 있다.
  • Expo 툴킷을 사용해 별도의 네이티브 코드를 작성하지 않고, 앱을 빠르게 개발할 수 있다.

 

장점

  • 리액트 네이티브는 React와 JavaScript를 사용하기 때문에 동일한 코드 베이스로 Android와 IOS 모바일 애플리케이션 개발이 가능하다.
  • 리액트 네이티브는 활발한 커뮤니티와 다양한 패키지를 가지고 있어 문제 해결이나 새로운 기능 추가에 대해 도움을 받을 수 있다.
  • 네이티브 모듈을 활용해 개발 생산성이 향상된다.
  • 핫 리로딩 기능이 활용해 개발 주기가 빠르다.

 

단점

  • 네이티브 앱(특정 플랫폼 또는 운영 체제에 최적화되어 개발된 모바일 애플리케이션)에 비해 성능의 제약이 있을 수 있다.
  • 특정 플랫폼에 특화된 기능이 필요한 경우, 네이티브 모듈의 사용해야 해서 추가적인 작업이 발생할 수 있다.

 


Expo란

Expo는 리액트 네이티브 앱을 개발하고 배포하기 위한 도구와 서비스를 제공하는 오픈 소스 프레임워크이다.

Expo를 사용하면 네이티브 개발 환경(Java, Android studio 등)을 설정하는 번거로움 없이 빠르게 리액트 네이티브 애플리케이션을 개발할 수 있다.

 

Expo로 리액트 네이티브 설치하기

https://expo.dev/

 

Expo

Expo is an open-source platform for making universal native apps for Android, iOS, and the web with JavaScript and React.

expo.dev

노마드 강의는 2021년 버전이기 때문에 Expo 공식 문서를 참고해 리액트 네이티브를 설치했다.

 

 

node 버전 18 이상

1. expo-app 설치하기

터미널에서 아래 명령어를 실행한다.

npx create-expo-app 폴더명

 

2. 종속성 설치하기

설치한 폴더로 이동 후, 아래 패키지를 설치한다.

npx expo install react-dom react-native-web @expo/webpack-config

 

3. 웹에서 실행하기

터미널에 qr 코드만 나오고 웹이 실행되지 않는다면 w 옵션(open web)을 누른다.

npx expo start

 

4. 웹 결과 화면

 

 

5. 모바일(핸드폰)에서 확인하기

안드로이드를 사용하면 플레이 스토어에서 Expo 앱 설치하기

IOS를 사용하면 앱 스토어에서 Expo Go 앱 설치하기

 

앱 설치가 완료된 후, 회원가입과 로그인을 한다.

 

핸드폰으로 터미널에 나온 qr 코드를 스캔하거나

터미널에서 npx expo login 명령어를 입력해 expo에 로그인을 한다.

 

Expo 앱에 내 프로젝트가 연결된 것을 확인할 수 있다.

※ 주의, 컴퓨터와 핸드폰 와이파이가 같은 인터넷을 사용해야만 핸드폰에서 확인이 가능하다. ※

 

6. 브라우저에서 코딩 및 앱 화면 확인하기

https://snack.expo.dev/

만약 브라우저만 사용해서 코딩을 해야 하는 상황이라면 위의 홈페이지에서 작업을 하면 된다.

어떠한 설치 없이 중앙 편집기에서 코드를 작성하면 우측에서 IOS, Android, Web의 결과 화면을 확인할 수 있다.

또한 회원가입 및 로그인을 한 후에 저장을 하면 나만의 URL이 생성된다.

NestJS로 API 만들기

https://nomadcoders.co/nestjs-fundamentals


노마드코더 강의를 참고하고 있습니다.

 


e2e-spec.ts

Nest.js에서 E2E(End to End) 테스트는 애플리케이션의 전체적인 동작을 테스트하는 방법 중 하나이다.

여러 개의 e2e-spec.ts 파일이 있을 수 있고, E2E 테스트는 사용자가 실제 애플리케이션을 사용할 때와 유사한 환경에서 동작을 검증할 수 있다.

실제 HTTP 요청을 서버에 보내고, 응답을 검증하기 때문에 테스트할 때에도 실제 애플리케이션과 동일한 환경을 세팅해야 한다.

 

1. app.e2e-spec.ts

GET 요청 테스트

describe('/movies', ~), describe('/movies/:id', ~) 2가지 엔드포인트에 대해 GET 요청을 테스트한다.

GET 요청 시, 값이 있으면 200 응답이 예측되고, 값이 없으면 404 응답이 예측된다.

describe('AppController (e2e)', () => {
  describe('/movies', () => {
    it('GET 200', () => {
      return request(app.getHttpServer())
        .get('/movies')
        .expect(200)
        .expect([])
    });
  });

  describe('/movies/:id', () => {
    it('GET 200', () => {
      return request(app.getHttpServer())
        .get('/movies/1')
        .expect(200)
    });

    it('GET 404', () => {
      return request(app.getHttpServer())
        .get('/movies/999')
        .expect(404)
    });
  });
});

 

 

POST 요청 테스트

describe('/movies', ~), describe('/movies/:id', ~) 2가지 엔드포인트에 대해 POST 요청을 테스트한다.

POST 요청 시, 올바른 값을 전송하면 201 응답이 예측되고, 잘못된 값을 전송하면 400 응답이 예측된다.

describe('AppController (e2e)', () => {
  describe('/movies', () => {
    it('POST 201', () => {
      return request(app.getHttpServer())
        .post('/movies')
        .send({
          title: 'Test',
          year: 2024,
          genres: ['Test'],
        })
        .expect(201)
    });

    it('POST 400', () => {
      return request(app.getHttpServer())
        .post('/movies')
        .send({
          name: 'Name',
        })
        .expect(400)
    });
  });
});

 

DELETE 요청 테스트

describe('/movies', ~), describe('/movies/:id', ~) 2가지 엔드포인트에 대해 DELETE 요청을 테스트한다.

DELETE 요청 시, 전체 배열은 삭제할 수 없기 때문에 404 응답이 예측되고, 하나의 배열은 삭제할 수 있기 때문에 200 응답이 예측된다.

describe('AppController (e2e)', () => {
  describe('/movies', () => {
    it('DELETE 404', () => {
      return request(app.getHttpServer())
        .delete('/movies')
        .expect(404)
    });
  });

  describe('/movies/:id', () => {
    it('DELETE 200', () => {
      return request(app.getHttpServer())
        .delete('/movies/1')
        .expect(200)
    });
  });
});

 

PATCH 요청 테스트

describe('/movies/:id', ~) 엔드포인트에 대해 PATCH 요청을 테스트한다.

PATCH 요청 시, 올바른 값을 전송하면 200 응답이 예측되고, 잘못된 값을 전송하면 400 응답이 예측된다.

describe('AppController (e2e)', () => {
  describe('/movies/:id', () => {
    it('PATCH 200', () => {
      return request(app.getHttpServer())
        .patch('/movies/1')
        .send({ title: 'Update title' })
        .expect(200)
    });
    
    it('PATCH 400', () => {
      return request(app.getHttpServer())
        .patch('/movies/1')
        .send({ name: 'Update name' })
        .expect(400)
    });
  });
});

 

2. 테스트 환경 맞추기

  • test/app.e2e-spec.ts

src/main.ts 파일에서 파이프 옵션을 설정했다.

E2E 테스트는 실제 애플리케이션과 테스트 환경이 동일해야 하기 때문에 main.ts에서 설정한 옵션들을 spec.ts 파일에도 동일하게 설정한다. (app.useGlobalPipes() 부분)

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from "@nestjs/common";
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('AppController (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({
        whitelist: true,
        forbidNonWhitelisted: true,
        transform: true,
      }),
    );

    await app.init();
  });

  describe('/movies', () => {
      ...
  });
});

 

3. 터미널에서 확인하기

터미널에서 아래 명령어를 실행한다.

e2e-spec 파일에 대해 테스트가 진행된다.

npm run test:e2e

NestJS로 API 만들기

https://nomadcoders.co/nestjs-fundamentals


노마드코더 강의를 참고하고 있습니다.

 


spec.ts

Nest.js에서 spec.ts 파일은 유닛 테스트를 작성하는 데 사용하는 파일이다.

특정 모듈, 서비스, 컨트롤러 등의 단위에 대해 테스트를 정의하는 데 사용한다.

 

일반적으로 Nest.js에서 유닛 테스트를 하기 위해 Jest 프레임워크를 사용한다.

package.json 파일을 보면 이미 jest 설정이 되어있는 것을 확인할 수 있다.

 

Jest

  • describe 함수

describe 함수는 테스트 파일이나 테스트 블록을 정의할 때 사용한다.

describe('Math operations', () => {
  // 덧셈에 대한 테스트 그룹
  describe('Addition', () => {
    it('should correctly add two numbers', () => {
      const result = 1 + 2;
      expect(result).toBe(3);
    });
  });

  // 뺄셈에 대한 테스트 그룹
  describe('Subtraction', () => {
    it('should correctly subtract two numbers', () => {
      const result = 5 - 2;
      expect(result).toBe(3);
    });
  });
});

 

  • it 함수

it 함수는 테스트 케이스를 정의할 때 사용한다.

it('테스트 설명', () => {
  // 테스트 로직
});

 

  • expect 함수

expect 함수는 예상한 결과와 실제 결과를 비교하고, 테스트가 성공적으로 통과했는지 여부를 판단한다.

밑에 예시에서 result가 배열의 인스턴스인지 검사한다.

it('should return an array', () => {
  const result = someFunction();
  expect(result).toBeInstanceOf(Array);
});

 


1. service.spec.ts

getAll 메서드 테스트

  1. getAll 메서드로 가지고 온 값이 Array 인스턴스인지 확인한다.
describe('MoviesService', () => {
  describe('getAll', () => {
    it('should return an array', () => {
      const result = service.getAll();
      expect(result).toBeInstanceOf(Array);
    });
  });
});

 

getOne 메서드 테스트

  1. service.create로 영화 데이터를 만든다.
  2. getOne 메서드로 id가 1인 영화 데이터를 가지고 와 id가 1이 맞는지 비교한다.
  3. id가 1이 아니라면(getOne(999)) 404 에러를 발생시킨다.
describe('MoviesService', () => {
  describe('getOne', () => {
    it('should return a movie', () => {
      service.create({
        title: 'Test Movie',
        year: 2024,
        genres: ['Test'],
      });

      const movie = service.getOne(1);
      expect(movie).toBeDefined();
      expect(movie.id).toEqual(1);
    });

    it('should throw 404 error', () => {
      try {
        service.getOne(999);
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
      }
    });
  });
});

 

deleteOne 메서드 테스트

  1. service.create로 영화 데이터를 만든다.
  2. getAll 메서드로 영화의 길이를 변수에 저장한다.
  3. deleteOne 메서드로 id가 1인 영화를 삭제한다.
  4. 그 후 다시 getAll 메서드로 영화의 길이를 변수에 저장해 영화를 삭제하기 전과 후의 값을 비교한다.
  5. 없는 id의 영화(deleteOne(999))를 삭제하면 404 에러를 발생시킨다.
describe('MoviesService', () => {
  describe('deleteOne', () => {
    it('deletes a movie', () => {
      service.create({
        title: 'Test Movie',
        year: 2024,
        genres: ['Test'],
      });

      const beforeDelete = service.getAll().length;
      service.deleteOne(1);

      const afterDelete = service.getAll().length;
      expect(afterDelete).toBeLessThan(beforeDelete);
    });

    it('should return a 404', () => {
      try {
        service.deleteOne(999);
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
      }
    });
  });
});

 

create 메서드 테스트

  1. getAll 메서드로 영화의 길이를 변수에 저장한다.
  2. create 메서드로 새로운 영화를 생성한다.
  3. 그 후 다시 getAll 메서드로 영화의 길이를 변수에 저장해 영화를 생성하기 전과 후의 값을 비교한다.
describe('MoviesService', () => {
  describe('create', () => {
    it('should create a movie', () => {
      const beforeCreate = service.getAll().length;

      service.create({
        title: 'Test Movie',
        year: 2024,
        genres: ['Test'],
      });

      const afterCreate = service.getAll().length;
      expect(afterCreate).toBeGreaterThan(beforeCreate);
    });
  });
});

 

update 메서드 테스트

  1. service.create로 영화 데이터를 만든다.
  2. update 메서드로 id가 1인 영화의 title을 수정한다.
  3. getOne 메서드로 해당 영화를 가져와 영화의 title이 'Updated Test'가 맞는지 비교한다.
  4. 없는 id의 영화(update())를 수정하면 404 에러를 발생시킨다.
describe('MoviesService', () => {
  describe('update', () => {
    it('should throw a NotFoundException', () => {
      service.create({
        title: 'Test Movie',
        year: 2024,
        genres: ['Test'],
      });

      service.update(1, { title: 'Updated Test' });

      const movie = service.getOne(1);
      expect(movie.title).toEqual('Updated Test');

      try {
        service.update(999, {});
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
      }
    });
  });
});

 

2. 터미널에서 확인하기

터미널에서 아래 명령어를 실행한다.

모든 spec 파일에 대해 테스트가 진행된다.

 

:watch 옵션을 사용하면 저장할 때마다 자동으로 테스트를 시작한다.

npm run test
또는
npm run test:watch

 

3. 전체 코드

  • src/movies/movies.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { MoviesService } from './movies.service';
import { NotFoundException } from '@nestjs/common';

describe('MoviesService', () => {
  let service: MoviesService;
  let testMovie;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [MoviesService],
    }).compile();

    service = module.get<MoviesService>(MoviesService);

    // 테스트에 사용될 영화 객체 생성, id는 자동으로 생성됨
    testMovie = {
      title: 'Test Movie',
      year: 2024,
      genres: ['Test'],
    };
  });

  describe('getAll', () => {
    it('should return an array', () => {
      expect(service.getAll()).toBeInstanceOf(Array);
    });
  });

  describe('getOne', () => {
    it('should return a movie', () => {
      service.create(testMovie);

      const movie = service.getOne(1);
      expect(movie).toBeDefined();
      expect(movie.id).toEqual(1);
    });

    it('should throw 404 error', () => {
      try {
        service.getOne(999);
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
      }
    });
  });

  describe('deleteOne', () => {
    it('deletes a movie', () => {
      service.create(testMovie);

      const beforeDelete = service.getAll().length;
      
      service.deleteOne(1);

      const afterDelete = service.getAll().length;
      expect(beforeDelete).toBeLessThan(afterDelete);
    });

    it('should return a 404', () => {
      try {
        service.deleteOne(999);
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
      }
    });
  });

  describe('create', () => {
    it('should create a movie', () => {
      const beforeCreate = service.getAll().length;

      service.create(testMovie);

      const afterCreate = service.getAll().length;
      expect(afterCreate).toBeGreaterThan(beforeCreate);
    });
  });

  describe('update', () => {
    it('should throw a NotFoundException', () => {
      service.create(testMovie);

      service.update(1, { title: 'Updated Test' });

      const movie = service.getOne(1);
      expect(movie.title).toEqual('Updated Test');

      try {
        service.update(999, {});
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
      }
    });
  });
});

NestJS로 API 만들기

https://nomadcoders.co/nestjs-fundamentals


노마드코더 강의를 참고하고 있습니다.

 


문제점

  • src/movies/entities/movies.entity.ts

Entity 파일은 데이터베이스의 테이블과 상호 작용하기 위해 데이터 모델을 정의하고 캡슐화한다.

하지만 정의된 값(id, title, year, geners)이 아닌 다른 값(hacked)을 전송해도 HTTP 요청을 성공적으로 처리한다. (Status 200)

이와 같은 문제를 해결하기 위해서는 DTO를 사용한다.

export class Movie {
  id: number;
  title: string;
  year: number;
  genres: string[];
}

 


DTO란

데이터 전송 객체(DTO, Data Transfer Object)는 클라이언트와 서버 간 데이터 교환을 위한 객체로 사용된다.

  • 클라이언트에서 서버로 데이터를 전송할 때 정확한 데이터 형식을 정의할 수 있어 잘못된 데이터 전송을 방지할 수 있다.
  • 클라이언트가 전송하는 데이터는 종종 서버에서 사용되는 형식과 다를 수 있는데 DTO를 사용해 서버에서 필요한 형식으로 변환할 수 있다.
  • 필요한 데이터만 전송받아 불필요한 정보 노출을 방지해 보안을 강화할 수 있다.

 

1. create DTO 만들기

  • src/movies/dto/create-movies.dto.ts
  1. dto 폴더를 만들고, create-movies.dto.ts 파일을 만든다.
  2. class-validator, class-transformer 패키지를 설치한다. 이 패키지들은 데이터 유효성 검사와 객체 변환을 쉽게 수행할 수 있도록 도와준다. (@IsString(), @IsNumber(), @IsOptional(), @MinLength() 등..)
npm install class-validator class-transformer
import { IsString, IsNumber, IsOptional } from "class-validator";

export class CreateMoviesDto {
  @IsString()
  readonly title: string;

  @IsNumber()
  readonly year: number;

  @IsOptional()
  @IsString({ each: true })
  readonly genres: string[];
}

 

2. update DTO 만들기

  • src/movies/dto/update-movies.dto.ts
  1. dto 폴더에 update-movies.dto.ts 파일을 만든다.
  2. PartialType()은 DTO 클래스의 모든 필드를 선택적으로 만들어주는 역할을 하고, 부분 업데이트를 수행할 때 사용한다. CreateMoviesDto의 모든 필드를 선택적으로 가지게 된다.
  3. 업데이트는 모든 필드의 수정이 필요 없기 때문에 ? 물음표를 사용해 선택 속성임을 나타낸다.
import { IsString, IsNumber } from 'class-validator';
import { PartialType } from '@nestjs/mapped-types';
import { CreateMoviesDto } from './create-movies.dto';

export class UpdateMoviesDto extends PartialType(CreateMoviesDto) {
  @IsString()
  readonly title?: string;

  @IsNumber()
  readonly year?: number;

  @IsString({ each: true })
  readonly genres?: string[];
}

 

3. DTO import 하기

  • src/movies/movies.service.ts
  1. CreateMoviesDto, UpdateMoviesDto를 import 한다.
  2. create 함수에 movieData 데이터 타입을 CreateMoviesDto로 명시한다.
  3. update 함수에 updateData 데이터 타입을 UpdateMoviesDto로 명시한다.
import { CreateMoviesDto } from './dto/create-movies.dto';
import { UpdateMoviesDto } from './dto/update-movies.dto';
...

@Injectable()
export class MoviesService {
  private movies: Movie[] = [];

  create(movieData: CreateMoviesDto) {
    this.movies.push({
      id: this.movies.length + 1,
      ...movieData,
    });
  }

  update(id: number, updateData: UpdateMoviesDto) {
    const movie = this.getOne(id);

    this.deleteOne(id);
    this.movies.push({ ...movie, ...updateData });
  }
  
  ...
}

 

  • src/movies/movies.controller.ts
  1. CreateMoviesDto, UpdateMoviesDto를 import 한다.
  2. POST 요청을 보낼 때 데이터 타입을 CreateMoviesDto로 명시한다.
  3. PATCH 요청을 보낼 때 데이터 타입을 UpdateMoviesDto로 명시한다.
import { CreateMoviesDto } from './dto/create-movies.dto';
import { UpdateMoviesDto } from './dto/update-movies.dto';
...

@Controller('movies') // Entry Point(URL)
export class MoviesController {
  constructor(private readonly moviesService: MoviesService) {}

  @Post()
  create(@Body() movieData: CreateMoviesDto) {
    return this.moviesService.create(movieData);
  }

  @Patch('/:id')
  patch(@Param('id') movieId: number, @Body() updateData: UpdateMoviesDto) {
    return this.moviesService.update(movieId, updateData);
  }
  
  ...
}

 

4. 결과 확인하기

DTO에 정의되지 않은 속성 또는 잘못된 타입을 POST 하면 에러 메세지가 나타난다.

 

DTO에 정의되지 않은 속성 또는 잘못된 타입을 PATCH 하면 에러 메세지가 나타난다.

+ Recent posts