sogno

전체 글 72

카테고리 설명
  • 스터디용으로 파놓은 repository에 push를 하려고 했는데... ! [rejected] main -> main (fetch first) error: failed to push some refs to 'https://github.com/sognociel/express_study.git' hint: Updates were rejected because the remote contains work that you do hint: not have locally. This is usually caused by another repository pushing hint: to the same ref. You may want to first integrate the remote changes hint: (e.g..

  • 이번 프로젝트를 진행하면서 multer-s3를 사용해 보게 되었는데, 팀장님이 작성한 코드만 보며 조각조각땃땃따를 하다 보니 도대체 얘는 어떻게 진행되고 있는 것인가... 도저히 이해를 못 하겠던 것이었다... 계속되는 머리털뜯기... 그걸 보는 팀장님... (진짜 답답하셨을 듯 이해를 1도 못하고 있어서...) 그도 그럴게 Node.js로 api 한번 만들어보질 않았는데 바로 Nest.js typeorm 이렇게 넘어가서 그런 걸 지도 미들웨어에 대한 개념도 부족해서 (저번에 팀장님이 설명해 주셨는데 또 잊었음!! 이게 다 블로그를 안 써서 그런 것 같음!!!) 이번에 Express로 미들웨어 함수를 만들어 보라고 하시길래 이번엔 절대 잊지 않겠다... 는 마음으로 만드는 과정을 샥샥 적어볼까 한다. 공..

  • 회사에서 Docker를 통해 MySQL을 띄워놓았다. 회사 노트북으로 돌아가고 있는 DB에 접근하려면 설정해줘야 할 것들이 몇 가지 있어서 나중에 또 이런 일이 있을 때 참고하려고 정리해 두기로 했다. 알아보기 쉽게 외부/내부로(어감이 조금 이상하긴 하지만...) 구분하겠다. 내부 노트북 : Docker를 통해 MySQL DB를 띄워둠. 이 노트북이 사용하는 공유기에 포트포워딩 설정을 해둬야 함 외부 노트북 : 내부 노트북의 외부 ip주소만 있으면 됨 1. Docker 컨테이너 설정 기존에 도커를 실행했을 때는 포트를 로컬의 3306번으로 지정을 해두었는데, 이렇게 도커 컨테이너를 실행하면 외부접속이 아예 불가능하므로 처음부터 외부 접속 할 포트를 설정하거나, 아니면 기존에 로컬에서 실행되는 도커 컨테이..

  • 스터디는 맥, 집에서 따로 공부할 때는 윈도우를 쓰는데, git repository에 그날 한 것들을 올려놓고 받아오는 식으로 진행을 하려고 했다. 출퇴근시간에 노트북을 들고 다니기가 번거로웠고... 목이랑 어깨가 안 좋아서 매번 들고 다닐 때마다 한의원 VVIP가 되었기 때문에^^7 하지만 처음부터 시뻘건 경고창을 맞닥뜨리게 되는데... Prettier와 ESLint를 적용하고 있었는데 Replace의 경우는 Prettier의 괄호 안 공간과 싱글쿼테이션 때문이라 설정에서 바꿔주었더니 바로 해결이 되었다. 그러나... Delete `␍`의 경우는 계속해서 남아있었다. 알아보니 ESLint에서 발생하는 오류라 실행은 정상적으로 된다고 한다. 하지만 이런 오류표시 정말 엄청 너무나도 심각하게 신경 쓰이고 ..

  • class-validator는 기본적으로 @IsString, @IsNumber 등 기본적인 데코레이터들을 제공해 주지만... 이것만으로 검증을 할 수 없을 때 커스텀이 필요한 경우가 있다! 나는 입력받은 키와 몸무게가 소수점이 없을 때는 3자리 이하로, 그리고 만약 소수점 자리가 있다면 소수점 이하 2자리까지만 입력된 값을 통과시키고 싶었는데, 이런 데코레이터는 없기 때문에... 직접 커스텀을 해야 했다. 다행히... custom class validator에 대해 정리해 주신 분이 계셔서😭 정말 많은 도움을 받았다. https://lee-eonsang.com/blog/custom-class-validator 일단 최종적으로 구현한 내용은 아래와 같다. import { registerDecorator, ..

  • 오늘은 node.js와 express에 대해서 쪼끔... 알아보기로 한다. 그냥 node.js 홈페이지에 가서 설치를 해도 되지만, 이전에 프로젝트를 수행하다가 노드 버전 때문에 계속 나 혼자 충돌? 이 일어났던 적이 있어 nvm으로 설치를 하는 게 좋은 것 같다. 알고 보니 이전에 개발 공부를 하시던 분들은 그때 최신버전이 16이었고, 나는 새싹에서 처음 다운로드하였어서 18 버전이라 그랬던 것... https://github.com/coreybutler/nvm-windows/releases nvm-setup.zip 이었던가..? 설치를 하고 실행하면 됐던 것 같다. $ nvm install v16 { // 응답에 Hello World 라는 문자를 담아서 보내겠다 // send 대신 좀 더 명시적으로 ..

  • 뭔가 이것저것 짬뽕인 느낌이지만 길이도 그렇게 길지 않을 테고.. 복습 겸 한꺼번에 모아두는 게 좋을 것 같았다. 나는 차트 데이터에 관해서 코드를 짜보았는데, class-validator는 로그인 등 다양한 곳에서 사용하는 것 같더라..! 요약 : class-validator로 DTO타입 체크하는 여정기. class-validator class-validator란 joi의 Typescript 버전으로, 데코레이터를 이용하여 편리하게 검증할 수 있는 라이브러리이다. 서버로 들어오는 Json 데이터의 검증을 할 때 유용하게 사용할 수 있다. joi란 무엇이냐? 이것 또한 validation 라이브러리라고 보면 된다. DTO (Data Transfer Object) DTO란 말 그대로 해석하면 '데이터 전송..

  • ★테스트를 작성하는 이유★ 우리가 작성한 코드가 생각한 시나리오에서 정상 동작함을 보장하기 위함이다. 물론! 우리가 생각하지 못한 상황에서에 대한 동작(알 수 없는 예외)에 대해서도 최소한의 예외 처리 등을 작성하여서 코드가 실행되는 애플리케이션이 의도치 않게 종료되는 것(의도된 종료와는 별개)은 막고 보장할 수 있어야 한다. 코드와 프로젝트는 생명주기라는 것이 존재하는데(업데이트, 실제 사용기간 등) 그 주기 내에서 발생할 수 있는 사이드 이펙트를 체크하고 그로 인해 발생할 수 있는 문제를 실제환경이 아니더라도 재현 및 선제적으로 체크할 수 있게 하기 위해서 테스트를 작성한다는 이유를 꼭!!! 생각하면서 테스트를 작성해 보자~ 프로젝트 생성 $ npx create-react-app 프로젝트이름 --te..

  • Jest란? Jest는 페이스북에서 만들어서 React와 더불어 많은 자바스크립트 개발자들로부터 좋은 반응을 얻고 있는 테스팅 라이브러리라고 한다. zero config라고 해서 별도의 설정 없이 빠르게 테스트 케이스를 작성할 수 있다는 장점을 가지고 있다. Jest 설치 npm i -D jest 설치를 완료한 뒤, package.json의 test script를 jest로 수정해 주면 된다. 이후 npm test로 실행하여 테스트 코드를 실행하면 되는데, 이때 실행되는 파일들은 아래와 같다. - 파일명.test.js 혹은 __tests__ 폴더 안에 있는 파일들 test코드 작성 방식 1. 실행 함수를 외부 파일에서 가져오지 않을 때 add.test.js describe('계산 테스트', () => {..

  • Webpack 공식 홈페이지 : https://webpack.js.org/ 웹팩을 설치하기 위해서는 진행하고 있는 프로젝트를 node.js의 패키지 프로젝트로 만들 필요가 있다. $ npm init 모든 HTTP 요청에는 베이스 오버헤드(base overhead), 지속(duration)이 발생한다. 실제 파일 다운로드는 비교적 빨리 진행되지만, 요청(request)을 설정하고, 서버에서 수행되는 것은 시간이 다소 걸리게 된다. 로컬 개발(local development) 서버에서 모든 것들은 우리 머신(machine) 상에서 이루어지고 있기 때문에 설정(setup)은 빠르게 진행되지만, 웹에서 실행하는 경우 이렇게 다수의 요청이 있다면 HTTP 요청의 양만으로 인해 다량의 대기시간이 발생하고 프로젝트가..

  • 유데미 【한글자막】 Typescript :기초부터 실전형 프로젝트까지 with React + NodeJS 강의와 inpa님 블로그를 참고하여 작성..! inpa님 블로그 | 링크 1. 데코레이터 데코레이터는 코드 조각을 장식해주는 역할을 하며 타입스크립트에서 그 기능을 함수로 구현할 수 있다. 예를들어 메소드 / 클래스 / 프로퍼티 / 파라미터 위에 @함수 를 장식해줌으로써, 코드가 실행(런타임)이 되면 데코레이터 함수가 실행되어, 장식한 멤버를 보다 파워풀하게 꾸며주는 것으로 이해하면 된다. TypeScript 공식 문서 설명 「TypeScript 및 ES6에 클래스가 도입됨에 따라, 클래스 및 클래스 멤버에 어노테이션을 달거나 수정하기 위해 추가 기능이 필요한 특정 시나리오가 있습니다. 데코레이터는 ..

  • 1. 인터섹션 타입 두가지의 타입을 결합하여 다른 타입을 만들 수 있다. type 사용 type Admin = { name: string; privileges: string[]; }; type Employee = { name: string; startDate: Date; }; type ElevatedEmployee = Admin & Employee; const e1: ElevatedEmployee = { name: "Max", privileges: ["create-server"], startDate: new Date(), }; interface 사용 interface Admin { name: string; privileges: string[]; }; interface Employee { name: str..

작성일
2024. 1. 11. 22:37
작성자
sognociel

스터디용으로 파놓은 repository에 push를 하려고 했는데...

 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to 'https://github.com/sognociel/express_study.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

짜란~~~ 다들 많이 봤을지도 모르는 화면이다

 

이유는

repository를 만들 때 README.md가 같이 생성되게 해 놓았는데, 내 로컬 프로젝트는 repository랑 연결해 놓고 pull을 받지 않은 상태에서 push를 날렸기 때문이다...

그럼 pull을 받으면 되겠지? 했는데 아..안되는 것이었다...

hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint: 
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint: 
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to spec

 

망할... 친절하게 알려주긴 하는데 머리터질거같음... 원격과 로컬사이의 조정타임이 필요한다고 한다.

조정 방법은 저 셋중에 하나를 사용해 보거라^^ 하는 것.

그런 고로 저 세 가지를 간단하게 정리해 보기로 했다 (몇 번 저 에러 볼 때마다 놀라서 이젠 그냥 머릿속에 콱 박아둘라고...)

 

 

1. git pull --rebase false : 병합 접근 방식

git이 원격 저장소를 끌어오는 동안 병합 접근 방식을 사용하도록 한다.

원격 저장소에서 변경 사항을 가져오면 git은 원격 브랜치의 변경 사항과 로컬 변경 사항을 결합하여 새로운 병합 커밋을 생성한다. 그러면 병합 커밋이 포함된 커민 기록이 생성되어 통합이 발생한 시기를 명확하게 알 수 있다고 한다.

 

그러니까 내가 push하려고 썼던 커밋 + Merged 어쩌고~ 이렇게 두 개가 남게 되는 것이다.

 

 

2. git pull --rebase true: 리베이스 접근 방식

git이 원격 저장소를 끌어오는 동안 rebase 접근 방식을 사용하도록 구성되는데, 이 rebase라는 것은 원격 브랜치의 변경 사항 위에 로컬 커밋을 적용하여 로컬 커밋 기록을 다시 작성하는 것이다. 이렇게 하면 추가의 병합 커밋 없이 선형적인 커밋 기록이 생성된다.

re base라고 base를 새로 지정하는 거라 생각하면 좋다는 글을 봤는데 아마 원격 저장소를 새로운 베이스로 해서 그 위에 내 작업들을 올리고 커밋하는 방식이라고 이해하기로 했다.

 

요렇게 내가 원래 쓴 커밋으로 정리되어 나오는 듯

 

 

3. git pull --ff only : fast-forward only

fast-forward라고 로컬 브랜치의 HEAD가 대상 브랜치의 HEAD까지로 옮기는 방법이다. 이것 또한 새 병합 커밋을 생성하지 않고 로컬 브랜치를 원격 브랜치의 최신 커밋으로 업데이트하는 방법이다.

rebase랑 비슷해 보이는데... 조금 다른 것은 ff의 경우 앞선 커밋 (원격 저장소)를 충돌 없이 따라갈 수 있는 관계에서만 사용 가능하다고 한다.

 

보통 fast-forward 방식을 더 추천한다고 한다 (rebase의 경우 히스토리를 영구적으로 변경할 수 있기 때문에...)

작성일
2024. 1. 9. 21:49
작성자
sognociel

이번 프로젝트를 진행하면서 multer-s3를 사용해 보게 되었는데, 팀장님이 작성한 코드만 보며 조각조각땃땃따를 하다 보니 도대체 얘는 어떻게 진행되고 있는 것인가... 도저히 이해를 못 하겠던 것이었다...

계속되는 머리털뜯기... 그걸 보는 팀장님... (진짜 답답하셨을 듯 이해를 1도 못하고 있어서...)

그도 그럴게 Node.js로 api 한번 만들어보질 않았는데 바로 Nest.js typeorm 이렇게 넘어가서 그런 걸 지도

미들웨어에 대한 개념도 부족해서 (저번에 팀장님이 설명해 주셨는데 또 잊었음!! 이게 다 블로그를 안 써서 그런 것 같음!!!)

이번에 Express로 미들웨어 함수를 만들어 보라고 하시길래 이번엔 절대 잊지 않겠다... 는 마음으로 만드는 과정을 샥샥 적어볼까 한다.

공부 잘하는 사람 커비처럼 집어삼켜서 그 사람 지식 내 것으로 만들고 싶다

 

 

Express 앱에서 사용하기 위한 미들웨어 작성

미들웨어란 무엇이냐?

요청 오브젝트(req), 응답 오브젝트(res), 그리고 애플리케이션의 요청-응답 주기 중 그다음의 미들웨어 함수에 대한 액세스 권한을 갖는 함수이다. 그 다음의 미들웨어 함수는 일반적으로 next라는 이름의 변수로 표시된다.

미들웨어 함수는 다음과 같은 task를 수행할 수 있다.

  • 모든 코드를 실행
  • 요청 및 응답 오브젝트에 대한 변경을 실행
  • 요청-응답 주기를 종료
  • 스택 내의 그 다음 미들웨어를 호출

현재의 미들웨어 함수가 요청-응답 주기를 종료하지 않는 경우에는 next()를 호출하여 그 다음 미들웨어 함수에 제어를 전달해야 한다. 그렇지 않으면 해당 요청은 정지된 채로 방치된다. 이게 무슨 말인가 하면...

미들웨어 함수를 실행했는데 응답이 없다? 그런데 next도 없다..? 그럼 timeout이 날 맞이해 줄 거라는 의미다^^7

미들웨어 함수 호출 요소

위 사진의 요소들을 살펴보자면

get: 미들웨어 함수가 적용되는 HTTP 메서드 (post, patch, delete 등이 있다)

'/': 미들웨어 함수가 적용되는 경로(라우트)로 '/sign-up' 등으로 용도에 맞게 설정할 수 있다.

function: 미들웨어 함수

req: 미들웨어 함수에 대한 HTTP 요청 인수

res: 미들웨어 함수에 대한 HTTP 응답 인수

next: 미들웨어 함수에 대한 콜백 인수 (일반적으로 next라 불림)

 

 

그리고 내가 지금 해야 될 건 무엇이냐

string data가 아닌 multipart/form-data를 받아야 한다는 것이다

multer를 사용해서

...

 

 

Multer란 어떤 모듈이냐?

multipart/form-data를 다루기 위한 node.js의 미들웨어로 파일 업로드를 위해 사용되는 모듈이다.

multer는 body객체와 한 개의 file 혹은 여러 개의 files 객체를 request객체에 추가한다.

body객체는 폼 텍스트 필드의 값을 포함하고, 한 개 혹은 여러개의 파일 객체는 폼을 통해 업로드된 파일들을 포함하고 있다.

 

multer package 안에는 여러 종류의 미들웨어가 있다.

  • storage: 저장할 공간에 대한 정보. 디스크나 메모리 저장 가능
  • diskStorage: 하드디스크에 업로드 파일을 저장
  • destination: 저장할 경로
  • filename: 저장할 파일명 (파일명+날짜+확장자 형식)
  • limits: 파일 개수나 파일 사이즈 제한

각각의 파일은 아래의 정보를 포함하고 있다고 한다.

Key Description Note
fieldname 폼에 정의된 필드 명  
originalname 사용자가 업로드한 파일 명  
encoding 파일의 엔코딩 타입  
mimetype 파일의 Mime 타입  
size 파일의 byte 사이즈  
destination 파일이 저장된 폴더 DiskStorage
filename destination에 저장된 파일 명 DiskStorage
path 업로드된 파일의 전체 경로 DiskStorage
buffer 전체 파일의 Buffer MemoryStorage

 

const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');

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

try {
  fs.readdirSync('uploads'); // 폴더 확인
} catch (err) {
  fs.mkdirSync('uploads'); // 폴더 생성
}

const upload = multer({
  storage: multer.diskStorage({
    // 저장하는 공간: 하드디스크
    destination: function (req, res, cb) {
      // 저장 위치
      cb(null, 'uploads/'); // uploads라는 폴더 안에 저장
    },
    filename: function (req, res, cb) {
      const ext = path.extname(res.originalname); // 파일의 확장자
      const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
      cb(null, `${res.fieldname}-${uniqueSuffix}${ext}`);
    },
  }),
  limits: { fileSize: 5 * 1024 * 1024 }, // 5메가로 용량 제한
});

app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'multer-study.html'));
});

