-
NodeJS - Typescript를 활용한 express서버 구축개발언어/TypeScript 2024. 3. 18. 11:18
TypeScript를 활용한 express 서버 구축
ts 파일 node 명령어로 실행하기
위와 같이 간단한 디렉토리 구조에 ts 파일을 생성하고 안에는 간단한 console.log를 찍는 코드를 적는다.
그 다음에 해당 파일을 node 명령어로 실행하면 보통의 js 파일을 실행하듯 실행해도 ts 파일의 코드대로 로그가 찍히는 것을 확인할 수 있다. 그러면 노드로 타입스크립트를 실행할 수 있는 것 일까? → 🙅♀️그렇지는 않다.
노드는 파일 확장자를 신경 쓰지 않고 파일안의 내용을 일반적인 자바스트립트로 처리한다. 위 코드에서는 타입스크립트의 기능이 하나도 없기 때문에 실행된 것이다.
Typescript의 기능이 추가된 ts파일 node 명령어로 실행
이번에는 타입스크립트의 기능을 활용한 코드를 작성한 파일이다.
이때 해당 파일을 실행시키면 오류가 발생하는 것을 확인할 수 있다. 즉 노드가 타입스크립트를 파싱하고 컴파일 할 수 있는 게 아니라 단지 모든 파일을 실행할 때 자바스크립트처럼 처리하는 것이다. ts파일이어도 해당 파일에 타입스크립트의 기능을 사용하는 코드가 없으면 정상실행 되지만 그렇지 않다면 오류를 발생시키는 것이다. 중요한 포인트는 노드는 타입스크립트를 실행하지 않는다는 것이다.
ts-node 패키지와 실행환경
https://github.com/TypeStrong/ts-node
이와 관련해서 ts-node라는 패키지를 설치할 수 있다. 이 패키지는노드 실행 파일과 결합된 타입스크립트 컴파일러를 제공한다. 해당 패키지를 전역에 설치해서 node를 사용해 타입스크립트 파일을 실행할 수 있다. 해당 패키지가 자체적으로 컴파일을 실행하고 코드를 실행한다. 요약하자면 tsc와 노드 단계를 하나로 결합해 주는 도구이다.
하지만 개발단 계에서는 이것을 사용할 수 있지만 프로더견에서 파일을 실제로 웹 서버나 웹 호스트에서 제공할 때는 부적합 할 수 있다. 코드를 실행 할때마다 추가적인 컴파일 단계를 거쳐야 한다면 추가 오버헤드가 발생하기 때문이다.
그래서 앞으로 하게될 실습에서도 ts-node를 사용하지 않고 노드에서 타입스크립트 파일을 실행하지 않는다. 대신 express.js를 활용해 만든 웹 어플리케이션, 즉 REST AP를 개발시에는 typeScript를 사용하되 실행시점에는 바닐라 Node.js만으로 빌드할 수 있는 걸정을 소개한다.
프로젝트 구성
해당 프로젝트에서는 타입스크립트로 작성된 파일을 tsc 명령으로 컴파일하고 node 명령으로 컴파일된 파일을 실행할 것이다.
yarn init -y //프로젝트 설정 초기화
tsc --init //타입스크립트 관련 설정 초기화
우선 yarn init을 사용해서 package.json파일을 만든다.
tsconfig.josn
{ "compilerOptions"{ "target":"es2018", "module":"commonjs", "moduleResolution":"node", "outDir":"./dist", "rootDir":"./src" } }
- moduleResolution :속성은 여러 파일과 import가 어떻게 상호작용하는지 타입스크립트에 알려주는 것이다.
- outDir : 컴파일된 js파일이 저장될 곳
- rootDir : 개발하면서 작성할 ts 파일이 위치할
express 패키지와 src 디렉토리 생성
yarn add express body-parser nodemon
src 디렉토리에 ts로 작업한 프로젝트의 소스 코드파일 들이 올것이고 이것을 build해서 js파일로 컴파일한 후 이것을 dist 디렉토리에 넣을 것이다.
프로젝트에 서버를 구동시킬 express 패키지와 수신된 요청 본문을 파싱할 body-parser 패키지를 설치해 준다.
마지막으로 nodemon패키지를 설치해준다. 이 패키지는 노드로 파일을 실행 시킬 뿐 만 아니라 파일과 파일이 들어 있는 폴더 또 그 위 폴더에서 변경 사항이 있는지 모니터링 한다. 그리고 변경 사항이 있을때 마다 노드 서버를 다시 시작한다.
express서버 코드 작성
위 코드는 일반적으로 자바스크립트에서 구동하는 방식이지만 ts 파일에 적용하려는 사용하려고면 require()에 오류가 발생하는 것을 확인할 수 있다. 왜냐하면 require()함수는 Node.js에 있는 함수이지만 브라우저에서는 사용할 수 없다. 또 tsconfig.json 파일을 보면 Node.js를 지원하는 lib를 명시하고있지 않다. 그래서 타입스크립트는 Node.js환경에 있는 일반 함수를 인식하지 못한다. 그래서 @type/node 패키지를 설치한다.
@types/node
yarn add @types/node --dev
@type/node를 설치하면 Node.js에 사용하는 모든 타입을 typeScript에서도 사용할 수 있다.
하지만 여전히 app.뒤에 커서가 활성화 되어도 자동추천 기능이 활성화되지 않는 등 정상적인 동작되지 않고 있는 것을 확인할 수있다. 그래서 @type/node를 설치해준것과 같은 이유로 @type/express도 설치해주어야 한다.
@types/express
yarn add @types/express --dev
위 패키지를 설치하고 나서도 express를 import하고 있는 구문을 고쳐주어야 적상적인 작동을 한다. require를 사용해서 설치된 패키지를 가져오는건 Node.js가 기본적으로 이용하는 CommonJS 임포트 구문이다. 하지만 지금은 타입스크립트 환경에 있고 타입스크립트 환경에서는 다른 임포트 구문을 사용한다.
바로 브라우저의 ES 모듈과 동일한 임포트 구문을 사용하는 것이다. 그 제서야 app위에 커서를 가져가면 app에 Express라는 타입이 반환되면서 타입스크립트가 정상적으로 동작하는 것을 확인할 수 있다.
express 서버 시작
ts에서 import구문이 js와 다르기 때문에 ts파일이 js파일로 컴파일되면 js원래의 import구문을 사용하는 코드로 돌아간다.
tsc -w
우선 위 명령어를 사용해 감시모드를 켠다. 그럼 컴파일이 시작되고 dist 폴더안에 server.js파일이 생긴다.
이때 컴파일해서 생성된 js파일을 보면 require문을 사용해서 express 패키지를 임포트하고 있는 것을 확인할 수 있다. 실제로는 컴파일된 js파일이 위 코드가 실행된는 것이다. 위 코드는 nodemon으로 실행할 수 있다.
터미널에서 컴파일 프로세스가 진행 중인 탭은 그대로 두고 새로운 터미널 탭을 연다.그리고 새탭에서 nodemon으로 서버를 시작한다.
"scripts": { "start": "nodemon dist/server.js" },
package.json 파일로가서 위와 같이 새 스크립트 start를 추가한다. dist 폴더의 app.js를 실행해서 서버를 구동하는 것이다.
yarn start
그 다음 위 명령어를 실행하면 정상적으로 서버가 실행되는 것을 확인 할 수 있다
Todo API 기능 구현
routes 등록
일단 기본적인 서버를 가동했지만 해당 서버는 아무런 작업을 하고 있지 않다. 여기에 추가적인 CRUD를 구현하기 위해 routes폴더를 생성한다.
todos.ts 파일에서 Express.js를 이요해 몇 개의 라우팅을 등록한다.
import { Router } from 'express'; const router = Router();
처음으로 Router를 임포트하고 나면 함수로 Router()를 호출해서 미들웨어를 등록할 수 있다. 이를 통해 라우팅 엔드포인트에서 요청을 수신하고 이러한 요청에 맞는 로직을 실행할 수 있어야 한다.
todo.ts
import { Router } from 'express'; const router = Router(); /** * 새로운 toDo를 생성한다. */ router.post('/',); /** * 존재하는 모든 toDo를 가져온다. */ router.get('/',); /** * 기존의 toDo를 수정 */ router.patch('/:id',); /** * 기존의 toDo를 삭제 */ router.delete('/:id'); export default router;
위 코드와 같이 기본적으로 사용할 라우터의 기본 골조를 만들고 이 파일에서 구성되 라우터를 export deault로 내보낸다. 아직은 해당 라우팅 엔드 포인트에 도달할 때 어떤 함수도 실행 시켜주지 않았기 때문에 해당 라우터를 호출해도 어떤 동작도 일어나지 않는다.
라우터를 실행 중인 서버에 연결
import express, { Request, Response } from 'express' import todoRoutes from './routes/todos' import { NextFunction } from 'connect'; const app = express(); app.use('/todo',todoRoutes); // 오류 핸들링 app.use((err: Error , req: Request, res: Response, next: NextFunction) =>{ res.status(500).json({message:err.message}); }) app.listen(4500);
server.ts에 임포트해온 todoRoutes를 express 애플리케이션에 연결하기 위해 app.use()를 이용한다. "/todos"로 시작하는 모든 요청을 todoRoutes로 전달한다. 위 처리에 대해 덧붙이자면 현재 이용 중인 익스프레스 미들웨어 설정에서는 오류를 처리하는 미들웨어도 추가할 수 있다.
라우터를 등록한 코드 다음에 app.use()를 사용하여 오류를 처리하는 코드를 등록한다. 이 앞의 다른 미들웨어서 오류가 발생하면 위해 express가 자동으로 이 함수를 실행 할 것이다.
controller 구현
controllersdp todos.ts 파일을 추가해서 라우터에서 연결해주어야 하는 컨트롤이나 함수를 담을 것이다.
매개변수 type 지정
import { NextFunction, Request } from "express"; export const createTodo = (req:Request, res:Response, next: NextFunction) => {};
RequestHandler
import { RequestHandler } from "express"; export const createTodo:RequestHandler = (req, res, next) => {};
createTodo 함수는 익스프레스의 미들웨어 함수가 사용하는 일반적인 매개변수를 받을 것이다. 하지만 일일히 수작없으로 받게된는 매개변수의 type을 지정해 주는건 번거롭고 지루한 작업이다.
매개변수 모두에 타입을 설정하지 않고 대신 createTodo 상수에 저장 함수를 타입스크립트에 알려주면 된다. 그럼 자동으로 각각 매개변수의 type이 지정되는 것을 확인할 수 있다. 하지만 RequestHandler 타입에 관한건 js 파일로 컴파일할 시에는 사라진다. 왜냐하면 실행되는 코드가 아니라 타입을 임포트했기 때문이다.
그다음 메모리에서 todos를 관리하는 todos배열을 생성한다. 코드안에 존재하는것이므로 서버를 다시시작하면 메모리에서사라진다.
Todo를 관리할 모델만들기
export class Todo{ constructor(public id:string, public text:string,){} }
construct()를 추가한 후 public 액세스 modifier를 추가한다. 이제 타입스크립트는 Todo 클래스를 인스턴스화할 때마다 이름이 같은 프로퍼티를 자동으로 추가하고 생성자에 전달된 값을 해당 필드에 할당하게 된다.
controllers/todo.ts
import { RequestHandler } from "express"; import {Todo} from '../models/todo' const TODOS = []; export const createTodo:RequestHandler = (req, res, next) => { const newTodo = {}; };
위 코드처럼 models 디렉토리에 만든 todo.ts를 임포트하면 해당 타입과 클래스를 이용할 수 있다. Todo는 타입이자 클래스가 된다. 모든 클래스는 타입이자 클래스 역할을 한다.
❗TODOS는 메모리에 저장하는 것이므로 코드를 변경해서 저장할때마다 nodemon으로 새로운 코드를 저장할 때마다 서버가 다시 시작 되고 기존에 메모리에 저장된 것 모두 삭제되기 때문이다. 재설정 된다.
req.body의 담긴 text의 타입
export const createTodo:RequestHandler = (req, res, next) => { const text = req.body.text; const newTodo = new Todo(Math.random().toString(),text); };
새로운 todo를 만들때는 임포트한 클래스를 활용해서 todo 인스턴를 만든다. 여기서 Math.random().toString() 함수를 사용해서 랜덤의 고유 id를 만든다. 실제로 고유한 것은 아니지만 지금은 이것으로 충분하다. 그 다음으로는 todo 본문이 필요한데 이것은 요청 본문에서 가져올 것이다.
위 코드와 같이 text를 req.body에서 받으면 컴파일 오류가 발생하지 않는데 이건 text가 any 타입으로 들어오기 때문이다. 타입스크립트는 수신 요청 받은 body에 어떤 데이터가 있을지 예상할 수 없다. 또 따로 수신된 요청에 어떤 데이터가 있을지 지정한 적이 없다.
text 타입 지정
export const createTodo:RequestHandler = (req, res, next) => { const text = (req.body as { text: string}).text; const newTodo = new Todo(Math.random().toString(),text); TODOS.push(newTodo); res.status(201).json({message:'Create the todo', createTodo: newTodo}); };
이렇게 개발의 주체는 요청 타입에 어떤 데이터가 들어올지 알고 있는 반면 타입스크립트는 이를 예측할 수 없을때 형 변환을 사용하면 편리하다.
req.body에서 텍스트에 엑세스하기 전에 문자열인 text 프로퍼티가 있는 객체로 타입을 변환한다. 이렇게 수신되어야 할 데이터의 타입을 지정하므로써 잘못된 타입으로 된 요청으로 인한 런타임에러를 방지할 수 있다.
본문 요청 파싱(body-parser)
server.ts
import express, { NextFunction, Request, Response } from 'express' // ① json 메서드 import import {json} from 'body-parser' import todoRoutes from './routes/todos' const app = express(); // ② 미들웨서로 json메서드 적용 app.use(json()); app.use('/todo',todoRoutes); app.use((err: Error , req: Request, res: Response, next: NextFunction) =>{ res.status(500).json({message:err.message}); }) app.listen(4500);
이제 추출할 본문이 실제로 존재하도록 만들어야 된다. 그러기 위해서는 app.ts 파일에서 수신된 요청의 본문을 파싱하는 해야한다. 따라서 ①이미 설치한 'body- parser'패키지에서 {json}메서드를 임포트한다. 그리고 ②이 메서드가 미드웨어로 실행 되도록 한다. 그러면 서드파티 패키지에서 제공하는 이 미들웨어가 수신되 요청의 본문을 파싱한다.
controller/todo.ts
export const createTodo:RequestHandler = (req, res, next) => { const text = (req.body as { text: string}).text; const newTodo = new Todo(Math.random().toString(),text); TODOS.push(newTodo); res.status(201).json({message:'Create the todo', createTodo: newTodo}); };
그리고 위 처럼 RequestHandler를 통해 받게되는 req.body 안에 JSON데이터를 추출해서 이 요청 객체의 body 키를 파싱된 JSON데이터로 채우게된다. 이것이 'body-parser' 패키지가 하는 일이다. 이제 새 todo를 만들 수 있는 서버가 준비 돼었다.
CRUD 작업
POST - Todo API 요청
이제 작업한 TypeScirpt파일을 컴파일한뒤에 이를 실행한다.
- http://localhost:4500/todo/
response
{ "message": "Create the todo", "createTodo": { "id": "0.17913337625108872", "text": "test todo post" } }
Patch - 특정 id의 todo 수정하기
URL의 :id
routes/todos.ts
/** * 기존의 toDo를 수정 */ router.patch('/:id',);
해당 URL 라우터에는 동적 세그먼트가(segment)존재한다. id세그먼트이다. 이건 URL에서 추출하게 된다.
controllers/todos.ts
export const updateTodo: RequestHandler<{id : string}> = (req, res, next) => { const todoId = req.params.id; }
그래서 요청이 수신되면 todoId를 req.params.id에서 가져온는 코드를 추가한다. 여기서 todoId의 type이 지정되지 않고 있다는걸 확인할 수 있는데 이유는 req.params의 type이 any이기 때문이다. 타입스크립트가 'todo/:id' URL 문자열을 파싱해서 내용을 이해할 수 없기 때문이다.
타입이 지원되게 하려면 RequestHandler를 사용할 수 있다. 제네릭 타입을 확용하는 것이다 즉 <>(꺽쇠)를 사용해서 어떤 type의 params을 사용할지 알려주면 된다. 꺽쇠 안에 타입을 일단 object로 정하고 URL의 모든 매개변수에 대해 키-값 매핑을 사용할 수 있다. request.params의 안의 들어있는 id의 value는 stirng type이다. RequestHandler에 제네릭을 활용하니까 req.params.id의 값이 any가 아닌 string으로 지정되는 것을 확인할 수 있다.
controllers/todos.ts
export const updateTodo: RequestHandler<{id : string}> = (req, res, next) => { const todoId = req.params.id; const updatedText = (req.body as{text: string}).text; const todoIndex = TODOS.findIndex(todo => todo.id === todoId); if(todoIndex < 0){ throw new Error('Could not find todo'); } TODOS[todoIndex].text = updatedText; res.json({message:'Updated!', updateTodo : TODOS[todoIndex]}); }
이제 TODOS에서 업데이트할 item의 인텍스를 찾는다. TODOS에 findIndex() 메서드를 사용해서 TODO 배열의 모든 item에 대해 앞서 url에서 추출한 todoId와 같은 인덱스를 가진 item이 있는지를 찾는다. 매칭되는 item을 변수 todoIndex에 넣는다.
그리고 해당하는 todo의 text 내용을 update해야 한다. 그런데 만약 todoId와 매칭되는 todo item을 찾을 수 없다면
todoIndex의 값은 -1이 될 것이다. 그럴 경우 if문을 사용해서 todoIndex가 0보다 작은 경우를 걸러내어 Error 처리를 해준다. 이렇게 Error 처리를 하면 server.ts에 지정된 기본 오류처리 미들웨어가 트리거 되어 작동한다.
이런 if문을 통과하고 매칭되는 todo 아이템을 찹은 경우는 해당하는 TODOS 배열해서 해당 인덱스의 todo를 업데이트 해서 위 코드대로 해당하는 todo를 update한다.
DELTE - todo 삭제
controllers/todos.ts
export const deleteTodo: RequestHandler< { id : string } > = (req, res, next) =>{ const todoId = req.params.id; const todoIndex = TODOS.findIndex(todo => todo.id === todoId); if(todoIndex < 0){ throw new Error("Could not find todo!"); } TODOS.splice(todoIndex, 1); res.json({message:'Todo deleted!',todos:TODOS}); }
[출처 - Typescript:기초부터 실전형 프로젝트까지 with React + NodeJS, Mximilian ]
https://www.udemy.com/course/best-typescript-21/?couponCode=24T5FS31824
'개발언어 > TypeScript' 카테고리의 다른 글
TypeScript - Core Syntax & Feature(JavaScript와 차이점) (0) 2023.04.16 TypeScript - Core Syntax & Feature(Core Type) (0) 2023.04.16 TypeScript - Tuple(튜플) (0) 2023.03.22 TypeScripte- typeScript를 javaScript로 compile (0) 2022.11.05 TypeScript의 함수 4 (Conclusions) (0) 2022.06.13