왕초보를 위한 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 하면 에러 메세지가 나타난다.

NestJS로 API 만들기

https://nomadcoders.co/nestjs-fundamentals


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

 


결과 화면

 


1. 컨트롤러(controller) 만들기

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

영화 API를 만들 것이기 때문에 컨트롤러의 이름은 movies로 한다. (이름은 API 성격에 맞게 자유롭게 작성하면 된다.)

app.module.ts 파일에서 MoviesController가 자동으로 import 된 것을 확인할 수 있다.

nest generate controller
또는
nest g co

 

참고

터미널에서 nest 명령어를 실행하면 nest에서 사용할 수 있는 명령어 리스트를 확인할 수 있다.

 

  • src/movies/movies.controller.ts

기본 형태

import { Controller } from '@nestjs/common';

@Controller('movies')
export class MoviesController {}

 

 

      1. @Controller('movies')

컨트롤러를 정의하고, 해당 컨트롤러의 기본 URL 경로를 /movies로 지정한다.

 

      2. @Get() getAll()

HTTP GET 메서드에 대한 핸들러이다.

/movies 경로의 GET 요청에 대한 응답을 반환한다.

 

      3. @Get('/:id') getOne()

동적인 URL 파라미터인 :id를 사용해 특정 영화에 대한 정보를 가지고 오는 핸들러이다.

/movies/:id 경로의 GET 요청에 대한 응답을 반환한다.

 

      4. @Post() create()

HTTP POST 메서드에 대한 핸들러이다.

/movies 경로의 POST 요청에 대한 응답을 반환한다.

 

      5. @Delete('/:id') remove()

동적인 URL 파라미터인 :id를 사용해 특정 영화에 대한 정보를 삭제하는 핸들러이다.

/movies/:id 경로의 DELETE 요청에 대한 응답을 반환한다.

 

      6. @Parch('/:id') patch()

동적인 URL 파라미터인 :id를 사용해 특정 영화에 대한 정보를 업데이트하는 핸들러이다.

/movies/:id 경로의 PATCH 요청에 대한 응답을 반환한다.

import { Controller, Get, Post, Delete, Patch, Param, Body } from '@nestjs/common';

@Controller('movies') // Entry Point(URL)
export class MoviesController {
  @Get()
  getAll() {
    return 'This will return all movies.';
  }

  @Get('/:id')
  getOne(@Param('id') movieId: string) {
    return `This will return one movie with the id: ${movieId}`;
  }

  @Post()
  create() {
    return 'This will create a movie.';
  }

  @Delete('/:id')
  remove(@Param('id') movieId: string) {
    return `This will delete a movie with the id: ${movieId}`;
  }

  @Patch('/:id')
  patch(@Param('id') movieId: string) {
    return `This will patch a movie with the id: ${movieId}`;
  }
}

 

2. 서비스(service) 만들기

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

서비스의 이름은 컨트롤러의 이름과 동일하게 movies로 한다. (네이밍 규칙에 따라 이름을 동일하게 작성한다.)

app.module.ts 파일에 MoviesService가 자동으로 import 된 것을 확인할 수 있다.

nest generate services
또는
nest g s

 

  • src/movies/movies.service.ts

기본 형태

import { Injectable } from '@nestjs/common';

@Injectable()
export class MoviesService {}

 

원래는 데이터베이스가 들어오는 영역이지만, 데이터베이스를 만들지 않을 것이기 때문에 가짜 데이터베이스를 위한 비즈니스 로직을 만든다.

 

      1. private movies

가짜 데이터베이스를 만들기 위해 빈 배열은 만든다.

 

      2. getAll()

모든 영화 정보를 반환하는 메서드이다.

movies 배열 전체를 반환한다.

 

      3. getOne()

특정 id에 해당하는 영화 정보를 반환하는 메서드이다.

movies 배열에서 해당 id의 영화를 찾아 반환하고, id가 존재하지 않는다면 NotFoundException()을 반환한다.

 

      4. deleteOne()