app.post('/upload', upload.single('image'), (req, res) => {
  const file = req.file;
  if (!file) {
    return res.status(400).send('No file uploaded.');
  }
  res.send('File uploaded!');
});

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

 

storage 속성에는 destination(어디에)과 filename(저장할 이름)을 지정하였다.

destination과 filename의 매개변수는 req, res, cb로 동일한데 다음과 같은 값을 가지고 있다.

req: 요청에 대한 정보

res: 응답, 업로드한 파일에 대한 정보

cb: 콜백함수로 첫 번째 인수에는 에러가 있다면 에러를, 두 번째 인수에는 실제 경로나 파일 이름을 넣어주면 된다고 한다.

req나 res의 데이터를 가공해 cb로 넘기는 것.

 

위 설정은 uploads라는 폴더에 '파일명-현재시간-랜덤숫자.확장자' 파일명으로 업로드를 하는 것이다.

 

그리고 업로드를 실행했을 때 res에 찍히는 값들은 아래와 같다.

// console.log(res)의 값
{
  fieldname: 'image',
  originalname: 'asdf.png',
  encoding: '7bit',
  mimetype: 'image/png'
}

// console.log(ext)의 값
.png

// console.log(uniqueSuffix)의 값
1704975872967-311838807

// 그런고로 파일 이름은
image-1704975872967-311838807.png

 

res를 찍어봤으니 req도 한 번 찍어보자

더보기

^^....

<ref *2> IncomingMessage {
  _readableState: ReadableState {
    objectMode: false,
    highWaterMark: 16384,
    buffer: BufferList { head: null, tail: null, length: 0 },
    length: 0,
    pipes: [],
    flowing: false,
    ended: true,
    endEmitted: true,
    reading: false,
    constructed: true,
    sync: false,
    needReadable: false,
    emittedReadable: false,
    readableListening: false,
    resumeScheduled: false,
    errorEmitted: false,
    emitClose: true,
    autoDestroy: true,
    destroyed: true,
    errored: null,
    closed: true,
    closeEmitted: true,
    defaultEncoding: 'utf8',
    awaitDrainWriters: null,
    multiAwaitDrain: false,
    readingMore: false,
    dataEmitted: true,
    decoder: null,
    encoding: null,
    [Symbol(kPaused)]: true
  },
  _events: [Object: null prototype] {},
  _eventsCount: 0,
  _maxListeners: undefined,
  socket: <ref *1> Socket {
    connecting: false,
    _hadError: false,
    _parent: null,
    _host: null,
    _closeAfterHandlingError: false,
    _readableState: ReadableState {
      objectMode: false,
      highWaterMark: 16384,
      buffer: BufferList { head: null, tail: null, length: 0 },
      length: 0,
      pipes: [],
      flowing: true,
      ended: false,
      endEmitted: false,
      reading: true,
      constructed: true,
      sync: false,
      needReadable: true,
      emittedReadable: false,
      readableListening: false,
      resumeScheduled: false,
      errorEmitted: false,
      emitClose: false,
      autoDestroy: true,
      destroyed: false,
      errored: null,
      closed: false,
      closeEmitted: false,
      defaultEncoding: 'utf8',
      awaitDrainWriters: null,
      multiAwaitDrain: false,
      readingMore: false,
      dataEmitted: false,
      decoder: null,
      encoding: null,
      [Symbol(kPaused)]: false
    },
    _events: [Object: null prototype] {
      end: [Array],
      timeout: [Function: socketOnTimeout],
      data: [Function: bound socketOnData],
      error: [Function: socketOnError],
      close: [Array],
      drain: [Function: bound socketOnDrain],
      resume: [Function: onSocketResume],
      pause: [Function: onSocketPause]
    },
    _eventsCount: 8,
    _maxListeners: undefined,
    _writableState: WritableState {
      objectMode: false,
      highWaterMark: 16384,
      finalCalled: false,
      needDrain: false,
      ending: false,
      ended: false,
      finished: false,
      destroyed: false,
      decodeStrings: false,
      defaultEncoding: 'utf8',
      length: 0,
      writing: false,
      corked: 0,
      sync: true,
      bufferProcessing: false,
      onwrite: [Function: bound onwrite],
      writecb: null,
      writelen: 0,
      afterWriteTickInfo: null,
      buffered: [],
      bufferedIndex: 0,
      allBuffers: true,
      allNoop: true,
      pendingcb: 0,
      constructed: true,
      prefinished: false,
      errorEmitted: false,
      emitClose: false,
      autoDestroy: true,
      errored: null,
      closed: false,
      closeEmitted: false,
      [Symbol(kOnFinished)]: []
    },
    allowHalfOpen: true,
    _sockname: null,
    _pendingData: null,
    _pendingEncoding: '',
    server: Server {
      maxHeaderSize: undefined,
      insecureHTTPParser: undefined,
      requestTimeout: 300000,
      headersTimeout: 60000,
      keepAliveTimeout: 5000,
      connectionsCheckingInterval: 30000,
      joinDuplicateHeaders: undefined,
      rejectNonStandardBodyWrites: false,
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      _connections: 2,
      _handle: [TCP],
      _usingWorkers: false,
      _workers: [],
      _unref: false,
      allowHalfOpen: true,
      pauseOnConnect: false,
      noDelay: true,
      keepAlive: false,
      keepAliveInitialDelay: 0,
      highWaterMark: 16384,
      httpAllowHalfOpen: false,
      timeout: 0,
      maxHeadersCount: null,
      maxRequestsPerSocket: 0,
      _connectionKey: '6::::3000',
      [Symbol(IncomingMessage)]: [Function: IncomingMessage],
      [Symbol(ServerResponse)]: [Function: ServerResponse],
      [Symbol(kCapture)]: false,
      [Symbol(async_id_symbol)]: 5,
      [Symbol(http.server.connections)]: ConnectionsList {},
      [Symbol(http.server.connectionsCheckingInterval)]: Timeout {
        _idleTimeout: 30000,
        _idlePrev: [TimersList],
        _idleNext: [TimersList],
        _idleStart: 68,
        _onTimeout: [Function: bound checkConnections],
        _timerArgs: undefined,
        _repeat: 30000,
        _destroyed: false,
        [Symbol(refed)]: false,
        [Symbol(kHasPrimitive)]: false,
        [Symbol(asyncId)]: 4,
        [Symbol(triggerId)]: 1
      },
      [Symbol(kUniqueHeaders)]: null
    },
    _server: Server {
      maxHeaderSize: undefined,
      insecureHTTPParser: undefined,
      requestTimeout: 300000,
      headersTimeout: 60000,
      keepAliveTimeout: 5000,
      connectionsCheckingInterval: 30000,
      joinDuplicateHeaders: undefined,
      rejectNonStandardBodyWrites: false,
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      _connections: 2,
      _handle: [TCP],
      _usingWorkers: false,
      _workers: [],
      _unref: false,
      allowHalfOpen: true,
      pauseOnConnect: false,
      noDelay: true,
      keepAlive: false,
      keepAliveInitialDelay: 0,
      highWaterMark: 16384,
      httpAllowHalfOpen: false,
      timeout: 0,
      maxHeadersCount: null,
      maxRequestsPerSocket: 0,
      _connectionKey: '6::::3000',
      [Symbol(IncomingMessage)]: [Function: IncomingMessage],
      [Symbol(ServerResponse)]: [Function: ServerResponse],
      [Symbol(kCapture)]: false,
      [Symbol(async_id_symbol)]: 5,
      [Symbol(http.server.connections)]: ConnectionsList {},
      [Symbol(http.server.connectionsCheckingInterval)]: Timeout {
        _idleTimeout: 30000,
        _idlePrev: [TimersList],
        _idleNext: [TimersList],
        _idleStart: 68,
        _onTimeout: [Function: bound checkConnections],
        _timerArgs: undefined,
        _repeat: 30000,
        _destroyed: false,
        [Symbol(refed)]: false,
        [Symbol(kHasPrimitive)]: false,
        [Symbol(asyncId)]: 4,
        [Symbol(triggerId)]: 1
      },
      [Symbol(kUniqueHeaders)]: null
    },
    parser: HTTPParser {
      '0': null,
      '1': [Function: parserOnHeaders],
      '2': [Function: parserOnHeadersComplete],
      '3': [Function: parserOnBody],
      '4': [Function: parserOnMessageComplete],
      '5': [Function: bound onParserExecute],
      '6': [Function: bound onParserTimeout],
      _headers: [],
      _url: '',
      socket: [Circular *1],
      incoming: [Circular *2],
      outgoing: null,
      maxHeaderPairs: 2000,
      _consumed: true,
      onIncoming: [Function: bound parserOnIncoming],
      [Symbol(resource_symbol)]: [HTTPServerAsyncResource]
    },
    on: [Function: socketListenerWrap],
    addListener: [Function: socketListenerWrap],
    prependListener: [Function: socketListenerWrap],
    setEncoding: [Function: socketSetEncoding],
    _paused: false,
    _httpMessage: ServerResponse {
      _events: [Object: null prototype],
      _eventsCount: 1,
      _maxListeners: undefined,
      outputData: [],
      outputSize: 0,
      writable: true,
      destroyed: false,
      _last: false,
      chunkedEncoding: false,
      shouldKeepAlive: true,
      maxRequestsOnConnectionReached: false,
      _defaultKeepAlive: true,
      useChunkedEncodingByDefault: true,
      sendDate: true,
      _removedConnection: false,
      _removedContLen: false,
      _removedTE: false,
      strictContentLength: false,
      _contentLength: null,
      _hasBody: true,
      _trailer: '',
      finished: false,
      _headerSent: false,
      _closed: false,
      socket: [Circular *1],
      _header: null,
      _keepAliveTimeout: 5000,
      _onPendingData: [Function: bound updateOutgoingData],
      req: [Circular *2],
      _sent100: false,
      _expect_continue: false,
      _maxRequestsPerSocket: 0,
      locals: [Object: null prototype] {},
      [Symbol(kCapture)]: false,
      [Symbol(kBytesWritten)]: 0,
      [Symbol(kNeedDrain)]: false,
      [Symbol(corked)]: 0,
      [Symbol(kOutHeaders)]: [Object: null prototype],
      [Symbol(errored)]: null,
      [Symbol(kHighWaterMark)]: 16384,
      [Symbol(kRejectNonStandardBodyWrites)]: false,
      [Symbol(kUniqueHeaders)]: null
    },
    [Symbol(async_id_symbol)]: 15,
    [Symbol(kHandle)]: TCP {
      reading: true,
      onconnection: null,
      _consumed: true,
      [Symbol(owner_symbol)]: [Circular *1]
    },
    [Symbol(lastWriteQueueSize)]: 0,
    [Symbol(timeout)]: null,
    [Symbol(kBuffer)]: null,
    [Symbol(kBufferCb)]: null,
    [Symbol(kBufferGen)]: null,
    [Symbol(kCapture)]: false,
    [Symbol(kSetNoDelay)]: true,
    [Symbol(kSetKeepAlive)]: false,
    [Symbol(kSetKeepAliveInitialDelay)]: 0,
    [Symbol(kBytesRead)]: 0,
    [Symbol(kBytesWritten)]: 0
  },
  httpVersionMajor: 1,
  httpVersionMinor: 1,
  httpVersion: '1.1',
  complete: true,
  rawHeaders: [
    'Host',
    'localhost:3000',
    'Connection',
    'keep-alive',
    'Content-Length',
    '38400',
    'Cache-Control',
    'max-age=0',
    'sec-ch-ua',
    '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
    'sec-ch-ua-mobile',
    '?0',
    'sec-ch-ua-platform',
    '"macOS"',
    'Upgrade-Insecure-Requests',
    '1',
    'Origin',
    'http://localhost:3000',
    'Content-Type',
    'multipart/form-data; boundary=----WebKitFormBoundarySREJJdJ3tdvDuAFa',
    'User-Agent',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    'Accept',
    'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
    'Sec-Fetch-Site',
    'same-origin',
    'Sec-Fetch-Mode',
    'navigate',
    'Sec-Fetch-User',
    '?1',
    'Sec-Fetch-Dest',
    'document',
    'Referer',
    'http://localhost:3000/',
    'Accept-Encoding',
    'gzip, deflate, br',
    'Accept-Language',
    'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7,ja;q=0.6,zh-CN;q=0.5,zh;q=0.4'
  ],
  rawTrailers: [],
  joinDuplicateHeaders: undefined,
  aborted: false,
  upgrade: false,
  url: '/upload',
  method: 'POST',
  statusCode: null,
  statusMessage: null,
  client: <ref *1> Socket {
    connecting: false,
    _hadError: false,
    _parent: null,
    _host: null,
    _closeAfterHandlingError: false,
    _readableState: ReadableState {
      objectMode: false,
      highWaterMark: 16384,
      buffer: BufferList { head: null, tail: null, length: 0 },
      length: 0,
      pipes: [],
      flowing: true,
      ended: false,
      endEmitted: false,
      reading: true,
      constructed: true,
      sync: false,
      needReadable: true,
      emittedReadable: false,
      readableListening: false,
      resumeScheduled: false,
      errorEmitted: false,
      emitClose: false,
      autoDestroy: true,
      destroyed: false,
      errored: null,
      closed: false,
      closeEmitted: false,
      defaultEncoding: 'utf8',
      awaitDrainWriters: null,
      multiAwaitDrain: false,
      readingMore: false,
      dataEmitted: false,
      decoder: null,
      encoding: null,
      [Symbol(kPaused)]: false
    },
    _events: [Object: null prototype] {
      end: [Array],
      timeout: [Function: socketOnTimeout],
      data: [Function: bound socketOnData],
      error: [Function: socketOnError],
      close: [Array],
      drain: [Function: bound socketOnDrain],
      resume: [Function: onSocketResume],
      pause: [Function: onSocketPause]
    },
    _eventsCount: 8,
    _maxListeners: undefined,
    _writableState: WritableState {
      objectMode: false,
      highWaterMark: 16384,
      finalCalled: false,
      needDrain: false,
      ending: false,
      ended: false,
      finished: false,
      destroyed: false,
      decodeStrings: false,
      defaultEncoding: 'utf8',
      length: 0,
      writing: false,
      corked: 0,
      sync: true,
      bufferProcessing: false,
      onwrite: [Function: bound onwrite],
      writecb: null,
      writelen: 0,
      afterWriteTickInfo: null,
      buffered: [],
      bufferedIndex: 0,
      allBuffers: true,
      allNoop: true,
      pendingcb: 0,
      constructed: true,
      prefinished: false,
      errorEmitted: false,
      emitClose: false,
      autoDestroy: true,
      errored: null,
      closed: false,
      closeEmitted: false,
      [Symbol(kOnFinished)]: []
    },
    allowHalfOpen: true,
    _sockname: null,
    _pendingData: null,
    _pendingEncoding: '',
    server: Server {
      maxHeaderSize: undefined,
      insecureHTTPParser: undefined,
      requestTimeout: 300000,
      headersTimeout: 60000,
      keepAliveTimeout: 5000,
      connectionsCheckingInterval: 30000,
      joinDuplicateHeaders: undefined,
      rejectNonStandardBodyWrites: false,
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      _connections: 2,
      _handle: [TCP],
      _usingWorkers: false,
      _workers: [],
      _unref: false,
      allowHalfOpen: true,
      pauseOnConnect: false,
      noDelay: true,
      keepAlive: false,
      keepAliveInitialDelay: 0,
      highWaterMark: 16384,
      httpAllowHalfOpen: false,
      timeout: 0,
      maxHeadersCount: null,
      maxRequestsPerSocket: 0,
      _connectionKey: '6::::3000',
      [Symbol(IncomingMessage)]: [Function: IncomingMessage],
      [Symbol(ServerResponse)]: [Function: ServerResponse],
      [Symbol(kCapture)]: false,
      [Symbol(async_id_symbol)]: 5,
      [Symbol(http.server.connections)]: ConnectionsList {},
      [Symbol(http.server.connectionsCheckingInterval)]: Timeout {
        _idleTimeout: 30000,
        _idlePrev: [TimersList],
        _idleNext: [TimersList],
        _idleStart: 68,
        _onTimeout: [Function: bound checkConnections],
        _timerArgs: undefined,
        _repeat: 30000,
        _destroyed: false,
        [Symbol(refed)]: false,
        [Symbol(kHasPrimitive)]: false,
        [Symbol(asyncId)]: 4,
        [Symbol(triggerId)]: 1
      },
      [Symbol(kUniqueHeaders)]: null
    },
    _server: Server {
      maxHeaderSize: undefined,
      insecureHTTPParser: undefined,
      requestTimeout: 300000,
      headersTimeout: 60000,
      keepAliveTimeout: 5000,
      connectionsCheckingInterval: 30000,
      joinDuplicateHeaders: undefined,
      rejectNonStandardBodyWrites: false,
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      _connections: 2,
      _handle: [TCP],
      _usingWorkers: false,
      _workers: [],
      _unref: false,
      allowHalfOpen: true,
      pauseOnConnect: false,
      noDelay: true,
      keepAlive: false,
      keepAliveInitialDelay: 0,
      highWaterMark: 16384,
      httpAllowHalfOpen: false,
      timeout: 0,
      maxHeadersCount: null,
      maxRequestsPerSocket: 0,
      _connectionKey: '6::::3000',
      [Symbol(IncomingMessage)]: [Function: IncomingMessage],
      [Symbol(ServerResponse)]: [Function: ServerResponse],
      [Symbol(kCapture)]: false,
      [Symbol(async_id_symbol)]: 5,
      [Symbol(http.server.connections)]: ConnectionsList {},
      [Symbol(http.server.connectionsCheckingInterval)]: Timeout {
        _idleTimeout: 30000,
        _idlePrev: [TimersList],
        _idleNext: [TimersList],
        _idleStart: 68,
        _onTimeout: [Function: bound checkConnections],
        _timerArgs: undefined,
        _repeat: 30000,
        _destroyed: false,
        [Symbol(refed)]: false,
        [Symbol(kHasPrimitive)]: false,
        [Symbol(asyncId)]: 4,
        [Symbol(triggerId)]: 1
      },
      [Symbol(kUniqueHeaders)]: null
    },
    parser: HTTPParser {
      '0': null,
      '1': [Function: parserOnHeaders],
      '2': [Function: parserOnHeadersComplete],
      '3': [Function: parserOnBody],
      '4': [Function: parserOnMessageComplete],
      '5': [Function: bound onParserExecute],
      '6': [Function: bound onParserTimeout],
      _headers: [],
      _url: '',
      socket: [Circular *1],
      incoming: [Circular *2],
      outgoing: null,
      maxHeaderPairs: 2000,
      _consumed: true,
      onIncoming: [Function: bound parserOnIncoming],
      [Symbol(resource_symbol)]: [HTTPServerAsyncResource]
    },
    on: [Function: socketListenerWrap],
    addListener: [Function: socketListenerWrap],
    prependListener: [Function: socketListenerWrap],
    setEncoding: [Function: socketSetEncoding],
    _paused: false,
    _httpMessage: ServerResponse {
      _events: [Object: null prototype],
      _eventsCount: 1,
      _maxListeners: undefined,
      outputData: [],
      outputSize: 0,
      writable: true,
      destroyed: false,
      _last: false,
      chunkedEncoding: false,
      shouldKeepAlive: true,
      maxRequestsOnConnectionReached: false,
      _defaultKeepAlive: true,
      useChunkedEncodingByDefault: true,
      sendDate: true,
      _removedConnection: false,
      _removedContLen: false,
      _removedTE: false,
      strictContentLength: false,
      _contentLength: null,
      _hasBody: true,
      _trailer: '',
      finished: false,
      _headerSent: false,
      _closed: false,
      socket: [Circular *1],
      _header: null,
      _keepAliveTimeout: 5000,
      _onPendingData: [Function: bound updateOutgoingData],
      req: [Circular *2],
      _sent100: false,
      _expect_continue: false,
      _maxRequestsPerSocket: 0,
      locals: [Object: null prototype] {},
      [Symbol(kCapture)]: false,
      [Symbol(kBytesWritten)]: 0,
      [Symbol(kNeedDrain)]: false,
      [Symbol(corked)]: 0,
      [Symbol(kOutHeaders)]: [Object: null prototype],
      [Symbol(errored)]: null,
      [Symbol(kHighWaterMark)]: 16384,
      [Symbol(kRejectNonStandardBodyWrites)]: false,
      [Symbol(kUniqueHeaders)]: null
    },
    [Symbol(async_id_symbol)]: 15,
    [Symbol(kHandle)]: TCP {
      reading: true,
      onconnection: null,
      _consumed: true,
      [Symbol(owner_symbol)]: [Circular *1]
    },
    [Symbol(lastWriteQueueSize)]: 0,
    [Symbol(timeout)]: null,
    [Symbol(kBuffer)]: null,
    [Symbol(kBufferCb)]: null,
    [Symbol(kBufferGen)]: null,
    [Symbol(kCapture)]: false,
    [Symbol(kSetNoDelay)]: true,
    [Symbol(kSetKeepAlive)]: false,
    [Symbol(kSetKeepAliveInitialDelay)]: 0,
    [Symbol(kBytesRead)]: 0,
    [Symbol(kBytesWritten)]: 0
  },
  _consuming: true,
  _dumped: false,
  next: [Function: next],
  baseUrl: '',
  originalUrl: '/upload',
  _parsedUrl: Url {
    protocol: null,
    slashes: null,
    auth: null,
    host: null,
    port: null,
    hostname: null,
    hash: null,
    search: null,
    query: null,
    pathname: '/upload',
    path: '/upload',
    href: '/upload',
    _raw: '/upload'
  },
  params: {},
  query: {},
  res: <ref *3> ServerResponse {
    _events: [Object: null prototype] { finish: [Function: bound resOnFinish] },
    _eventsCount: 1,
    _maxListeners: undefined,
    outputData: [],
    outputSize: 0,
    writable: true,
    destroyed: false,
    _last: false,
    chunkedEncoding: false,
    shouldKeepAlive: true,
    maxRequestsOnConnectionReached: false,
    _defaultKeepAlive: true,
    useChunkedEncodingByDefault: true,
    sendDate: true,
    _removedConnection: false,
    _removedContLen: false,
    _removedTE: false,
    strictContentLength: false,
    _contentLength: null,
    _hasBody: true,
    _trailer: '',
    finished: false,
    _headerSent: false,
    _closed: false,
    socket: <ref *1> Socket {
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: null,
      _closeAfterHandlingError: false,
      _readableState: [ReadableState],
      _events: [Object: null prototype],
      _eventsCount: 8,
      _maxListeners: undefined,
      _writableState: [WritableState],
      allowHalfOpen: true,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: [Server],
      _server: [Server],
      parser: [HTTPParser],
      on: [Function: socketListenerWrap],
      addListener: [Function: socketListenerWrap],
      prependListener: [Function: socketListenerWrap],
      setEncoding: [Function: socketSetEncoding],
      _paused: false,
      _httpMessage: [Circular *3],
      [Symbol(async_id_symbol)]: 15,
      [Symbol(kHandle)]: [TCP],
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: null,
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(kCapture)]: false,
      [Symbol(kSetNoDelay)]: true,
      [Symbol(kSetKeepAlive)]: false,
      [Symbol(kSetKeepAliveInitialDelay)]: 0,
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0
    },
    _header: null,
    _keepAliveTimeout: 5000,
    _onPendingData: [Function: bound updateOutgoingData],
    req: [Circular *2],
    _sent100: false,
    _expect_continue: false,
    _maxRequestsPerSocket: 0,
    locals: [Object: null prototype] {},
    [Symbol(kCapture)]: false,
    [Symbol(kBytesWritten)]: 0,
    [Symbol(kNeedDrain)]: false,
    [Symbol(corked)]: 0,
    [Symbol(kOutHeaders)]: [Object: null prototype] { 'x-powered-by': [Array] },
    [Symbol(errored)]: null,
    [Symbol(kHighWaterMark)]: 16384,
    [Symbol(kRejectNonStandardBodyWrites)]: false,
    [Symbol(kUniqueHeaders)]: null
  },
  route: Route {
    path: '/upload',
    stack: [ [Layer], [Layer] ],
    methods: { post: true }
  },
  body: [Object: null prototype] { title: '' },
  file: {
    fieldname: 'image',
    originalname: 'asdf.png',
    encoding: '7bit',
    mimetype: 'image/png',
    destination: 'uploads/',
    filename: 'image-1704976867905-748609076.png',
    path: 'uploads/image-1704976867905-748609076.png',
    size: 38126
  },
  [Symbol(kCapture)]: false,
  [Symbol(kHeaders)]: {
    host: 'localhost:3000',
    connection: 'keep-alive',
    'content-length': '38400',
    'cache-control': 'max-age=0',
    'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"macOS"',
    'upgrade-insecure-requests': '1',
    origin: 'http://localhost:3000',
    'content-type': 'multipart/form-data; boundary=----WebKitFormBoundarySREJJdJ3tdvDuAFa',
    'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
    'sec-fetch-site': 'same-origin',
    'sec-fetch-mode': 'navigate',
    'sec-fetch-user': '?1',
    'sec-fetch-dest': 'document',
    referer: 'http://localhost:3000/',
    'accept-encoding': 'gzip, deflate, br',
    'accept-language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7,ja;q=0.6,zh-CN;q=0.5,zh;q=0.4'
  },
  [Symbol(kHeadersCount)]: 38,
  [Symbol(kTrailers)]: null,
  [Symbol(kTrailersCount)]: 0
}