특정 id에 해당하는 영화 정보를 삭제하는 메서드이다.

getOne() 메서드를 사용해 해당 id의 영화가 존재하는지 확인한 후, 영화를 movies 배열에서 삭제한다.

 

      5. create()

새로운 영화 정보를 추가하는 메서드이다.

movies 배열에 새로운 영화를 추가하고, id는 현재 배열 길이 +1로 설정한다.

 

      6. update()

특정 id에 해당하는 영화 정보를 업데이트 하는 메서드이다.

getOne() 메서드를 사용해 해당 id의 영화를 가져와 deleteOne() 메서드를 사용해 삭제한 후, 업데이트된 영화를 movies 배열에 추가한다.

import { Injectable, NotFoundException } from "@nestjs/common";
import { Movie } from './entities/movies.entity';

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

  getAll(): Movie[] {
    return this.movies;
  }

  getOne(id: string): Movie {
    const movie = this.movies.find((movie) => movie.id === +id);

    if (!movie) {
      throw new NotFoundException(`Movie with ID: ${id} not found.`);
    }

    return movie;
  }

  deleteOne(id: string) {
    this.getOne(id);
    this.movies = this.movies.filter((movie) => movie.id !== +id);
  }

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

  update(id: string, updateData) {
    const movie = this.getOne(id);
    
    this.deleteOne(id);
    this.movies.push({ ...movie, ...updateData });
  }
}

 

2-1. 엔티티(entity) 만들기

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

Nest.js는 타입스크립트 기반이기 때문에 데이터 타입을 정의한다.

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

 

3. 컨트롤러 수정하기

  • src/movies/movies.controller.ts

2번에서 컨트롤러에 대한 개념을 잡기 위해 문자열을 return을 했지만, HTTP 요청을 처리할 수 있도록 서비스에 만든 비즈니스 로직을 return 한다.

 

    1. constructor(private ~) {}

의존성 주입을 사용해 MoviesService를 주입한다.

 

    2. @Get() getAll()

movieService의 getAll() 메서드를 호출해 모든 영화에 대한 정보를 반환한다.

 

    3. @Get('/:id') getOne()

@Param 데코레이터를 사용해 URL 파라미터인 id를 추출하고, moviesService의 getOne() 메서드를 호출해 id에 해당하는 영화 정보를 반환한다.

 

    4. @Post() create()

@Body 데코레이터를 사용해 요청 본문에서 전달된 데이터를 moviesSevice의 create() 메서드를 호출해 새로운 영화 정보를 추가한다.

 

    5. @Delete('/:id') remove()

@Param 데코레이터를 사용해 URL 파라미터인 id를 추출하고, moviesService의 deleteOne() 메서드를 호출해 id에 해당하는 영화 정보를 삭제한다.

 

    6. @Parch('/:id') patch()

@Param 데코레이터를 사용해 URL 파라미터인 id를 추출하고, @Body 데코레이터를 사용해 요청 본문에서 데이터를 전달한다.

moviesService의 update() 메서드를 호출해 id에 해당하는 영화 정보를 업데이트한다.

import { Controller, Get, Post, Delete, Patch, Param, Query, Body } from '@nestjs/common';
import { MoviesService } from './movies.service';
import { Movie } from './entities/movies.entity';

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

  @Get()
  getAll(): Movie[] {
    return this.moviesService.getAll();
  }

  @Get('/:id')
  getOne(@Param('id') movieId: string) {
    return this.moviesService.getOne(movieId);
  }

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

  @Delete('/:id')
  remove(@Param('id') movieId: string) {
    return this.moviesService.deleteOne(movieId);
  }

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

 

데코레이터란,

@로 시작하는 함수를 데코레이터 라고 부르며, 클래스, 메서드, 프로터디 또는 매개변수에 부가적인 메타데이터를 제공하는 역할을 한다.

 

  • @Module