req의 끝부분쯤에 file 객체가 보이는데 여기서 업로드한 이미지의 자세한 정보를 볼 수 있다~~~

{
  fieldname: 'image',
  originalname: 'asdf.png',
  encoding: '7bit',
  mimetype: 'image/png',
  destination: 'uploads/',
  filename: 'image-1704976826388-3119730.png',
  path: 'uploads/image-1704976826388-3119730.png',
  size: 38126
}

 

 

이미지 잘 들어갔다~!!

 

 

 

참고

Express 홈페이지

multer github

Inpa님의 [Express] multer 미들웨어 사용법 정리

'일하면서 공부해욧' 카테고리의 다른 글

git pull  (0) 2024.01.11
데이터베이스 외부접속하기 (Docker, MySQL, 포트포워딩)  (0) 2023.07.16
Delete `␍` eslint (prettier/prettier)  (0) 2023.07.01
custom class validator  (0) 2023.06.25
node.js와 express  (0) 2023.06.18
작성일
2023. 7. 16. 19:34
작성자
sognociel

회사에서 Docker를 통해 MySQL을 띄워놓았다.

회사 노트북으로 돌아가고 있는 DB에 접근하려면 설정해줘야 할 것들이 몇 가지 있어서 나중에 또 이런 일이 있을 때 참고하려고 정리해 두기로 했다.

 

알아보기 쉽게 외부/내부로(어감이 조금 이상하긴 하지만...) 구분하겠다.

내부 노트북 : Docker를 통해 MySQL DB를 띄워둠. 이 노트북이 사용하는 공유기에 포트포워딩 설정을 해둬야 함

외부 노트북 : 내부 노트북의 외부 ip주소만 있으면 됨

 

 

1. Docker 컨테이너 설정

기존에 도커를 실행했을 때는 포트를 로컬의 3306번으로 지정을 해두었는데, 이렇게 도커 컨테이너를 실행하면 외부접속이 아예 불가능하므로 처음부터 외부 접속 할 포트를 설정하거나, 아니면 기존에 로컬에서 실행되는 도커 컨테이너를 삭제하고 새로 띄우는 수밖에 없다고 한다...

기존에 띄운 컨테이너의 포트 설정을 바꾸는 방법이 있다면 그렇게 해도 되겠지만 난 방법을 찾질 못해서...ㅎ

 

포트포워딩 하기 전 띄웠던 컨테이너 (이렇게 하면 안 됨!!!!!)

docker run --name (컨테이너이름) -p 127.0.0.1:3306:3306/tcp -e MYSQL_ROOT_PASSWORD=(MySQL비밀번호) -d arm64v8/mysql:latest

 

그래서 위 코드에서 -p 앞에 127.0.0.1을 지워주면 끝! 나는 기존의 컨테이너를 지우고 새로 띄웠다...

docker run --name (컨테이너이름) -p 3306:3306/tcp -e MYSQL_ROOT_PASSWORD=(MySQL비밀번호) -d arm64v8/mysql:latest

 

 

 

 

2. 포트포워딩 설정

공대생 공작소의 공대생님 게시글이 정말 많이 도움이 되었다😭

https://m.blog.naver.com/seoulworkshop/221265052717

 

공유기 관리도구에 들어간다

 

규칙 이름은 본인이 하고 싶은 이름으로 하고, 내부 IP주소는 아래 체크박스를 클릭해서 설정해 주었다.

외부 포트는 외부에서 접속할 포트 번호로, 내부 포트는 외부 접속을 내부 포트 번호로 연결해 준다고 보면 된다.

예를 들면 외부 노트북 3306번으로 접속을 하면 내부 노트북의 3306 포트로 연결해 준다는 것.

포트 번호가 똑같아서 헷갈릴 수도 있으니 다른 번호로 해보자면 외부 포트를 3306으로 하고 내부 포트를 3000으로 한다면 외부 노트북 3306번으로 접속을 했을 때 내부 노트북의 3000으로 연결해 준다.

그리고 추가를 해주면 포트포워딩은 끝!

 

 

 

3. 외부 노트북에서 내부 노트북 DB 접속하기

.local.env 파일을 바꿔주었다.

# ###############
# MAIN
# ###############
NODE_ENV=local
PORT=3000

# ###############
# DB info
# ###############
DB_HOST=(내부 노트북의 외부 ip 주소)
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=(MySQL root 비밀번호)
DB_DATABASE=DBStudy

 

내부 노트북의 외부 ip주소는 초록창에서 ip주소를 검색하거나, 파워쉘 혹은 명령프롬포트에서 확인할 수 있다.

curl ipinfo.io/ip

 

windows powershell

 

그리고 실행을 하면..!

짜잔~ 잘 실행되는 걸 볼 수 있다^^7

'일하면서 공부해욧' 카테고리의 다른 글

git pull  (0) 2024.01.11
Multer와 미들웨어에 대해 알아보았다  (0) 2024.01.09
Delete `␍` eslint (prettier/prettier)  (0) 2023.07.01
custom class validator  (0) 2023.06.25
node.js와 express  (0) 2023.06.18
작성일
2023. 7. 1. 18:21
작성자
sognociel

스터디는 맥, 집에서 따로 공부할 때는 윈도우를 쓰는데, git repository에 그날 한 것들을 올려놓고 받아오는 식으로 진행을 하려고 했다. 출퇴근시간에 노트북을 들고 다니기가 번거로웠고... 목이랑 어깨가 안 좋아서 매번 들고 다닐 때마다 한의원 VVIP가 되었기 때문에^^7

하지만 처음부터 시뻘건 경고창을 맞닥뜨리게 되는데...

ㅠㅠ....

Prettier와 ESLint를 적용하고 있었는데 Replace의 경우는 Prettier의 괄호 안 공간과 싱글쿼테이션 때문이라 설정에서 바꿔주었더니 바로 해결이 되었다.

괄호 안 공백x, single quote 사용

 

그러나... Delete `␍`의 경우는 계속해서 남아있었다. 알아보니 ESLint에서 발생하는 오류라 실행은 정상적으로 된다고 한다. 하지만 이런 오류표시 정말 엄청 너무나도 심각하게 신경 쓰이고 없애버리고 싶기 때문에 방법을 찾아보았다.

 

ESLint 자체에서 수정해주면 됨!!!

rules:
  'prettier/prettier': [
    'error',
    {
      endOfLine: 'auto',
    },
  ],

ESLint 파일의 rules 부분에 이 코드를 추가해 주면,

짠! 오류 하나 없이 깔끔한 코드를 볼 수 있게 되었다! 햅삐~!

작성일
2023. 6. 25. 17:41
작성자
sognociel

class-validator는 기본적으로 @IsString, @IsNumber 등 기본적인 데코레이터들을 제공해 주지만... 이것만으로 검증을 할 수 없을 때 커스텀이 필요한 경우가 있다!

나는 입력받은 키와 몸무게가 소수점이 없을 때는 3자리 이하로, 그리고 만약 소수점 자리가 있다면 소수점 이하 2자리까지만 입력된 값을 통과시키고 싶었는데, 이런 데코레이터는 없기 때문에... 직접 커스텀을 해야 했다.

 

다행히... custom class validator에 대해 정리해 주신 분이 계셔서😭 정말 많은 도움을 받았다.

https://lee-eonsang.com/blog/custom-class-validator

 

일단 최종적으로 구현한 내용은 아래와 같다.

import {
  registerDecorator,
  ValidationOptions,
  ValidatorConstraint,
  ValidatorConstraintInterface,
  ValidationArguments,
} from 'class-validator';

//키, 몸무게 validate custom
// 소숫점 앞자리는 최대 3자리, 소숫점 이하 두자리
@ValidatorConstraint({name: 'isDecimalPointLength', async: false})
export class CustomDecimalPointConstraint
  implements ValidatorConstraintInterface
{
  // 값 검증 로직
  validate(value: any, args: ValidationArguments) {
    if (!value.includes('.') && value.length > 3) return false;
    if (value.includes('.') && value.split('.')[0].length > 3) return false;
    else if (value.includes('.') && value.split('.')[1].length > 2)
      return false;
    else return true;
  }
}

export function IsDecimalPointLength(validationOptions?: ValidationOptions) {
  return function (object: object, propertyName: string) {
    registerDecorator({
      name: 'isDecimalPointLength',
      target: object.constructor,
      propertyName: propertyName,
      options: validationOptions,
      validator: CustomDecimalPointConstraint,
    });
  };
}

@IsDemicalPointLength라는 데코레이터 함수를 만든 것이다. 데코레이션 함수에서 class validator의 검증을 실행하기 위해서는 registerDecorator를 실행해야 한다고 한다.

 

registerDecorator의 옵션

name custom한 validation의 이름
target validate할 object
propertyName 프로퍼티 명
options validation options
constraints validation 조건 배열
validator validatorConstraintInterface
async 데코레이터가 비동기인지

validator랑 name, async 말고는 그대로 복사해서 붙여 넣으면 되는듯하다.

이 코드에서는 constraints부분을 넣지 않았는데... 그래도 돌아가는 걸 보면 괜찮은 건가..? 내가 넣는 값이 배열이 아니라서 괜찮았던 것일지도...

만약 넣는다면 constraints: []로 빈 배열을 지정해 주면 되는 것 같다.

 

중요한 건 값 검증을 구현하는 validator부분인데, 이는 ValidatorConstraintInterface를 통해 구현을 해야 한다.

async는 데코레이터가 비동기로 작동되는지의 여부를 정해주는 것으로 registerDecorator에 작성을 하거나 ValidatorConstraint에서 작성을 해도 상관이 없는 것 같다.

나는 validator부분을 class로 따로 빼내어 작성했지만, registerDecorator 안에서 바로 작성해도 된다..!

(위 코드에서 validator 부분만 떼어온 코드...)

//키, 몸무게 validate custom
// 소숫점 앞자리는 최대 3자리, 소숫점 이하 두자리
@ValidatorConstraint({name: 'isDecimalPointLength', async: false})
export class CustomDecimalPointConstraint
  implements ValidatorConstraintInterface
{
  // 값 검증 로직
  validate(value: any, args: ValidationArguments) {
    if (!value.includes('.') && value.length > 3) return false;
    if (value.includes('.') && value.split('.')[0].length > 3) return false;
    else if (value.includes('.') && value.split('.')[1].length > 2)
      return false;
    else return true;
  }
}

class이름으로 지정한 것을 register 할 때 validator에 넣어주면 된다.

validate부분은 본인이 검증하고 싶은 로직을 넣어주면 됨!

 

defalutMessage도 지정해 줄 수 있다고 하는데, 다음에 필요할 때가 온다면 지정해 보는 것으로..!

 

사용법은 간단하다. custom validator 함수를 import 하고 데코레이터처럼 사용하면 되는 것

import {IsDemicalPointLength} from '함수 만든 경로'

export class testDTO {
  @IsDemicalPointLength()
  height!: string;
  
  @IsDemicalPointLength()
  weight!: string;
}
작성일
2023. 6. 18. 16:57
작성자
sognociel

오늘은 node.js와 express에 대해서 쪼끔... 알아보기로 한다.

그냥 node.js 홈페이지에 가서 설치를 해도 되지만, 이전에 프로젝트를 수행하다가 노드 버전 때문에 계속 나 혼자 충돌? 이 일어났던 적이 있어 nvm으로 설치를 하는 게 좋은 것 같다.

알고 보니 이전에 개발 공부를 하시던 분들은 그때 최신버전이 16이었고, 나는 새싹에서 처음 다운로드하였어서 18 버전이라 그랬던 것...

 

https://github.com/coreybutler/nvm-windows/releases

nvm-setup.zip 이었던가..? 설치를 하고 실행하면 됐던 것 같다.

$ nvm install v16 <- 설치하고 싶은 버전

// 내가 설치한 목록 확인
$ nvm ls

// 사용할 버전
$ nvm use 16

 

이렇게 사용하면 된다..!

 

 

index.js

console.log("Hello");

원래라면 해당 파일은 브라우저 상의 console에서 확인이 가능했었는데, node.js를 설치함으로 인해서 node 명령어를 사용하여 해당 내용을 확인할 수 있게 되었다.

node 명령어를 이용하여 실행!

 

 

 

express

node.js 기반의 웹 프레임워크(요청에 따라 응답해 주는 역할)

 

설치방법

$ npm i express
$ yarn add express

yarn은 npm을 보완하여 나온 패키지 매니저인데, node를 설치하면 사용할 수 있는 npm과 달리 따로 설치를 해주어야 한다.

$ npm install yarn -g
$ yarn --version

 

이제 express를 사용해보자!

index.js

// 예제는 express 공식 문서에서 가져왔다.
const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  // 응답에 Hello World 라는 문자를 담아서 보내겠다
  // send 대신 좀 더 명시적으로 해주려면 json을 사용해도 된다.
  res.send('Hello World!')
})

// 항상 돌아가고 있다.
app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

 

port가 3000번이니 localhost:3000번을 듣고 있을 때(주소창에서 localhost를 포트 3000으로 열었을 때) 명령어를 실행할 수 있게 된다.

port는 서버의 주소로 3000, 5000등 포트마다 다른 프로그램의 실행이 가능하다.

HTTP 메서드는 요청의 목적, 종류를 알리는 수단으로 여기에서는 위의 예제에서는 get을 사용하고 있다.

get과 post의 차이점을 간단하게 알아보자면  get(주소창에서 데이터 전달), post(body에 정보를 담아서 전달)

'/'는 라우팅이라고 한다. 

 

cf) axios와 express의 차이

axios: 요청을 보내서 받아오는 것
express: req를 받아서 res를 돌려주는 기능

 

 

 

get

get으로 정보를 전달할 때는 2가지 방법이 있는데 params와 query가 있다.

 

params

유튜브 등 라우팅 뒤에 각각 채널의 아이디가 들어가는데, 유튜버마다 각각 라우팅을 해준다는 것은... n만자가 넘어갈 것이기 때문에  말이 안 된다. 아이디에 따라 화면을 다르게 보여줘야 하는데 이때 각각의 아이디를 파라미터를 통해 받아올 수 있다.

app.get("/user/:id", (req, res) => {
  const q = req.params;
  console.log(q);
  res.json({ id: q.id });
});

localhost로 접근
console로 나오는 값

 

 

query

app.get("/user/:id", (req, res) => {
  const q = req.query;
  console.log(q);
});

위와 같이 코드를 작성하고 주소창에 아래와 같이 입력을 하면

http://localhost:3000/user/water?q=query&name=water&age=30

이렇게 값을 받아올 수 있게 된다

 

 

잠깐 알고 가는 package.json과 package-lock.json의 차이

package.json | 내용을 대략적으로 확인하기 위함
package-lock.json | 내용을 상세하게 확인하기 위함

(아직 뭔가 설치를 하지는 않았지만...)

패키지 설치를 하게 되면, 오른쪽 화면과 같이 해당 패키지의 버전 등 조금 더 상세한 내용이 package-lock.json에 들어가게 된다.

 

 

 

동물 소리 API 서버 만들어보기

조코딩님의 유튜브 참고

원래는 param를 p에 담았다면 p.id 혹은 p.name을 찍어주어야 그 값이 나왔는데, 중괄호 안에 key값을 입력하여 사용하면 간단해진다. (구조분해할당)

const { name } = req.params

 

그래서 새로 작성한 index.js를 실행해 보면...

app.get("/sound/:name", (req, res) => {
  const { name } = req.params;

  if (name == "dog") {
    res.json({ sound: "멍멍" });
  } else if (name == "cat") {
    res.json({ sound: "야옹" });
  } else if (name == "pig") {
    res.json({ sound: "꿀꿀" });
  } else {
    res.json({ sound: "알 수 없음" });
  }
});

이런 식으로 params에 따라 다른 값들을 얻을 수 있다.

 

 

 

CORS Error

CORS는 Cross Origin Resource Sharing의 약자이다. (Origin : Protocol + Host + Port)

로컬에 프론트엔드 서버를 3000번으로 띄웠다면 http://localhost:3000 이 Origin에 해당한다.

Same Origin이란 같은 Origin을 뜻하고, Cross Origin이란 서로 다른 Origin을 뜻하는데 CROS는 서로 다른 Origin 간의 리소스 교환을 뜻한다.
같은 Origin상에서는 요청이 오는 곳과 처리하는 곳이 동일하기 때문에 특별히 보안상 처리해 줄 이유가 없는데, 다른 Origin에서 오는 요청이라면 요청으로 받아온 결과를 믿을만한지 그렇지 않은지 검증하는 과정이 필요하다. 그리고 브라우저에서는 결과의 헤더 값을 통해 CORS를 확인한다.
CORS 에러는 브라우저에서 발생하는 에러로 postman 등으로 서버에 요청을 하면 문제없이 응답이 날아오는데, 프론트엔드 작업을 할 때 브라우저에서 개입을 하기 때문에 발생한다고 한다.

 

해결방법...?

이라기에는 조금 애매한가 싶지만 cors라는 모듈이 있다고 한다.

$ npm i cors
const express = require("express");
const cors = require("cors");
const app = express();
const port = 3000;

app.use(cors());

cors모듈을 설치하고 require로 불러와서 use를 해주면 된다.

cors(어떤 포트들을 허용할 건지 등 조건을 설정해 줄 수 있다)라고 한다..!

'일하면서 공부해욧' 카테고리의 다른 글

Delete `␍` eslint (prettier/prettier)  (0) 2023.07.01
custom class validator  (0) 2023.06.25
class validator와 DTO, 그리고 직렬화/역직렬화  (0) 2023.06.11
React, TypeScript 그리고 Jest  (0) 2023.05.26
Jest  (0) 2023.05.26
작성일
2023. 6. 11. 00:57
작성자
sognociel

뭔가 이것저것 짬뽕인 느낌이지만 길이도 그렇게 길지 않을 테고.. 복습 겸 한꺼번에 모아두는 게 좋을 것 같았다.

나는 차트 데이터에 관해서 코드를 짜보았는데, class-validator는 로그인 등 다양한 곳에서 사용하는 것 같더라..!

요약 : class-validator로 DTO타입 체크하는 여정기.

 

class-validator

class-validator란 joi의 Typescript 버전으로, 데코레이터를 이용하여 편리하게 검증할 수 있는 라이브러리이다. 서버로 들어오는 Json 데이터의 검증을 할 때 유용하게 사용할 수 있다.

joi란 무엇이냐? 이것 또한 validation 라이브러리라고 보면 된다.

 

DTO (Data Transfer Object)
DTO란 말 그대로 해석하면 '데이터 전송 객체'가 된다고 한다. 데이터의 전송을 담당하는 클래스라고 할 수 있으며 웹 서비스의 클라이언트와 서버의 서비스 계층 사이에서 교환되는 데이터를 담는 그릇이라고 할 수 있다. 쉽게? 말하자면 계층 간 데이터 교환을 위한 객체..! 인 듯?
DAO(Data Access Object) 패턴에서 유래된 단어로 DAO에서 DB처리 로직을 숨기고 DTO라는 결괏값을 내보내는 용도로 활용했다고 한다.

 

차트는 react-chartjs-2를 사용했는데, 기본적으로 필요한 데이터는 아래와 같이 설정했다.

차트의 범례가 될 labels와 값이 될 data, 그리고 그려질 차트의 색이 될 backgroundColor를 필수 값으로 설정하고 나머지는 선택으로!

export interface ChartProps {
  labels: string[];
  datasets: {
    data: number[];
    backgroundColor: string[];
    label?: string;
    borderColor?: string[];
    borderWidth?: number;
  }[];
}

const chartDataBase: ChartProps = {
  labels: [],
  datasets: [
    {
      data: [],
      backgroundColor: [],
    },
  ],
};

이 차트의 데이터는 하드코딩이 아니라 API를 통해 받아오게 되는데, 이 받아오는 과정에서 그 값들이 맞게 들어왔는지 확인...? 하기 위해 class-validator를 사용하는 것 같다.

class-validator가 데이터를 검증하는 것은 알겠어! DTO가 데이터 전송 객체라는 것도 대충은 알겠어..! 그런데 둘 사이의 관계가 도대체 어떻게 되는데? -> 정말 헷갈렸는데 찾다 보니 이런 글이 있더라

 

어떠한 값을 검증하려면 그 값에 대한 정의가 필요하겠죠?
그 정의를 dto라는 곳에서 하게 됩니다

 

오마이갓... 그제야 개안한 느낌이었다. DTO로 데이터의 형식을 정의하고, 그 정의를 기반으로 인스턴스를 만들어서 값을 집어넣은 다음 집어넣은 데이터들이 정의에 맞는지 아닌지를 class-validator로 확인한다는... 그런 것이었다. (맞나? 여튼...)

 

그리고 chat-GPT의 도움을 받아(ㅎ..) 아래와 같이 chart 데이터에 대한 DTO를 작성해 주었다.

import {
  IsArray,
  IsString,
  ValidateNested,
  IsNumber,
  IsOptional,
  IsInt,
  Min,
} from 'class-validator';

export class DatasetDTO {
  @IsArray()
  @IsNumber({}, {each: true})
  data: number[];

  @IsArray()
  @IsString({each: true})
  backgroundColor: string[];

  @IsOptional()
  @IsString()
  label?: string;

  @IsOptional()
  @IsArray()
  @IsString({each: true})
  borderColor?: string[];

  @IsOptional()
  @IsNumber()
  @IsInt()
  @Min(0)
  borderWidth?: number;
}

export class ChartDataPropsDTO {
  @IsArray()
  @IsString({each: true})
  labels: string[];

  @ValidateNested({each: true})
  @IsArray()
  datasets: DatasetDTO[];
}

tsconfig.json 에서 strictPropertyInitialization을 true로 해주었다면 아마 속성 ㅇㅇ은 이니셜라이저가 없고 생성자에 할당되어 있지 않습니다.라는 오류가 뜰 것이다. 아마 데코레이터에 지정되어 있지 않은 속성(내가 만든 것)이라 그런 것 같은데 그럴 때는! 를 붙여주면 된다.

@IsArray()
@IsNumber({}, {each: true})
data!: number[];

 

이제는 API로 전송된 값을 DTO를 통해 만들어주고, validate를 통해 검증해 보겠다!

그래서...! 여기서 알고 가야 하는 게 직렬화/역직렬화이다. 클래스, 객체, 인스턴스 세 가지의 개념이 있는데 간단하게 설명해 보자면

클래스(Class) | 객체를 만들어 내기 위한 설계도 혹은 툴.
객체(Object) | 소프트웨어 세계에 구현할 대상. 클래스에 선언된 모양 그대로 생성된 실체.

인스턴스(Instance) | 설계도인 클래스를 바탕으로 소프트웨어 세계에 구현된 구체적인 실체.

 

직렬화 (객체 -> 인스턴스)

객체를 직렬화하여 전송 가능한 형태로 만드는 것. 객체들의 데이터를 연속적인 데이터로 변형하여 stream을 통해 데이터를 읽도록 해준다
역직렬화 (인스턴스 -> 객체)
직렬화된 파일 등을 역으로 직렬화하여 다시 객체의 형태로 만드는 것을 의미한다. 저장된 파일을 읽거나 전송된 스트림 데이터를 읽어 원래 객체의 형태로 복원한다.

 

그리고 이런 직렬화, 역직렬화를 해주는 라이브러리가 class-transformer 이다.

validate를 이행하기 위해 DTO로 새로운 인스턴스를 만들어주는데, 내가 설정한 차트 데이터에서 dataset은 또 인스턴스로 설정이 되어있다. 따라서 값을 그대로 넣을 수 있는 labels와는 다르게 그 안에 값을 집어넣기 위해서는 객체형태가 아닌 인스턴스 형태로 넣어주어야 한다. 이때 class-transformer를 사용하는 것.

 

사용법은 아래와 같은데, 버전? 때문인지 plainToClass가 아니라 plainToInstance를 사용하는 듯하다.

// 직렬화 예시
let users = plainToInstance(User, userJson)

처음에 봤을 땐 뭐.,..User에 뭘 넣어야 하고 userJson에는 뭘 넣어야 하는 건데... 했는데 알고 보니 User에는 변형할 인스턴스의 형태를 가진 것(DTO라고 하자...)이고 userJson에는 객체의 값이 들어가면 되는 것이었다.

나는 plainToInstance만 사용했으니.. 여기서는 이것만 설명하겠다.

 

그리고 완성된 코드..! 😭

const chartDataBase: ChartProps = {
  labels: [],
  datasets: [
    {
      data: [],
      backgroundColor: [],
    },
  ],
};

const [data, setData] = useState<ChartDataPropsDTO>(chartDataBase);

useEffect(() => {
const setChartData = (data: ChartProps) => { // data는 객체의 형태로 들어온다
  const chartData = new ChartDataPropsDTO();
  chartData.labels = data.labels;
  chartData.datasets = plainToInstance(DatasetDTO, data.datasets); // DatasetDTO에 맞게 직렬화
  return chartData; // return 되는 chartData는 ChartDataPropsDTO형태의 인스턴스
};

const fetchData = async () => {
  const resolvedChartData = setChartData({ // 객체 들어가욧
    labels: ['Label 1', 'Label 2', 'Label 3'],
    datasets: [
      {
        data: [20, 30, 50],
        backgroundColor: ['red', 'green', 'blue'],
      },
    ],
  });

  const errors = await validate(resolvedChartData); // 그리고 인스턴스를 validate 한다
  if (errors.length > 0) throw new Error(JSON.stringify(errors));
  setData(resolvedChartData); // validate가 통과하면 차트 데이터를 set!
};

fetchData();
}, []);

 

처음 코드를 작성하고 나서는 이게 뭔지.. 맞는지... 잘 모르겠었는데 그래도 이렇게 정리해 보니까 쪼끔... 알 것 같기도?

사실 fetchData에서 resolvedChartData는 인스턴스의 형태일 텐데 차트에 넣어주는 data는 객체... 일 텐데.. 화면에 그려지긴 해서 상관이 없..나..? 싶으면ㅅㅓ도 이거 되면 안 되는 거 아냐..? 의 무한반복...

에러가 난다면 아..! instanceToPlain으로 변형해서 넣어주면 되겠구나! 하겠는데 오류 없이 잘.. 그려져서...

나중에 transformer로 한번 변형해서 넣어봐야겠다. ^^)9

'일하면서 공부해욧' 카테고리의 다른 글

Delete `␍` eslint (prettier/prettier)  (0) 2023.07.01
custom class validator  (0) 2023.06.25
node.js와 express  (0) 2023.06.18
React, TypeScript 그리고 Jest  (0) 2023.05.26
Jest  (0) 2023.05.26
작성일
2023. 5. 26. 18:29
작성자
sognociel

★테스트를 작성하는 이유★

우리가 작성한 코드가 생각한 시나리오에서 정상 동작함을 보장하기 위함이다.