모듈을 정의하는데 사용되며, 모듈은 애플리케이션을 구성하고 기능을 캡슐화한다.

 

  • @Controller

컨트롤러를 정의하는데 사용되며, HTTP 요청을 처리하는 역할을 한다.

 

  • @Injectable

서비스를 정의하는데 사용되며, 의존성 주입을 가능하게 한다.

 

  • @Body

POST, PUT, PATCH 등의 HTTP 메서드에서 주로 사용한다.

json 또는 대량의 데이터 전송에 적합하다.

 

  • @Query 

GET HTTP 메서드에서 주로 사용한다.

데이터를 필터링하거나 정렬하는 기분값을 전송에 적합하다.

ex) /movies?year=2024

 

  • @Param

동적인 경로를 가지는 라우팅에서 사용한다.

특정 데이터를 식별할 때 적합하다.

ex) /movies/2024

 

4. 결과 확인하기

Postman 또는 Insomnia로 HTTP에 대한 요청을 확인할 수 있다.

 

  • GET 

빈 배열을 확인할 수 있다.

 

  • POST

Status 201을 확인할 수 있다.

 

  • GET 

POST로 전송한 데이터를 확인할 수 있다.

 

  • DELETE

Status 200을 확인할 수 있다.

NestJS로 API 만들기

https://nomadcoders.co/nestjs-fundamentals


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

 


결과 화면

 


Nest.js 설치하기

NestJs 설치에 대한 설명은 아래 링크를 참고한다.

https://jae-study.tistory.com/78

 

[Nest] #1 NestJs 설치하기

NestJS(Nest)란? https://docs.nestjs.com/ NestJs는 효율적이고, 확장 가능한 Node.js 서버 애플리케이션을 구축하기 위한 프레임워크이다. 프로그레시브 Javascript를 사용하고, Typescript로 구축되어 Typescript를

jae-study.tistory.com

 


Nest.js

네스트(Nest.js)는 Node.js를 위한 서버 사이드 애플리케이션을 개발하기 위한 프레임워크이다.

애플리케이션을 구성하는데 모듈, 컨트롤러, 서비스의 개념을 사용한다.

 

모듈 (Module)

@Module 데코레이터를 사용해 모듈을 정의하고, 해당 모듈은 컨트롤러, 서비스, 미들웨어 및 다른 컴포넌트들을 그룹화하는 방법을 제공한다.

Nest.js 애플리케이션을 여러 모듈로 구성되며, 의존성 주입을 통해 모듈 간에 서비스 및 기능을 공유하고, 코드의 재사용성을 높인다.

 

  • src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

 

컨트롤러 (Controller)

@Controller 테코레이터를 사용해 컨트롤러를 정의하고, 해당 컨트롤러에는 특정 엔드포인트(경로)에 대한 라우팅 정보가 포함된다.

또한 각 컨트롤러는 HTTP 메서드(GET, POST 등)에 메핑 되며, HTTP 요청을 처리하고 응답하는 역할을 수행한다.

 

  • src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Get('/hi')
  sayHello(): string {
    return this.appService.getHi();
  }
}

 

서비스 (Service)

@Injectable() 데코레이션을 사용해 서비스를 정의하고, 해당 서비스에는 필요한 비즈니스 로직이나 데이터를 구현한다.

컨트롤러에서 호출되어 컨트롤러에게 데이터가 기능을 제공하는 역할을 한다.

 

  • src/app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello Nest!';
  }

  getHi(): string {
    return 'Hi Nest!';
  }
}

 

주의

데코레이터는 함수나 클래스랑 붙어있어야 한다. (스페이지 또는 엔터를 사용하면 안 된다.)

결과 화면

 


수정사항

https://jae-study.tistory.com/85

기존에는 react-js-pagination 패키지를 사용해 페이지네이션을 구현했다.