물론! 우리가 생각하지 못한 상황에서에 대한 동작(알 수 없는 예외)에 대해서도 최소한의 예외 처리 등을 작성하여서 코드가 실행되는 애플리케이션이 의도치 않게 종료되는 것(의도된 종료와는 별개)은 막고 보장할 수 있어야 한다.

 

코드와 프로젝트는 생명주기라는 것이 존재하는데(업데이트, 실제 사용기간 등) 그 주기 내에서 발생할 수 있는 사이드 이펙트를 체크하고 그로 인해 발생할 수 있는 문제를 실제환경이 아니더라도 재현 및 선제적으로 체크할 수 있게 하기 위해서 테스트를 작성한다는 이유를 꼭!!! 생각하면서 테스트를 작성해 보자~

 

 

프로젝트 생성

$ npx create-react-app 프로젝트이름 --template typescript

프로젝트를 생성하면 자동으로 package.json에 jest-dom과 testing-library/react가 설치되어 있는 모습을 볼 수 있다.

 

src 디렉터리에는 이미 App.test.tsx라는 파일이 있는데... 이곳에서 테스트를 하면 되는 것 같다. 매번 react 프로젝트를 만들면 지웠던 파일인데... 역시 사람은 배운 만큼 보이나 보다...

맨날 지워서 미안하다...
npm test 결과

 

 

사칙연산 만들어 테스트해 보기

일단은 컴포넌트 테스트보다 심플한 사칙연산 함수 테스트 작성을 해보는 게 좋다고 하셔서 더하기..!부터 해봤다. 그래도 1시간 안으로는 할 줄 알았는데 조금... 헤매다 보니... 꽤 시간이 걸린 듯...😂

 

CalculatorBase.ts

export const addFunction = (a: number, b: number): number => {
  if (!Number(a)) throw new Error("addFunction. Not supported parameter. a must be a number");
  if (!Number(b)) throw new Error("addFunction. Not supported parameter. b must be a number");
  return a + b;
};

 

CalculatorBase.test.ts

import { addFunction } from "./CalculatorBase";

test("addFunction", () => {
  const a = 3;
  const b = 5;
  expect(addFunction(a, b)).toEqual(a + b);
});

처음에 계속 import부분에서 문제가 있다고 에러를 띄워주더니 뭐., 가.. 어떻게 된 건진 몰라도 갑자기... 또 실행이 되어서...  ....? ?... 하면서 그냥 하늘에 감사합니다 하고 있음...

 

 

 

비동기 테스트 코드

이번에는 비동기로 데이터를 받아와서, 받아온 값을 테스트해 보는? 코드를 작성해 보았다.

데이터는 jsonplaceholder에서 user 데이터를 가져옴!

 

Fetch.ts

export const fetchData = async (url: string): Promise<any> => {
  if (!url) throw new Error("API does not exist.");
  const response = await fetch(url);
  const data = await response.json();
  return data;
};

 

Fetch.test.ts

import { fetchData } from "./Fetch";

describe("jsonplaceholder user 데이터 가져오기", () => {
  test("데이터 가져오기 성공", async () => {
    const data = await fetchData("https://jsonplaceholder.typicode.com/users");
    const data1 = data[0];

    expect(data1.name).toBe("Leanne Graham");
  });
});

 

resolves와 rejects를 사용한 방법도 있다.

test("3초 후에 받아온 나이는 30", () => {
  return expect(함수).resolves.toBe(30);
});

test("3초 후에 에러가 납니다", () => {
  return expect(함수).reject.toMatch(설정한 에러메세지);
});

toBe(30)을 사용하면 에러가 나고, toMatch(에러메시지)를 하면 pass가 된다. 에러 처리를 한 대로 에러가 나왔기 때문.

 

 

 

테스트 전후 작업

beforeEach, afterEach

let num = 0

// num의 숫자를 각 테스트 시작 전에 0으로 초기화해준다.
beforeEach(() => {
  num = 0;
});

num을 쓰는 test1
num을 쓰는 test2
....

각각의 테스트 전, 후로 실행되는 함수로 변수의 값이 변동이 되지 않기 위해? 사용해 준다.

beforeAll, afterAll도 있는데 변수를 초기화하는 것과는 달리 유저 정보를 db에서 가져오고, 끊을 때 각각의 테스트마다 할 필요 없이 최초에 한 번, 마지막에 한 번씩만 하면 되기 때문에 이 경우에는 Each보다는 All을 써주는 게 좋다.

 

.only .skip

test.only로 테스트를 진행한다면 다른 test케이스는 스킵하고 only가 붙은 하나의 경우만 테스트를 진행한다.
반대로 test.skip으로 테스트를 진행한다면 해당 test 케이스만 스킵하고 전체적으로 테스트가 진행된다.

 

 

 

mock function

테스트하기 위해 흉내만 내는 함수로 jest.fn()으로 작성하면 된다.

가짜로 대체하는 이유 : 테스트하고 싶은 기능이 다른 기능들과 엮여있을 경우(의존) 정확한 테스트를 하기 힘들기 때문이다.
request body에 사용자의 id와 password를 넣어서 post요청을 보내는 경우, 실제 데이터베이스에 사용자의 id, password를 넣는 방식으로 테스트를 하는 것은 좋은 방법이 아니다. 실제 트랜잭션이 일어나기에 IO시간도 테스트에 포함되고, 데이터베이스 연결 상태에 따라 테스트가 실패할 수도 있기 때문이다.
테스트가 실패했을 경우 코드의 문제인지, 데이터베이스의 문제인지 알기 힘들기 때문에 올바른 단위테스트라고 할 수 없다.

 

 

jest 공식 문서에 나와있는 코드로 테스트를 해보았는데, mock함수에 어떤 것들이..? 담겨있는지 확인해 보는 게 좋을 것 같아서 추가로 작성해 본다.

 

forEach

export function forEach(items: number[], callback: (item: number) => number) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

숫자 배열을 보내면 배열 안의 숫자마다 콜백함수를 적용해서 값을 return 하는..? 것...이라고 생각하면 되려나...

 

forEach.test.ts

import { forEach } from "./MockFn";

const mockCallback = jest.fn((x) => 42 + x);

describe("Mock Test", () => {
  beforeEach(() => {
    forEach([0, 1], mockCallback);
  });

  // mock 함수는 두 번 호출됨
  it("forEach mock function toHaveLength", () => {
    expect(mockCallback.mock.calls).toHaveLength(2);
  });

  // forEach의 [0][0]은 0
  it("forEach mock function [0][0] toBe 0", () => {
    expect(mockCallback.mock.calls[0][0]).toBe(0);
  });

  // forEach의 [0][1]은 1
  it("forEach mock function [0][1] toBe 1", () => {
    expect(mockCallback.mock.calls[1][0]).toBe(1);
  });

  it("forEach mock function results", () => {
    console.log(mockCallback.mock);
    expect(mockCallback.mock.results[0].value).toBe(42);
  });
});

여기서 궁금한 게 공식문서에서는 마지막 expect가 42가 될 거라고 했는데, 막상 테스트를 진행해 보니 계속 에러가 나와서 도대체 mockCallback에는 어떤 것들이 있고, 콜백함수 안에 mock의 result는 무슨 값인가?! 싶어서 console.log로 확인해 보니 아래와 같았다...

 

mockCallback

// mockCallback
[Function: mockConstructor] {
  _isMockFunction: true,
  getMockImplementation: [Function (anonymous)],
  mock: [Getter/Setter],
  mockClear: [Function (anonymous)],
  mockReset: [Function (anonymous)],
  mockRestore: [Function (anonymous)],
  mockReturnValueOnce: [Function (anonymous)],
  mockResolvedValueOnce: [Function (anonymous)],
  mockRejectedValueOnce: [Function (anonymous)],
  mockReturnValue: [Function (anonymous)],
  mockResolvedValue: [Function (anonymous)],
  mockRejectedValue: [Function (anonymous)],
  mockImplementationOnce: [Function (anonymous)],
  mockImplementation: [Function (anonymous)],
  mockReturnThis: [Function (anonymous)],
  mockName: [Function (anonymous)],
  getMockName: [Function (anonymous)]
}

// mockCallback.mock
{
  calls: [ [ 0 ], [ 1 ] ],
  instances: [ undefined, undefined ],
  invocationCallOrder: [ 1, 2 ],
  results: [
    { type: 'return', value: undefined },
    { type: 'return', value: undefined }
  ],
  lastCall: [ 1 ]
}

result의 value가 왜 undefined인지..? instance는 왜 또 undefined가 나오는지...?😭

일단. mock 프로퍼티에는 호출되었던 값들이 고스란히 저장되어 있다.

그리고. mock의 프로퍼티 중. calls는 함수가 몇 번 호출되었는지, 호출될 때 전달된 인수는 무엇인지 알 수 있다.

 

 

 

MockReturnValues

mock 함수를 이용하여 테스트 중에 테스트 값을 코드에 입력할 수도 있다.

const myMock = jest.fn();
console.log(myMock());
// > undefined

myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true

처음 만들어진 mock 함수는 아무것도 가지고 있지 않지만, 추가적으로 값을 넣을 수 있는 것..!

mockReturnValueOnce는 값을 한 번만 넣을 때

mockReturnValue는 입력된 값이 호출할 때마다 계속 넣어진다.

 

아니 근데 왜 .mock.result만 확인해보려고 하면 요꼬라지...?

mockResolvedValue를 사용하면 비동기 함수를 흉내낼 수 있다! 실제로 API를 사용하지 않고 jest.mock(...) 함수를 사용하여 테스트할 수 있다는 것..!

import Chart from './Chart';

const chartMock = jest.fn();

describe('Chart Test', () => {
  it('들어온 값 테스트', async () => {
    chartMock.mockResolvedValue({male: 25, female: 75});
    const data = await doughnutMock();
    console.log(data);
    expect(data.male).toBe(25);
  });
});

 

'일하면서 공부해욧' 카테고리의 다른 글

Delete `␍` eslint (prettier/prettier)  (0) 2023.07.01
custom class validator  (0) 2023.06.25
node.js와 express  (0) 2023.06.18
class validator와 DTO, 그리고 직렬화/역직렬화  (0) 2023.06.11
Jest  (0) 2023.05.26
작성일
2023. 5. 26. 17:14
작성자
sognociel

Jest란?

Jest는 페이스북에서 만들어서 React와 더불어 많은 자바스크립트 개발자들로부터 좋은 반응을 얻고 있는 테스팅 라이브러리라고 한다. zero config라고 해서 별도의 설정 없이 빠르게 테스트 케이스를 작성할 수 있다는 장점을 가지고 있다.

 

 

 

Jest 설치

npm i -D jest

설치를 완료한 뒤, package.json의 test script를 jest로 수정해 주면 된다. 이후 npm test로 실행하여 테스트 코드를 실행하면 되는데, 이때 실행되는 파일들은 아래와 같다.

- 파일명.test.js 혹은 __tests__ 폴더 안에 있는 파일들

 

test코드 작성 방식

1. 실행 함수를 외부 파일에서 가져오지 않을 때

add.test.js

describe('계산 테스트', () => {
   const a = 1, b = 2;

   test('a + b는 3이다.', () => {
      expect(a + b).toEqual(3);
   });
});

/*
describe('그룹 테스트 설명 문자열', () => {
   const a = 1, b = 2; // 테스트에 사용할 일회용 가짜 변수 선언

   test('개별 테스트 설명 문자열', () => {
      expect(검증대상).toXxx(기대결과);
   });
});
*/

 

2. 함수를 외부 파일에서 가져올 때

fn.js

const fn = {
  add : (num1, num2) => (num1 + num2)
}

module.exports = fn;

fn.test.js

const fn = require('./fn')

test("2 더하기 3은 5", () => {
  expect(fn.add(2, 3)).toEqual(5)
})

 

 

 

Macher

Common

toBe | 숫자나 문자 등 기본타입값을 사용할 때

toEqual | 객체 또는 배열의 모든 필드를 재귀적으로 검사한다. 단, toEqual은 정의되지 않은 속성, 정의되지 않은 배열 항목, 배열 희소성 또는 객체 유형 불일치가 있는 객체 키를 무시하기 때문에, 이를 고려하려면 toStrictEqual을 사용해야 한다.

 

Truthiness

toBeNull | null 값인지
toBeUndefined | undefined인지
toBeDefined | toBeUndefined의 반대
toBeTruthy | true인지
toBeFalsy | false인지

 

Number

toBeGreaterThan | 크다
toBeGreaterThanOrEqual | 크거나 같다
toBeLessThan | 작다
toBeLessThanOrEqual | 작거나 같다

toBeCloseTo | 숫자를 더할 때 정수가 아니라 실수인 경우. toBe는 작동하지 않는다.

test("ID는 10자 이하여야 합니다.", () => {
  const ID = "TEST_TEST_TEST"
  
  expect(ID.length).toBeLessThanOrEqusl(10);
})

=> Error // ID가 10자 이상이기 때문에 오류가 난다

 

String

toMatch | 정규식에 대해 문자열을 확인

test('there is no I in team', () => {
  expect('team').not.toMatch(/I/);
});

test('but there is a "stop" in Christoph', () => {
  expect('Christoph').toMatch(/stop/);
});

대소문자 구분을 하기 때문에 구분 없이 확인을 하고 싶다면 정규표현식 뒤에 i를 붙이면 된다. ex) /a/i

 

Arrays and iterables

toContain | 배열이나 이터러블에 특정 항목이 포함되어 있는지

const shoppingList = [
  'diapers',
  'kleenex',
  'trash bags',
  'paper towels',
  'milk',
];

test('the shopping list has milk on it', () => {
  expect(shoppingList).toContain('milk');
  expect(new Set(shoppingList)).toContain('milk');
});

 

Exceptions

toThrow | 특정 함수가 호출될 때 오류를 발생시키는지 여부를 테스트

function compileAndroidCode() {
  throw new Error('you are using the wrong JDK!');
}

test('compiling android goes as expected', () => {
  expect(() => compileAndroidCode()).toThrow();
  expect(() => compileAndroidCode()).toThrow(Error);
});
카테고리
작성일
2023. 3. 12. 20:05
작성자
sognociel

Webpack 공식 홈페이지 : https://webpack.js.org/

 

웹팩을 설치하기 위해서는 진행하고 있는 프로젝트를 node.js의 패키지 프로젝트로 만들 필요가 있다.

$ npm init

 

유데미 Typescript: 섹션9 Drag&Drop 프로젝트

모든 HTTP 요청에는 베이스 오버헤드(base overhead), 지속(duration)이 발생한다.
실제 파일 다운로드는 비교적 빨리 진행되지만, 요청(request)을 설정하고, 서버에서 수행되는 것은 시간이 다소 걸리게 된다.

로컬 개발(local development) 서버에서 모든 것들은 우리 머신(machine) 상에서 이루어지고 있기 때문에 설정(setup)은 빠르게 진행되지만, 웹에서 실행하는 경우 이렇게 다수의 요청이 있다면 HTTP 요청의 양만으로 인해 다량의 대기시간이 발생하고 프로젝트가 느려질 수 있다.

이 때, 웹팩(Webpack)을 이용하여 요청의 양을 줄일 수 있다.

 

webpack을 이용하여 불필요한 http요청을 줄임

 

 

Normal Setup Webpack
여러 개의 타입스크립트 파일 & imports (HTTP 요청) 코드를 묶어서 import를 덜 할 수 있게 함
최적화되지 않은 코드 (최대한 작아진게 아니다) 코드를 최적화(작게) 하여 적은 코드를 다운로드할 수 있게 함
추가 개발 서버 필요 (여기서는 lite-server 사용) 개발 서버 쉽게 추가 가능

 

웹팩 (Webpack)

웹팩은 오픈 소스 자바스크립트 모듈 번들러로써 여러개로 나누어져 있는 파일들을 하나의 자바스크립트 코드로 압축하고 최적화하는 라이브러리이다.
파일을 묶고(bundling) 빌드하고(building), 종합하는(orchestration) 도구로 코드를 묶음으로써 HTTP 요청의 양을 줄이는 것을 도와준다.

  • 여러 파일의 자바스크립트 코드를 압축하여 최적화 할 수 있기 때문에 로딩에 대한 네트워크 비용을 줄일 수 있다.
  • 모듈 단위로 개발이 가능하여, 가독성과 유지보수가 쉽다.
  • 최신 자바스크립트 문법을 지원하지 않는 브라우저에서 사용할 수 있는 코드로 쉽게 변환시켜 준다.

 

설치방법

$ npm install webpack webpack-cli --save-dev

// 유데미 강의에서 설치한 코드
$ npm install --save-dev webpack webpack-cli webpack-dev-server typescript ts-loader

 

// package.json

"devDependencies": {
    "lite-server": "^2.5.4",
    "ts-loader": "^9.4.2",
    "typescript": "^4.9.5",
    "webpack": "^5.76.1",
    "webpack-cli": "^5.0.1",
    "webpack-dev-server": "^4.11.1"
  }

ts-loader | 웹팩에게 어떻게 코드를 자바스크립트로 변환할 것인지를 전달한다. 웹팩은 ts-loader의 도움을 받아 코드를 컴파일하고 (후드(hood) 하에서 컴파일러의 타입(type)을 사용함), 자바스크립트 파일을 1개의 묶인 bundle 코드 파일로 묶을 수 있게 된다.

typescript | 프로젝트마다 타입스크립트의 사본을 설치하는 게 글로벌로 설치하는 것보다 좋은 관행이다.
만약 타입스크립트 자체에 큰 변경사항이 있는 경우, 프로젝트를 위한 특정 버전의 타입스크립트로 고정되어 있다면 글로벌 버전과 충돌하지 않기 때문이다.

webpack | 코드를 묶고 변환하기 위한 특정 기능을 플러그인 하도록 해준다
webpack-cli | 프로젝트에서 웹팩 명령어를 실행하기 위해
webpack-dev-server | 개발 서버 내에서 빌트하기 위해서. 파일의 변경사항을 점검하고, 무언가 변경이 생겼을 때 웹페이지의 재컴파일을 시작한다.

 

웹팩이 필요한 정보.
1. 시작 지점(entry point) ex.) app.ts 등
2. 시작 지점 파일 내의 import 파악
3. ts-loader의 도움을 받아 파일 내용(contents) 파악

★ES 모듈을 사용했을 때는 import 경로에 .js 확장자를 붙였는데, 웹팩을 정확하게 동작시키기 위해서는 모든 import에서 .js 확장자를 삭제해야한다.

 

 

 

Entry(엔트리)
Output(출력)
Loaders(로더)
Plugins(플러그인)
Mode(모드)
Browser Compatibility(브라우저 호환성)

 

 

webpack dev server

매번 코드가 바뀔 때마다 npm run dev + 브라우저 새로고침을 하기에는 번거롭다. webpack dev server는 코드 변경을 자동으로 감지해서 웹팩을 자동으로 실행시켜주는데 웹팩을 실행한 결과 파일이 만들어지면 브라우저를 자동으로 리로드 해준다.

 

개발모드가 아닌 배포모드용 webpack.config.prod.js 파일 만들기

$ npm install --save-dev clean-webpack-plugin

프로젝트를 다시 빌드할 때마다 dist 폴더를 지우고 최신 상태를 유지하게 하는 것

 

webpack.config.prod.js

const path = require("path");
const CleanPlugin = require("clean-webpack-plugin");

module.exports = {
  mode: "production",
  entry: "./src/app.ts",
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
  devtool: "none",
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: [".ts", ".js"],
  },
  plugins: [new CleanPlugin.CleanWebpackPlugin()],
};

 

웹팩5로 버전 업그레이드가 되면서 package.json script에 webpack-dev-server가 아니가 webpack serve라 쓰고, config 파일에 devServer 설정을 추가해 주어야 한다.

그리고 실행을 했는데... 계속 오류가 나서... 아마 강의랑 지금 버전 차이 문제인 것 같은데 해결 방법을 계속 찾아보고는 있지만 쉽지가 않다..🫠 언제쯤 해결할 수 있을까... prod 버전도 되질 않아서 뭘 참고해서 에러를 해결해야 하는지.. 정말 모르겠음... 다른 웹팩 강의들 이것저것 다 보고 있는데 다.. 안된다...

카테고리
작성일
2023. 2. 27. 18:07
작성자
sognociel

유데미 【한글자막】 Typescript :기초부터 실전형 프로젝트까지 with React + NodeJS 강의와 inpa님 블로그를 참고하여 작성..!

inpa님 블로그 | 링크

 

1. 데코레이터

데코레이터는 코드 조각을 장식해주는 역할을 하며 타입스크립트에서 그 기능을 함수로 구현할 수 있다.

예를들어 메소드 / 클래스 / 프로퍼티 / 파라미터 위에 @함수 를 장식해줌으로써, 코드가 실행(런타임)이 되면 데코레이터 함수가 실행되어, 장식한 멤버를 보다 파워풀하게 꾸며주는 것으로 이해하면 된다.

 

TypeScript 공식 문서 설명

「TypeScript 및 ES6에 클래스가 도입됨에 따라, 클래스 및 클래스 멤버에 어노테이션을 달거나 수정하기 위해 추가 기능이 필요한 특정 시나리오가 있습니다. 데코레이터는 클래스 선언과 멤버에 어노테이션과 메타-프로그래밍 구문을 추가할 수 있는 방법을 제공합니다. 데코레이터는 JavaScript에 대한 2단계 제안이며 TypeScript의 실험적 기능으로 이용 가능합니다.」

 

  • 데코레이터는 클래스 선언, 메서드(method), 접근자(accessor), 프로퍼티(property) 또는 매개변수(parameter)에 첨부할 수 있는 특수한 종류의 선언
  • 데코레이터 함수에는 target(현재타겟), key(속성이름), descriptor(설명)가 전달 (단, 어떤 멤버를 장식했느냐에 따라 인수가 달라짐)
  • 메소드나 클래스 인스턴스가 만들어지는 런타임에 실행된다. 즉, 매번 실행되지 않는다.
  • 데코레이터는 클래스 또는 클래스 내부의 생성자, 프로퍼티 , 접근자, 메서드, 그리고 매개변수에만 장식될 수 있다.
function readonly(writable: boolean) {
   return function (target: any, decoratedPropertyName: any): any {
      return {
         writable: !writable,
      };
   };
}

class Test {
   property = 'property';

   @readonly(false)
   public data1 = 0;

   @readonly(true)
   public data2 = 0;
}

const t = new Test();
t.data1 = 1000;
t.data2 = 1000; // 런타임 에러 !! - data2는 writable이 false라서 값을 대입할 수가 없다.

데코레이터 패턴은 클래스를 수정하지 않고 클래스의 멤버들의 정의를 수정 및 기능을 확장할 수 있는 구조적 패턴의 하나이다. 데코레이터 패턴을 사용하면 전체 기능에 신경 쓰지 않고 특정 인스턴스에 초점을 맞출 수 있다.

 

 

데코레이터를 활성화하려면 명령줄 또는 tsconfig.json에서 실험적 데코레이터 컴파일러 옵션을 활성화해야 한다.

$ tsc --target ES6 --experimentalDecorators

 

tsconfig.json 파일 수정

{
  "compilerOptions": {
    "target": "ES6",
    "experimentalDecorators": true
  }
}

 

데코레이터는 @expression 형식을 사용하는데, 여기서 expression은 데코레이팅 된 선언에 대한 정보와 함께 런타임에 호출되는 함수여야 한다. (함수 이름... 적으면 됨. 보통 첫 글자는 대문자로 작성한다고 하는데 소문자도 상관없다고 한다)

function Logger(constructor: Function) {
  console.log('Logging...');
  console.log(constructor);
}

@Logger
class Person {
  name = 'Max';

  constructor() {
    console.log('Creating person object...');
  }
}

const pers = new Person();

console.log(pers);

 

 

 

2. 데코레이터 팩토리

데코레이터 팩토리 함수는 데코레이터 함수를 감싸는 래퍼 함수로 보통 데코레이터가 선언에 적용되는 방식을 원하는 대로 바꾸고 싶을때 사용된다. 데코레이터 팩토리는 단순히 데코레이터가 런타임에 호출할 표현식을 반환하는 함수이다.

프로그래밍에서 함수에게 사용자가 인자를 전달할 수 있는 것과 유사하게, 데코레이터 함수 또한 팩토리를 사용해 사용자로부터 인자를 전달 받도록 설정할 수 있다.

 

일반적으로 데코레이터에서는 팩토리를 만들어 사용하고 있기 때문에 거의 디폴트처럼 사용된다고 보면 된다.

// 데코레이터 팩토리
function Logger(logString: string) {
  // 데코레이터 함수
  return function (constructor: Function) {
    console.log(logString);
    console.log(constructor);
  };
}

@Logger("LOGGING - PERSON")
class Person {
  name = "Max";

  constructor() {
    console.log("Creating person object...");
  }
}

const pers = new Person();

console.log(pers);

 

 

 

3. 데코레이터 합성

선언에 여러 개의 데코레이터를 사용할 수 있다.

 

그렇다면 데코레이터들의 실행 순서는?

function Logger(logString: string) {
  console.log("Logger return 앞");
  return function (constructor: Function) {
    console.log(logString);
    console.log(constructor);
  };
}

function WithTemplate(template: string, hookId: string) {
  console.log("WithTemplate return 앞");
  return function (_: Function) { //_를 입력해서 존재는 알지만 쓰지 않겠다고 명시하는것
    console.log(hookId);
    const hookEl = document.getElementById(hookId);
    if (hookEl) {
      hookEl.innerHTML = template;
    }
  };
}

@Logger("logString")
@WithTemplate("<h1>My Person Object</h1>", "app")

Logger, WithTemplate 두 개의 데코레이터가 있고, class 앞에서 두 데코레이터를 호출하면 아래와 같은 결과가 나온다.

빨간 선을 기준으로 먼저 데코레이터들의 return부 앞에 있는 코드가 실행되고, 그 이후에는 데코레이터의 return부가 역순으로 실행되는 모습을 볼 수 있다.

 

// Size 데코레이터 팩토리
function Size() {
   console.log('Size(): 평가됨');
   // Size 데코레이터
   return function (target: any, prop: string, desc: PropertyDescriptor) {
      console.log('Size(): 실행됨');
   };
}
// Color 데코레이터 팩토리
function Color() {
   console.log('Color(): 평가됨');
   // Color 데코레이터
   return function (target: any, prop: string, desc: PropertyDescriptor) {
      console.log('Color(): 실행됨');
   };
}
// Button 클래스 정의
class Button {
   // 메서드에 멀티 데코레이터 적용
   @Size()
   @Color()
   isPressed() {}
}

Size(): 평가됨
Color(): 평가됨
Color(): 실행됨
Size(): 실행됨

순으로 출력된다.

 

4. 클래스 데코레이터 (Class Decorators)

클래스 데코레이터는 클래스 선언 직전에 선언된다. 클래스 데코레이터는 클래스 생성자에 적용되며 클래스 정의를 관찰, 수정 또는 교체하는 데 사용할 수 있다. 클래스를 수정하거나 교체한다는 것은, 클래스 데코레이터는 클래스를 반환할 수가 있는데 옛 클래스를 대체, 확장할 수 있다는 것이다.

 

클래스 반환 클래스 데코레이터의 예시

function reportableClassDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
  return class extends constructor { // 기존의 클래스에서 reportingURL이라는 요소 추가
    reportingURL = "http://www...";
  };
}
 
@reportableClassDecorator
class BugReport {
  type = "report";
  title: string;
 
  constructor(t: string) {
    this.title = t;
  }
}
 
const bug = new BugReport("Needs dark mode");
console.log(bug.title); // Prints "Needs dark mode"
console.log(bug.type); // Prints "report"
 
bug.reportingURL; // Error! Property 'reportingURL' does not exist on type 'BugReport'.
(bug as any).reportingURL // 이렇게 접근할 수 있다.

데코레이터가 확장된 클래스를 반환하지만, 클래스 자체를 바꾸는 것은 아니기 때문에 bug에는 reportingURL 속성이 없다. (bug as any).reportingURL 로 접근이 가능하다.

 

 

 

5. 메서드 데코레이터 (Method Decorators)