하지만 페이지네이션을 프론트에서 만들었을 경우, 서버와 데이터베이스의 부하가 발생할 수 있기 때문에 백앤드의 작업이 필요하다.

백앤드 없이 프론트에서 페이지네이션을 구현하고 싶다면 위의 링크를 참고한다.

 

https://jae-study.tistory.com/138

NestJs로 페이지네이션을 만들었다.

페이지네이션을 백앤드에서 만들어야 하는 이유에 대한 설명은 위의 링크를 참고한다.

 


1. boardReducer.tsx

  • src/store/boardReducer.tsx
  1. page 파라미터를 받는다. 타입스크립트이기 때문에 타입을 정의한다.
  2. axios.get을 사용해 page에 해당하는 데이터를 요청한다. URL은 쿼리 파라미터가 붙은 key=value 형태이다.
export const getBoardList = createAsyncThunk(
  'GET_BOARD_LIST',
  async (page: number) => {
    try {
      const response = await axios.get(`http://localhost:3001/board?page=${page}`)
      return response.data
    } catch (error) {
      console.log(error)
    }
  }
)

...

 

2. 페이지네이션에 해당하는 데이터 가져오기

  • src/pages/board/list/index.tsx
  1. dispatch를 사용해 page에 해당하는 게시판 데이터를 가져온다.
  2. axios를 통해 받아온 데이터가 객채(object) 형태이기 때문에 (배열처럼 보이지만 typeof로 확인하면 object이다.) [board, setBoard] state 변수를 만들고, 전개 연산자를 사용해 배열 형태로 바꾼다.
  3. axios를 통해 받아온 page 데이터가 문자(string) 형태이기 때문에 [page, setPage] state 변수를 만들고, Number로 타입을 변환한다.
  4. 페이지네이션 버튼에 대한 공통 함수(handlePagination)를 만든다.
import { useEffect, useState } from 'react'
import { useAppDispatch, useAppSelector } from '../../../hooks/useApp'
...

const BoardList = () => {
  // ** Hooks
  const dispatch = useAppDispatch()

  // ** Redux States
  const boardList = useAppSelector(state => state.boardReducer)

  // ** States
  const [board, setBoard] = useState<boardType[]>([])
  const [page, setPage] = useState<number>(1)

  // 페이지네이션
  function handlePagination(newPage: number) {
    if(newPage >= 1 && newPage <= boardList.meta.last_page) {
      dispatch(getBoardList(newPage))
    }
  }

  // 페이지가 로딩되면 게시판 리스트 가져옴
  useEffect(() => {
    dispatch(getBoardList(page))
  }, [])

  // 페이지가 로딩된 후 state 변수에 boardList 데이터 저장함
  useEffect(() => {
    if (boardList && boardList.data) {
      setBoard([...boardList.data])
      setPage(Number(boardList.meta.page))
    }
  }, [boardList])

  return (
    ...
  )
}

export default BoardList

 

3. 마크업 하기

  • src/pages/board/list/index.tsx.
  1. 삼항연산자를 사용해 데이터가 있고, 없을 때 마크업을 다르게 표현한다.
  2. map 메서드를 사용해 게시판 데이터를 보여준다.
  3. 처음으로, 이전, 다음, 마지막으로 버튼 기능을 구현한다.
const BoardList = () => {
  ...

  return (
    <div className="board-list">
      <Title children="Board list"/>

      <h4>Total post : {boardList.meta?.total}</h4>

      <table>
        <colgroup>
          <col width="15%"/>
          <col width="65%"/>
          <col width="20%"/>
        </colgroup>

        <thead>
          <tr>
            <th>No</th>
            <th>Title</th>
            <th>Date</th>
          </tr>
        </thead>

        <tbody>
          {
            board.length === 0 ? (
              <tr>
                <td colSpan={3}>데이터가 없습니다.</td>
              </tr>
            ) : (
              board.map((board, index) => {
                return (
                  <tr key={index}>
                    <td>{board.id}</td>
                    <td className="title">
                      <Link to={`/board/${board.id}`}>{board.title}</Link>
                    </td>
                    <td>{dayjs(board.created_at).format('YYYY.MM.DD')}</td>
                  </tr>
                )
              })
            )
          }
        </tbody>
      </table>

      {
        board.length === 0 ? (
          <></>
        ) : (
          <div className="pagination">
            {
              boardList.meta.last_page === 1 ? (
                <>
                  <button disabled>&#60;</button>
                  <p>{page}</p>
                  <button disabled>&#62;</button>
                </>
              ) : (
                <>
                  <button onClick={() => handlePagination(1)}>&#60;&#60;</button>
                  <button onClick={() => handlePagination(page - 1)}>&#60;</button>
                  <p>{page} / {boardList.meta.last_page}</p>
                  <button onClick={() => handlePagination(page + 1)}>&#62;</button>
                  <button onClick={() => handlePagination(boardList.meta.last_page)}>&#62;&#62;</button>
                </>
              )
            }
          </div>
        )
      }

      <Link to="/board/create">
        <Button children="Write" variant="primary"/>
      </Link>
    </div>
  )
}

export default BoardList

결과 화면

 


페이지네이션(Pagination)이란

페이지네이션은 대량의 데이터를 관리하고 효율적으로 표시하기 위한 기술로 사용자가 웹 페이지 상에서 여러 페이지로 나뉜 데이터를 탐색할 수 있도록 하는 방법이다.

 

페이지네이션을 백앤드에서 만들어야 하는 이유

  1. 서버 리소스 효율성 증가 : 페이지네이션은 클라이언트가 필요로 하는 양의 데이터만 전송하여 서버 리소스를 효율적으로 활용한다.
  2. 데이터베이스 부하 감소 : 페이지네이션을 통해 서버는 필요한 데이터만 데이터베이스에서 가져와 부하를 감소시킨다.
  3. 클라이언트 성능 향상 : 서버와 데이터베이스에서 필요한 데이터만 요청하여 클라이언트의 로딩 시간을 최소화해 사용자의 경험을 향상한다.
  4. 검색 엔진 최적화(SEO) : 페이지네이션은 검색 엔진이 페이지 간 이동을 파악하고 데이터를 효과적으로 색인화할 수 있도록 도와준다.

간단히 말해, 데이터의 양이 상대적으로 적은 경우(예: 100개), 페이지네이션의 유무에 따른 성능 차이를 느끼지 못할 수 있지만, 데이터의 양이 많아지면(예: 10만개 이상) 클라이언트에서 서버로 데이터를 요청하고 수신하는 과정에서 서버 및 데이터베이스 부하가 증가하게 된다. 

따라서 백엔드에서 페이지네이션을 구현해 데이터를 효율적으로 관리하고 사용자의 경험을 최적화할 수 있다.

 


1. board.service.ts

service 파일에는 비즈니스 로직이나 데이터 조작 등을 수행하는 메서드를 구현한다.

 

  • src/board/board.service.ts
  1. paginate 메서드를 만든다.
  2. page 매개변수로 페이지 번호를 지정하고, 기본값을 1로 설정한다.
  3. take 변수는 한 페이지에 표시될 아이템 수를 나타낸다.
  4. findAndCount 메서드는 데이터베이스에서 페이징 된 데이터를 검색하고, 해당 데이터와 전체 아이템 수를 배열로 반환한다.
  5. 페이지와 관련된 meta 정보를 return 한다.
@Injectable()
export class BoardService {
  constructor(private readonly boardRepository: BoardRepository) {}

  async paginate(page = 1): Promise<any> {
    const take = 5;

    const [board, total] = await this.boardRepository.findAndCount({
      take,
      skip: (page - 1) * take,
    });

    return {
      data: board,
      meta: { take, total, page, last_page: Math.ceil(total / take) },
    };
  }
  
  ...
}

 