메서드 데코레이터는 메서드 선언 직전에 선언 된다. 메서드 관찰, 수정 또는 대체하는 데 사용할 수 있으며 가장 많이 이용되는 데코레이터라고 한다. 클래스 데코레이터는 클래스(생성자 함수)를 extends 하는 방법으로 기능을 확장할 수 있었지만, 메서드 데코레이터는 메서드의 Property Descriptor를 수정하여 메서드를 확장한다.

 

메서드 데코레이터는 총 3개의 인자를 전달받는다.

  • 첫 번째 argument : static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 prototype 객체
  • 두 번째 argument : 해당 method의 이름
  • 세 번째 argument : 해당 method의 property descriptor 
function methodDecorator(
  target: any, // static 메서드라면 클래스의 생성자 함수, 인스턴스의 메서드라면 클래스의 prototype 객체
  propertyKey: string, // 메서드 이름
  descriptor: PropertyDescriptor // 메서드의 Property Descriptor
) {
	... 
} // return 값 무시됨

 

만약 해당 메서드가 호출될 때 특정 무엇인가를 동작시키고 싶다면 데코레이터에서 descriptor를 다음과 같이 재정의 해주면 된다.

Property Description
value 현재 값 value
writable 수정 가능하면 true, 아니면 false
enumarable for (i in [1, 2, 3])과 같이 순회가 가능하면 true, 아니면 false
configurable Property definition이 수정 및 삭제가 가능하면 true, 아니면 false

 

function methodDecorator() {
   return function (target: any, property: string, descriptor: PropertyDescriptor) {

      // descriptor.value는 test() 함수 자체를 가리킨다. 이 함수를 잠시 변수에 피신 시킨다.
      let originMethod = descriptor.value; 

      // 그리고 기존의 test() 함수의 내용을 다음과 같이 바꾼다.
      descriptor.value = function (...args: any) {
         console.log('before');
         originMethod.apply(this, args); // 위에서 변수에 피신한 함수를 call,apply,bind 를 통해 호출
         console.log('after');
      };
   };
}

class Test {
   property = 'property';
   hello: string;

   constructor(m: string) {
      this.hello = m;
   }

   @methodDecorator()
   test() {
      console.log('test');
   }
}

let test = new Test("world")
test.test()
[Running] ts-node "index.ts"
before
test
after

 

데코레이터 인수의 descriptor.value 로 접근하면 장식된 함수 자체(Test 클래스의 test 메소드)로 접근하게 되는데 이를 잠시 백업하고 나중에 실행하는 식으로 응용이 가능하다. 백업된 메서드를 호출할 때 apply를 이용하여 this를 바인딩해줘야 한다. 여기서 this는 메서드를 호출하는 객체를 의미하는데, 만일 this를 바인딩하지 않으면 해당 메서드를 어느 객체가 호출 했는지 알 수 없다.


 

6. 프로퍼티 데코레이터 (Property Decorators)

프로퍼티 데코레이터는 프로퍼티 선언 바로 전에 선언 된다. 메서드 데코레이터와 다르게 데코레이터 함수에 Property Descriptor 가 인자로서 제공되지 않는다는 차이가 있다. 

function propertyDecorator(
  target: any, // static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 prototype 객체
  propName: string, // 프로퍼티 이름
) {
	...
} // return하는 값이 Property Descriptor 형태 혹은 void.
function Log(target: any, propertyName: string | Symbol) {
  console.log("Property Decorator");
  console.log(target, propertyName);
}

class Product {
  @Log
  title: string;
  private _price: number;

  set price(val: number) {
    if (val > 0) {
      this._price = val;
    } else {
      throw new Error("Invalid pricd - should be positive");
    }
  }

  constructor(t: string, p: number) {
    this.title = t;
    this._price = p;
  }

  getPriceWithTax(tax: number) {
    return this._price * (1 + tax);
  }
}

target은 title

 

 

 

7. 접근자 데코레이터 (Accessor Decorators)

접근자 데코레이터는 접근자 선언 바로 전에 선언되며 getter, setter 함수의 앞에 사용하는듯하다.

function configurable(value: boolean) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.configurable = value;
  };
}

class Point {
  private _x: number;
  private _y: number;
  constructor(x: number, y: number) {
    this._x = x;
    this._y = y;
  }

  @configurable(false)
  get x() {
    return this._x;
  }

  @configurable(false)
  get y() {
    return this._y;
  }
}

 

 

 

8. 매개변수 데코레이터 (Parameter Decorators)

매개변수 데코레이터는 함수의 매개변수 왼쪽 옆에 명시해 주면 된다.(가로로 써도 인식되기 때문에...)

프로퍼티나 접근자 데코레이터와는 다르게 세 번째 인자를 descriptor가 아니라 parameterIndex로 받는다.

function parameterDecorator(
  target: any, // static 메서드의 파라미터 데코레이터라면 클래스의 생성자 함수, 인스턴스의 메서드라면 prototype 객체
  methodName: string, // 매개변수가 들어있는 메서드의 이름
  paramIndex: number // 매개변수의 순서 인덱스
) {
    ...
} // 리턴값은 무시됨

 

function parameterDecorator(target: any, methodName: string, paramIndex: number) {
   console.log('parameterDecorator start');
   console.log(target);
   console.log(methodName);
   console.log(paramIndex);
   console.log('parameterDecorator end');
}

class Test7 {
   private _name: string;
   private _age: number;

   constructor(name: string, @parameterDecorator age: number) {
      this._name = name;
      this._age = age;
   }

   print(@parameterDecorator message: string) {
      console.log(message);
   }
}

카테고리
작성일
2023. 2. 26. 22:26
작성자
sognociel

1. 인터섹션 타입

두가지의 타입을 결합하여 다른 타입을 만들 수 있다.

 

type 사용

type Admin = {
  name: string;
  privileges: string[];
};

type Employee = {
  name: string;
  startDate: Date;
};

type ElevatedEmployee = Admin & Employee;

const e1: ElevatedEmployee = {
  name: "Max",
  privileges: ["create-server"],
  startDate: new Date(),
};

 

interface 사용

interface Admin {
  name: string;
  privileges: string[];
};

interface Employee {
  name: string;
  startDate: Date;
};

interface ElevatedEmployee extends Admin, Employee {};

 

union 타입을 사용하여 인터섹션 타입을 만들면 두 가지 타입에서 공통된 타입만 인식한다.

type Combinable = string | number;
type Numeric = number | boolean;

type Universal = Combinable & Numeric;

number만 인식되는 모습

 

 

 

2. 타입 가드

유니온 타입이 지닌 유연성을 활용할 수 있게 해준다.

특정 속성이나 메소드를 사용하기 전에 그것이 존재하는지 확인하거나 타입을 사용하기 전에 이 타입으로 어떤 작업을 수행할 수 있는지를 확인하는 개념 또는 방식을 나타내는 용어이다. 클래스의 경우 instanceof, 객체의 경우 in을 사용하여 수행할 수 있고 다른 타입들의 경우 typeof를 사용할 수 있다.

 

type priceType = string | number;
let totalPrice: number;
let itemPrice: priceType = "1000";

if (typeof itemPrice === "string") {
  totalPrice = parseInt(itemPrice);
} else {
  totalPrice = itemPrice;
}

전형적인 타입 가드 방법. typeof로 price가 string 인지 number 인지 판별 후 각 상태에 따라 다르게 처리하는 로직을 구현한다.

 

type Admin = {
  name: string;
  privileges: string[];
};

type Employee = {
  name: string;
  startDate: Date;
};

type UnknownEmployee = Employee | Admin;

function print(emp: UnknownEmployee) {
  console.log(emp.name);
  if ("privileges" in emp) {
    console.log(emp.privileges);
  }
  if ("startData" in emp) {
    console.log(emp.startData);
  }
}

 

 

UnknownEmployee는 Employee 또는 Admin 타입을 가진다. 따라서 privileges가 있을 때, 없을 때가 있고, startDate가 있을 때, 없을 때가 있다. 해당 속성들에 접근하기 위해 자바스크립트의 in을 사용한다.

emp에 privileges가 없는 객체를 전달한다면 Admin 타입을 가졌으니 이름과 srateDate를 콘솔에 출력하게 되고, startDate가 없는 객체를 전달한다면 Employee 타입을 가져 이름과 privileges를 콘솔에 출력하게 된다.

 

 

instanceof

클래스의 타입을 감지하는 역할. 객체가 클래스에 기반하는지 확인할 수 있고, 클래스에 기반한다면 해당 클래스의 메소드를 가지고 있는지 확인할 수 있다. 인터페이스에서는 작동하지 않는데 인터페이스는 자바스크립트로 컴파일되지 않기 때문이다.

// Car와 Truck 각각의 class들... Truck에는 loadCargo라는 메소드 존재.
// Car와 Truck를 interface로 정의했다면 작동하지 않는다.
type Vehicle = Car | Truck;

function useVehicle(vehicle: Vehicle) {
  if (vehicle instanceof Truck) {
    vehicle.loadCargo(1000);
  }
}

 

 

 

3. 구별된 유니언 (discriminated)

구별된 유니언을 구성하는 모든 객체에는 하나의 공통 속성만 있고 유니언은 해당 속성을 설명하므로 객체를 설명하는 이 속성을 switch 문에 사용하여 완전한 타입 안전성을 갖추고 객체에 어떤 속성을 사용할 수 있는지 파악할 수 있다. 객체와 유니언 타입을 사용한 작업 시 사용할 수 있는 아주 유용한 패턴이라고 한다.

// type도 가능
interface Bird {
  type: 'bird';
  flyingSpeed: number;
}

interface Horse {
  type: 'horse';
  runningSpeed: number;
}

type Animal = Bird | Horse;

function moveAnimal(animal: Animal) {
  let speed;
  switch (animal.type) {
    case 'bird':
      speed = animal.flyingSpeed;
      break;
    case 'horse':
      speed = animal.runningSpeed;
  }
  console.log('Moving at speed: ' + speed);
}

moveAnimal({type: 'bird', flyingSpeed: 10});

 

 

 

4. 형 변환(typecasting)

타입스크립트가 직접 감지하지 못하는 특정 타입의 값을 타입스크립트에 알려주는 역할을 한다. 타입스크립트에 특정 값이 특정 타입임을 알리려고 하는 것.

 

dom에 있는 무언가로 접근할 때 (ex. const paragraph= document.querySelector('p')) 타입스크립트는 HTML파일을 분석하지 못하기 때문에 단순히 HTML요소나 null로 추론하게 된다.
만약 text타입의 input에 접근한 후 (const userInputElement=document.getElementById('user-input')) userInputElement.value에 새로운 값을 할당하려고 한다면 객체가 null일 수 있다는 에러가 발생하게 된다.

이 때 형 변환을 통해 에러를 해결할 수 있다.

const userInputElement = <HTMLInputElement>document.getElementById('user-input')!;
const userInputElement = document.getElementById('user-input')! as aHTMLInputElement;

userInputElement.value = "Hi, There!";

두 가지 방법 중 하나를 선택해서 사용하면 된다.

 

cf.) userInputElement의 느낌표는?

느낌표를 사용하여 느낌표 앞의 표현식을 null로 반환(yield)하지 않겠다고 타입스크립트에게 인식시킬 수 있다.

 

 

 

5. 인덱스 속성

객체가 지닐 수 있는 속성에 대해 보다 유연한 객체를 생성할 수 있게 해주는 기능.

사용자 입력의 유효성을 검사하는 애플리케이션을 작성하는 경우 여러 입력 필드가 필요할 것이며 사용자의 관심사가 무엇이며 어떤 필드인지에 따라 각기 다른 에러 메시지를 저장해서 보여주고자 한다면, 각 상황에 적합한 에러 메시지를 에러 컨테이너에 추가해야 한다.

interface ErrorContainer {
  [prop: string]: string;
}

const errorBag: ErrorContainer = {
  email: "Not a valid email!",
  username: "Must start with a capital character!",
};

인덱스 타입을 키:string : 값:string으로 정했기 때문에 만들어지는 객체의 키, 값은 이를 따라야 한다.

 

 

 

6. 함수 오버로드

동일한 함수에 대해 여러 함수 시그니처를 정의할 수 있는 기능으로, 다양한 매개변수를 지닌 함수를 호출하는 여러 가지 가능한 방법을 사용하여 함수 내에서 작업을 수행할 수 있게 해준다.

타입스크립트가 자체적으로 반환 타입을 정확히 추론하지 못하는 경우에 유용하며 함수에서 지원할 수 있는 다양한 조합에 대해 어떤 것이 반환되는지 명확하게 알 수 있다.

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: string | number, b: string | number) {
  if (typeof a === "string" || typeof b === "string") {
    return a.toString() + b.toString();
  }
  return a + b;
}

 

 

 

7. 선택적 체이닝

백엔드에서 데이터를 가져오고 데이터베이스나 객체 내 특정 속성이 정의되어 있는지 확실하지 않은 소스에서 데이터를 가져오는 애플리케이션이 있다고 가정했을 때, 정의되어 있는지 여부가 확실치 않은 요소 다음에 물음표를 추가하여 에러를 막는 방법이다.

 

const fetchUserData = {
  id: "u1",
  name: "Max",
  job: { title: "CEO", description: "My own company" },
};

console.log(fetchUserData?.job?.title);

백엔드 데이터에서 가져온 fetchUserData에서 job이 없는 경우 에러가 발생하게 되는데, 물음표를 통해 정의되어 있는지의 여부를 확인한다고 보면 된다. 이 부분은 데이터에 접근하기 전에 데이터의 존재 여부를 확인하는 if 문으로 컴파일된다.

 

 

 

8. Null 병합

어떤 데이터나 입력값이 있는데 그것이 null 인지, undefined 인지, 유효한 데이터인지 알 수 없는 경우 폴백(fallback) 값 (디폴트 값...이라고 생각하면 될 것 같다) 을 저장하고자 할 때, || 연산자를 사용하면 폴백 값이 할당되게 된다.

const userInput = " ";
const storedData = userInput || "DEFAULT";

// 이 경우 storeData는 빈 문자열을 null로 인식해 DEFAULT 값을 가지게 된다.

 

 

??는 null 병합 연산자로, 빈 문자열이나 0이 아닌 null이나 undefined 둘 중 하나라면 폴백을 사용하고 싶을 때 사용하면 된다.