2. board.controller.ts

  • src/board/board.controller.ts
  1. @Get()으로 HTTP GET 요청을 처리하는 핸들러임을 나타낸다.
  2. @Query('page')를 통해 쿼리 파라미터를 받아와 page 변수에 할당하고, 기본값을 1로 설정한다.
  3. boardService 클래스에 정의된 paginate 메서드를 호출해서 return 한다.
@Controller('board')
export class BoardController {
  constructor(private readonly boardService: BoardService) {}

  @Get()
  async all(@Query('page') page = 1): Promise<any[]> {
    return await this.boardService.paginate(page);
  }
  
  ...
}

 

 

포스트맨(postman)을 통해서 결과를 확인할 수 있다.

url 뒤에 page=1 쿼리 파라미터가 생겼다.

 

쿼리 파라미터(Query Parameter)는 URL 끝에 ?로 시작하며, 그 뒤에 'key=value' 형태로 데이터를 전달하는 방식이다.

목표

  • 투두리스트 상세페이지 만들기
  • useRoute()로 파라미터 가져오기
  • useFetch()로 서버 데이터 가져오기

 


결과 화면

 


버전

node v.20.8.1

nuxt v.3.8.1

vue v.3.3.8

 

1. 상세페이지 만들기

  • pages/[id].vue
  1. pages 디렉터리 내에 [id].vue 파일을 만든다. 별도의 설정 없이 자동으로 동적 라우팅이 생성된다.

투두리스트 목록 페이지 = index.vue

투두리스트 상세 페이지 = [id].vue

 

Nuxt3의 핵심 기능 중 하나는 파일 시스템 라우터이다.

pages/ 디렉터리 내의 모든 Vue 파일은 URL을 생성한다.

 

https://nuxt.com/docs/getting-started/routing

라우팅에 대한 자세한 설명은 공식문서를 참고한다.

 

주의

Vue나 Nuxt2에서는 동적 라우팅 페이지가 _id.vue(언더바 _) 였다. Nuxt3부터는 [id].vue(대괄호 []) 이다.

 

2. 파라미터 가져오기

  • pages/[id].vue
  1. route 변수를 만든다.
  2. ${route.params.id}로 url의 파라미터 값을 가져온다.
  3. useFetch()로 해당 파라미터에 대한 서버 데이터를 가져온다.

 

https://nuxt.com/docs/api/composables/use-route

useRoute()에 대한 자세한 설명은 공식 문서를 확인한다.

<script setup lang="ts">
const route = useRoute()
console.log(route)

const { data } = await useFetch(`http://localhost:3001/todo/${route.params.id}`)
console.log(data)
</script>

 

3. 마크업

  • pages/[id].vue
  1. 테일윈드를 사용해 CSS를 작성했다.
  2. { data }의 id와 todo 값을 화면에 보여준다.
<script setup lang="ts">
const route = useRoute()

const { data } = await useFetch(`http://localhost:3001/todo/${route.params.id}`)
</script>

<template>
  <section>
    <h1 class="mt-6 mb-12 md:mt-10 text-5xl font-bold text-center">To Do Detail</h1>

    <div class="p-4 md:p-6 md:pb-12 border rounded">
      <p class="block mb-6 text-xl font-bold text-right">No.{{ data.id }}</p>
      <p class="flex gap-2 text-base md:text-lg">{{ data.todo }}</p>
    </div>

    <NuxtLink to="/"  class="block w-full md:w-1/3 m-auto mt-12 px-3 py-4 bg-teal-950 hover:bg-teal-500 transition rounded text-center">
      Go to To Do List
    </NuxtLink >
  </section>
</template>

목표

  • useFetch()로 서버 데이터 가져오기
  • watch()를 사용해 서버 데이터 변경 감지하기
  • $fetch 사용해 HTTP 메서드 사용하기 (GET, POST, DELETE)
  • 서버 데이터를 등록순/최신순 정렬하기

 


결과 화면

 


버전

node v.20.8.1

nuxt v.3.8.1

vue v.3.3.8

 

1. Nest.js + PSQL (백앤드 + 데이터베이스)

https://jae-study.tistory.com/80

Nest.js와 Psql을 사용해서 백앤드와 데이터베이스를 만들었다.

 

직접 백앤드와 데이터베이스를 만들어도 되고,

JSONPlaceholder 사이트를 통해 가짜 데이터를 받아와도 된다.

 

2. toDo 가져오기

  • pages/index.vue

Nuxt3에서는 axios 대신 useFetch()를 사용해 데이터 패칭을 한다.

useFetch()는 데이터를 비동기적으로 불러올 수 있고, 기존 axios 코드보다 로직을 간소화할 수 있다.

 

https://jae-study.tistory.com/127

useFetch()에 대한 자세한 설명은 위의 링크를 참고한다.

 

  1. Nuxt3는 타입스크립트 기반이기 때문에 useFetch()로 받아오는 데이터의 타입을 정의한다. (IToDo[])
  2. 서버에서 받은 데이터를 가공하기 위해 toDoData 변수를 만든다. useFetch()로 받아오는 데이터의 타입은 기본적으로 객체(object)이다. 이것을 배열로 만들어 정렬(sort 메서드)을 하고 싶어 toDoData 변수를 만들었다.
  3. toDo를 추가하고 삭제할 때마다 실시간으로 렌더링 하기 위해 watch 함수를 사용한다.
<script setup lang="ts">
interface IToDo {
  id: number
  todo: string
  created_at: Date
}

// axios.get 기능 (toDo 가져오기)
const { data, refresh } = await useFetch<IToDo[]>('http://localhost:3001/todo')
const toDoData = ref(data.value ? [...data.value] : [])

watch(() => data.value, () => {
  toDoData.value = isActive.value ? [...data.value] : [...data.value].reverse()
})

...
</script>

 

3. toDo 추가하기

  • pages/index.vue
  1. newToDo 변수를 만들어 빈 값이면 alert() 창이 나오게 하고, 값이 있으면 POST 메서드를 실행한다.
  2. POST가 완료되면 newToDo를 빈 값으로 만들고, 실시간 값 변화를 감지하기 위해 refresh()를 실행한다.
<script setup lang="ts">
const newToDo = ref('')

// axios.post 기능 (toDo 추가하기)
async function addToDo() {
  if(newToDo.value === '') {
    alert('할 일을 입력해 주세요.')
  } else {
    await $fetch('http://localhost:3001/todo', {
      method: 'POST',
      body: { todo: newToDo.value }
    })

    newToDo.value = ''
    await refresh()
  }
}

...
</script>

 

4. toDo 삭제하기

  • pages/index.vue
  1. 리스트 1개만 삭제하고 싶으면 해당하는 id 값을 매개변수로 받아 DELETE 메서드를 실행한다.
  2. 리스트 전체를 삭제하고 싶으면 api 전체 리스트에서 DELETE 메서드를 실행한다.
  3. DELETE가 완료되면 실시간 값 변화를 감지하기 위해 refresh()를 실행한다.
<script setup lang="ts">
// axios.delete 기능 (toDo 삭제하기)
async function deleteToDo(id: number) {
  await $fetch(`http://localhost:3001/todo/${id}`, {
    method: 'DELETE'
  })

  await refresh()
}

// axios.delete 기능 (toDo 전체 삭제하기)
async function clearToDo() {
  if(window.confirm('리스트를 모두 삭제하시겠습니까?')) {
    await $fetch(`http://localhost:3001/todo`, {
      method: 'DELETE'
    })

    await refresh()
  }
}

...
</script>

 

5. toDo 정렬하기

  • pages/index.vue
  1. useFetch()로 받은 데이터를 배열로 만들어 sort() 메서드를 사용해 등록순/최신순으로 정렬한다.
<script setup lang="ts">
const isActive = ref(true)

// 등록순, 최신순 정렬하기
function sortToDo(compareFn: (a: IToDo, b: IToDo) => number) {
  toDoData.value = [...data.value].sort(compareFn)
  isActive.value = !isActive.value
}

...
</script>

<template>
  <section class="w-full max-w-screen-lg min-h-screen m-auto">
    ...

    <div v-if="toDoData.length > 0">
      <div class="flex gap-2 mb-6">
        <button
          :class="{ 'underline underline-offset-4' : isActive }"
          @click="() => sortToDo((a, b) => a.id - b.id)">
          등록순
        </button>
        <i>|</i>
        <button
          :class="{ 'underline underline-offset-4' : !isActive }"
          @click="() => sortToDo((a, b) => b.id - a.id)">
          최신순
        </button>
      </div>
    </div>
    
    ...
  </section>
</template>

 

6. 마크업

  • pages/index.vue
  1. 테일윈드를 사용해 CSS를 작성했다.
  2. 각각의 버튼에 맞는 @click 이벤트를 작성한다.
<template>
  <section class="w-full max-w-screen-lg min-h-screen m-auto">
    <h1 class="mt-6 mb-12 md:mt-10 text-5xl font-bold text-center">To Do List</h1>

    <form
      class="mb-6"
      @submit.prevent="addToDo()">
      <label
        for="toDo"
        class="block mb-3 text-xl font-bold">
        New To Do
      </label>

      <div class="flex gap-3 md:gap-5">
        <input
          v-model="newToDo"
          type="text"
          id="toDo"
          class="w-3/4 px-3 py-4 rounded outline-none text-black"
          placeholder="할 일을 입력해 주세요."/>
        <button class="w-1/4 bg-teal-600 hover:bg-teal-500 transition rounded">추가하기</button>
      </div>
    </form>

    <div v-if="toDoData.length > 0">
      <div class="flex gap-2 mb-6">
        <button
          :class="{ 'underline underline-offset-4' : isActive }"
          @click="() => sortToDo((a, b) => a.id - b.id)">
          등록순
        </button>
        <i>|</i>
        <button
          :class="{ 'underline underline-offset-4' : !isActive }"
          @click="() => sortToDo((a, b) => b.id - a.id)">
          최신순
        </button>
      </div>

      <ul>
        <li
            v-for="(data, key) in toDoData"
            :key="key"
            class="flex justify-between items-center gap-3 mb-6 px-4 py-3 md:px-5 border rounded">
          <p class="flex gap-2 text-base md:text-lg">
            <span>{{ key + 1 }}.</span> {{ data.todo }}
          </p>

          <button
            class="min-w-fit px-5 py-2 bg-teal-600 hover:bg-teal-500 transition rounded"
            @click="deleteToDo(data.id)">
            삭제
          </button>
        </li>
      </ul>

      <button
        class="block w-full md:w-1/3 m-auto mt-12 px-3 py-4 bg-teal-950 hover:bg-teal-500 transition rounded"
        @click="clearToDo()">
        전체 삭제하기
      </button>
    </div>
  </section>
</template>

 


상세 코드는 깃허브 페이지를 참고한다.

 

https://github.com/heejae0811/nuxt3-todo/tree/axios

 

GitHub - heejae0811/nuxt3-todo: main: Pinia To Do List / axios: Nest.js + PSQL To Do List (useFetch)

main: Pinia To Do List / axios: Nest.js + PSQL To Do List (useFetch) - GitHub - heejae0811/nuxt3-todo: main: Pinia To Do List / axios: Nest.js + PSQL To Do List (useFetch)

github.com

 

https://github.com/heejae0811/todo-backend

 

GitHub - heejae0811/todo-backend: Nest.js + PSQL ToDo 백앤드

Nest.js + PSQL ToDo 백앤드. Contribute to heejae0811/todo-backend development by creating an account on GitHub.

github.com

+ Recent posts