참고: Building RESTful APIs Using Node.js and Express - Documenting and Building REST API’s using Vanilla Node.js - 1주 | Coursera
A Vanilla Node.js REST API without Frameworks such us Express | Engineering Education (EngEd) Program | Section
- 이 포스트에서는 coursera의 Building RESTful APIs Using Node.js and Express | Coursera 를 수강하며 배운 주요 개념과 과제를 진행하며 발생한 이슈 해결 방법을 공유합니다.
- 이는 저의 개인적인 이해를 바탕으로 작성되었으며 모든 정보가 최신이거나 완전한 것은 아니므로 항상 개선의 여지가 있습니다. 따라서 읽는 동안 어떤 피드백이나 추가적인 아이디어가 있으시다면 언제든지 댓글로 알려주시기 바랍니다.
1. RESTful Service의 구성요소
- resource
- request verbs (http method)
- request headers
- request body
- response status code
1.1. 리소스 선택하기
- 비즈니스 도메인 분석
- 관련있는 명사를 추출해내기
- 이때 api consumer들의 요구사항을 확인해라.
- API를 모델링한다
- nouns를 추출했으면, 그것에 어떤 작업을 할지(http method)를 정하라
1.2. request verbs
자주 사용되는 6가지
HTTP 요청 메서드 | 종류 | 목적 | 페이로드 |
---|---|---|---|
GET | index/retrieve | 모든/특정 리소스 취득 | X |
POST | create | 리소스 생성 | O |
PUT | replace | 리소스의 전체 교체 | O |
PATCH | modify | 리소스의 일부 수정 | O |
DELETE | delete | 모든/특정 리소스 삭제 | X |
OPTIONS | fetch | 사용 가능한 모든 REST 연산 불러오기 | O |
post 메소드의 예시
레스토랑과 디쉬의 위계를 표현하고 있음에 주목. 결국 DB 모델링에서부터 쭉 이어지는 것이 아닐까 한다.
1.3. request headers
- 모든 헤더가 요청 헤더는 아님. 예를 들어, content-type 헤더는 표현 헤더로 일컬어짐.
1.4. response status code
- 특정 요청이 성공적인지 성공적이지 않은지 알려줌. "success"의 의미는 http method가 무엇이냐에 따라 달라진다.
- 만약 성공적이지 않다면 에러 타입을 반환함
- 자주 쓰이는 상태 코드 종류
- 401 Unauthorized : 403 forbidden과 비슷하지만 401은 특히 인증이 요구되거나 인증이 아직 제공되지 않았을 때.
- 404 not found: 리소스가 현재는 없지만 미래에는 있을 수도
- 500 internal server error: generic error message, 예기치 않은 조건이 발생하여 특정 메시지가 적합하지 않을 때 표시
- 503 service not available: 일시적으로 서버가 요청을 핸들링 할 수 없을 때
1.5. vanila js로 서버 만들기
// server.js
const http = require('http');
const PORT = process.env.PORT || 4000;
// create server - returns a server object
const server = http.createServer((request, response) => {
response.writeHead(200, {
'content-type': 'text/plain',
});
response.end('hello!!!');
});
// make the server listen for clients
// event emitter model
// server -> emits a listen event, port number etc
server.listen(PORT, () => {
console.log('Server is ready and listening at port', PORT);
});
server.on('error', (error) => {
if (error.code === 'EADRINUSE') {
console.log('Port already in use');
}
});
http.createServer((request, response) => { // 모든 요청과 응답에 대한 처리는 여기서! })
1.6. 방금 만든 서버에 요청 보내고 응답 확인하기
powershell에서 $ iwr http://localhost:4000
StatusCode : 200
StatusDescription : OK
Content : hello!!!
RawContent : HTTP/1.1 200 OK
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked
Content-Type: text/plain
Date: Sat, 01 Jul 2023 11:10:39 GMT
hello!!!
Forms : {}
Headers : {[Connection, keep-alive], [Keep-Alive, timeout=5], [Transfer-Encoding, chunked], [Content-Type,
text/plain]...}
Images : {}
InputFields : {}
Links : {}
ParsedHtml : mshtml.HTMLDocumentClass
RawContentLength : 8
1.7. get, post, put, delete 메소드 구현하기 w/ vanilla js
TASK 1: 프로덕트 CRUD API 구현하기 w/ 바닐라JS
과제 목표
- 바닐라JS로 http 서버 구축하기
- 지정한 포트, 지정한 보일러플레이트 코드에서 구현할 것
- get, post, put, delete 메소드 정의하기
- routes 정의하기
- 모든 상품 가져오기
- 상품id로 프로덕트 가져오기
- 새 프로덕트 생성하고 data 포스팅
- 특정 프로덕트의 디테일 업데이트하기
- 상품id로 프로덕트 삭제하기
- 포스트맨으로 테스트하기
나의 생각
product.json가 데이터베이스 역할을 한다.
프로덕트 디테일 정보를 다루는 CRUD 라우터를 app.js에 구현하고, 상세한 비즈니스 로직은 productService.js에 담는다.
utils.js 파일에는 어플리케이션 전반에 걸쳐 사용되는 유틸리티 함수를 담는다. 예를 들어, 이 과제에서는 request body를 읽어내는 유틸리티 함수를 사용한다.
http.createServer()
의 인자로 주어진 콜백 함수에서 요청과 응답을 처리하는 router를 정의한다.
바닐라JS 구현 과제를 하며 느낀, 바닐라JS를 이용하여 라우터를 정의하고 API를 설계할 때의 불편한 점을 꼽아보면 다음과 같다.
- 조건문을 통해서 HTTP method와 API 엔드포인트를 구분한다
- 일일이 URL 문자열 파싱하여 엔드포인트의 parameter를 추출해내야 한다.
- 요청 body를 파싱도 직접 해야 한다.
- (이번 과정에서는 미들웨어를 구현하진 않았지만) 미들웨어 체인을 구현하는 것도 비교적 불명확하고 번거로울 것으로 추측된다.
가독성이 매우 떨어지고 유지보수가 어렵겠다고 느꼈다. 줄줄이 이어진 if, else if 를 사용하건 switch-case 문을 사용하건 마찬가지일 것이다.
후에 express를 이용해 라우터를 구현하다보면, 바닐라JS로는 얼마나 불편하고 번거로운지 비교 대상이 되어 체감하게 된다.
TASK 2: 무비 애플리케이션
과제 목표
- http 서버 구축하기
- db.json 파일에 resource 저장
- db.json 파일을 서버로서 특정 포트에서 실행시키기 (데이터베이스 역할, JSON 서버란?)
- get, post, put, delete 메소드 정의하기
- routes 정의하기
- 모든 상품 가져오기
- 상품id로 프로덕트 가져오기
- 새 프로덕트 생성하고 data 포스팅
- 특정 프로덕트의 디테일 업데이트하기
- 상품id로 프로덕트 삭제하기
- 포스트맨으로 테스트하기
나의 생각
task 1과는 달리 json-server라는 라이브러리를 이용하여 API를 통해 json 파일에 접근할 수 있다.
DB를 사용하는 실제 앱과 비슷한 환경으로 쓸 수 있다.
json-server는 이번 기회에 처음 사용해보았는데 매우 신기했다.
질문 & 배운 점
1) post 메소드를 통해 이미 존재하는 자료를 생성한 경우의 에러 핸들링?
if (req.url === '/movies' && req.method === 'POST') {
// Save movie details
const reqBody = JSON.parse(await getRequestData(req));
moviesService.saveMovie(reqBody, (err, result) => {
if (err) {
res.writeHead(409, { 'content-type': 'application/json' });
res.end(err);
} else {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(result);
}
});
}
409 Conflict: 이미 존재하는 자료와 충돌이 발생하는 경우 사용. 예를 들어, 유일한 값을 가진 필드(예: 사용자 이름, 이메일 등)에 대해 중복된 값을 사용하여 새로운 리소스를 생성하려고 시도할 때 이 코드를 사용할 수 있다.
2) 데이터 타입 변환(ex. JSON에서 object, object에서 JSON, string에서 number로 등)을 서버사이드, 클라이언트 사이드 중 어느 곳에서 처리할지 어떻게 결정할까?
데이터 변환은 클라이언트 사이드와 서버 사이드 모두에서 발생할 수 있으며, 그 처리 위치는 구체적인 상황과 요구 사항에 따라 달라질 수 있다.
클라이언트 사이드에서는 주로 사용자의 입력을 처리하거나, 서버로부터 받은 데이터를 사용자 인터페이스에 표시하는 등의 작업을 수행한다. 예를 들어, 사용자로부터 받은 폼 입력을 적절한 형태로 변환하거나, 서버로부터 받은 JSON 데이터를 JavaScript 객체로 변환하여 화면에 표시하는 등의 작업이 있다.
서버 사이드에서는 주로 데이터베이스로부터 데이터를 받아 클라이언트에게 전송하거나, 클라이언트로부터 받은 데이터를 처리하여 데이터베이스에 저장하는 등의 작업을 수행한다. 예를 들어, 데이터베이스로부터 받은 데이터를 JSON 형태로 변환하여 클라이언트에게 전송하거나, 클라이언트로부터 받은 JSON 데이터를 JavaScript 객체로 변환하여 데이터베이스에 저장하는 등의 작업을 수행할 수 있다.
일반적으로는 다음과 같은 기준에 따라 클라이언트 사이드와 서버 사이드 중 어디에서 데이터 변환을 처리할지 결정할 수 있다고 한다:
- 데이터의 복잡성: 데이터 변환 로직이 복잡하거나 리소스를 많이 사용한다면, 이를 서버 사이드에서 처리하는 것이 효율적일 수 있다. 반대로, 간단한 데이터 변환은 클라이언트 사이드에서 처리하는 것이 더 효율적일 수 있다.
- 보안: 민감한 데이터는 클라이언트 사이드에서 처리하지 않는 것이 좋다. 이 경우 데이터 변환은 서버 사이드에서 이루어져야 한다.
- 사용자 경험: 데이터 변환 로직이 사용자 경험에 영향을 미칠 수 있다. 예를 들어, 대용량 데이터의 변환을 클라이언트 사이드에서 처리하면 애플리케이션의 반응성이 떨어질 수 있다. 이 경우, 데이터 변환을 서버 사이드에서 처리하는 것이 더 나을 수 있다.
3) response.end()
를 두 번 부르지 않기
이미 보낸 응답을 또 보낼 수는 없다!
4) 요청 바디를 읽는 getRequestData(req)
함수 explained
const getRequestData = (req) => {
return new Promise((resolve, reject) => {
try {
// 클라이언트가 보낸 요청 바디를 파싱한다.
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', () => {
resolve(body);
});
} catch (error) {
reject(error);
}
});
};
이 함수는 Node.js의 HTTP 요청 객체에서 클라이언트로부터 보낸 데이터를 비동기적으로 읽는 역할을 한다. 이 함수가 비동기적인 이유는 Node.js에서 HTTP 요청 데이터를 읽는 과정이 비동기적으로 이루어지기 때문이다.
req
파라미터: Node.js HTTP 서버에 들어오는 HTTP 요청 객체- Promise 객체를 반환: Promise 객체는 JavaScript에서 비동기 작업을 나타낸다. 이 Promise는 클라이언트가 보낸 데이터를 읽는 작업이 성공적으로 완료되면 'resolve’되고, 에러가 발생하면 'reject’된다.
req.on('data', callback)
: 요청 객체에서 ‘data’ 이벤트를 감지하는 이벤트 리스너를 등록. 클라이언트가 보낸 body 데이터는 스트림 형태로 chunk로 나누어져 서버에 도착하는데, 각 chunk가 도착할 때마다 ‘data’ 이벤트가 발생한다. chunk가 도착할 때마다 콜백 함수가 이를 문자열로 변환하고,body
변수에 추가한다.req.on('end', callback)
: 요청 객체에서 ‘end’ 이벤트를 감지하는 이벤트 리스너를 등록. ‘end’ 이벤트는 클라이언트가 보낸 모든 데이터를 서버가 받았을 때 발생한. 여기서의 콜백 함수는 Promise를 'resolve’하며, Promise가 결과로body
를 반환.reject
객체: 만약 이 과정에서 어떤 에러가 발생하면, Promise는 'reject’되고 에러 객체가 반환된다.
5) HTTP의 request 객체의 ‘data’, ‘end’ 외의 다른 이벤트?
close
: 클라이언트가 요청을 취소하고 연결을 닫았을 때 발생. 이 이벤트는 클라이언트가 서버에 보내는 데이터의 스트림이 아직 끝나지 않았는데도 연결이 끊어졌을 때 유용하다.aborted
: 클라이언트가 요청을 취소했을 때 발생. 이 이벤트는 클라이언트가 요청을 취소한 경우를 처리하려는 경우에 유용하다.error
: 요청에서 오류가 발생했을 때 발생. 예를 들어, 네트워크 오류나 클라이언트로부터 잘못된 데이터를 받았을 때 발생한다.
이러한 이벤트는 요청 객체의 on(event, callback)
메서드를 사용하여 감지할 수 있다.
6) json-server 패키지 사용하기
npx json-server -w data/movies.json
JSON 파일을 서버로 구동시켜준다.
localhost:포트/movies
로 접속 가능하고 json 서버 리소스에 변경 사항을 가하면 영구적으로 json 파일 내용이 변한다.
7) ERROR: response.end()의 인자로 넘겨줄 수 있는 타입은?
데이터베이스 서버에서 받은 response 객체를 고대로 res.end()
로 쓩 보내주려는데 에러가가 발생했다.
TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of Array at new NodeError (node:internal/errors:400:5) at write_ (node:_http_outgoing:862:11) at ServerResponse.end (node:_http_outgoing:996:5) at C:\Users\jamie\Documents\coursera-nodejs-practice\movie-application-master\src\app.js:19:13 at Object.getMovies (C:\Users\jamie\Documents\coursera-nodejs-practice\movie-application-master\src\moviesService.js:11:12) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) { code: 'ERR_INVALID_ARG_TYPE' }
해당 에러 메시지는 Node.js에서 res.end()
또는 res.write()
함수에 전달되는 “chunk” 인수가 잘못된 타입을 가지고 있다고 알려준다.
해당 함수는 문자열, Buffer, 또는 Uint8Array 타입의 데이터만 받아들인다. 그러나 나는 배열을 전달했기 때문에 문제인 것.
[!NOTE]
Buffer
타입과Uint8Array
타입이란?
자바스크립트에서 바이트 단위로 작업할 수 있게 해주는 특별한 데이터 타입. 이런 데이터 타입은 네트워크를 통해 데이터를 전송하거나, 파일에서 데이터를 읽고 쓸 때 사용되며 효율적인 이진 데이처 처리를 위해 사용한다.
- buffer: Node.js의 전역 객체로, 고정된 크기의 메모리를 할당하고, 바이트 단위의 데이터를 읽고 쓰는데 사용된다. 특히 이진 데이터를 처리할 때 많이 사용되며, 파일 시스템 작업, 이미지 처리, 데이터 압축, 복사 등에서 자주 볼 수 있다고 한다.
Uint8Array
: 자바스크립트의TypedArray
중 하나로, 8비트 부호 없는 정수의 배열을 말한다. 버퍼와 마찬가지로 이진 데이터를 처리할 때 사용되며, WebRTC 같은 웹 API에서 많이 사용한다고 함.
[!NOTE]
TypedArray
란?
바이너리 데이터를 작업하기 위한 객체.
고정 길이의 원시 이진 데이터 버퍼를 배열과 유사한 객체로 래핑한 것.
일반 JS 배열은 동적이며 다양한 타입의 원소를 가질 수 있지만,TypedArray
는 고정 길일르 가지며 특정 타입의 숫자만 원소로 가질 수 있다.
이진 데이터를 처리하거나 제어해야 하는 웹API에서 주로 사용. 이런 상황에서는 일반 JS 배열보다 더 성능이 좋다고 함.
Int8Array
: -128부터 127까지의 8비트 정수를 저장하는 배열Uint8Array
: 0부터 255까지의 8비트 부호 없는 정수를 저장하는 배열Float32Array
: 32비트 부동 소수점 숫자를 저장하는 배열
이진 데이터를 다루고 있진 않으므로, 에러를 해결하기 위해 배열을 문자열로 변환한다. JSON.stringify()
함수를 사용한다.
// 잘못된 코드
// res.end(arrayData);
// 수정된 코드
res.end(JSON.stringify(arrayData));
8) ERROR: 순환 참조 객체 JSON 변환 불가 에러
자, 앞선 에러에서 JSON.stringify()
함수를 이용하여 배열을 string으로 변환했다.
그런데 에러가 발생했다. 아래를 보자.
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'ClientRequest'
| property 'socket' -> object with constructor 'Socket'
--- property '_httpMessage' closes the circle
at JSON.stringify (<anonymous>)
at Object.getMovies (C:\Users\jamie\Documents\coursera-nodejs-practice\movie-application-master\src\moviesService.js:10:28)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
circular 구조를 JSON으로 바꿀 수 없다는 듯한데, 이게 대체 뭘까?!
검색을 통해 알아본 바, 위 에러는 순환 참조를 가진 객체를 변환할 수 없다는 것을 뜻한다.
[!NOTE] 순환 참조를 가진 객체란?
자신의 하위 객체가 결국 자신을 참조하는 방식으로 구성된 객체.
객체의 속성이 직접적으로 혹은 간접적으로 자기 자신을 참조하는 구조.
예를 들어 아래와 같은 객체다.
// 여기서 `obj`는 자신의 `child` 속성을 통해 자기 자신을 참조하고 있다.
let obj = {
name: "obj",
child: {}
};
obj.child.parent = obj;
순환 참조를 가진 객체는 JSON.stringify
같은 함수로 JSON으로 직렬화할 때 문제를 일으킨다고 한다. JSON 형식은 이러한 순환 참조를 지원하지 않는다고 한다. 따라서 이 경우 순환 참조를 제거하거나, JSON.stringify
의 replacer 매개변수를 사용하여 순환 참조를 특별히 처리해야 한다.
replacer
함수를 이용하여 순환 참조를 처리하는 방법을 보자.
아래 replacer 함수는 각 속성에 대해 호출되며 반환되는 값이 문자열에 포함된다. 순환 참조가 발견되면 이 함수는 undefined
를 반환하여 해당 속성을 제외할 수 있다.
function replacer(key, value) {
if (value === obj) {
return; // 순환 참조를 제거
}
return value;
}
const jsonString = JSON.stringify(obj, replacer);
그러나 이 방법은 복잡한 구조의 객체의 경우 제대로 순환 참조를 제거하지 못할 수도 있다고 한다.
참고로 에러 메시지에서 "starting at object with constructor ‘ClientRequest’"라고 언급하는 부분이 ClientRequest
객체가 순환 참조를 가지고 있다는 것을 나타낸다고 한다.
모든 객체를 JSON 문자열로 변경할 수 있는 것은 아니었다! 나의 경우 replacer와 같은 방법까지 사용할 필요 없이, 순환 참조 객체가 없는 data 프로퍼티만 json화하여 넘겨주는 것으로 간단히 해결하였다. ㅎㅎ
'웹' 카테고리의 다른 글
Prisma 스키마 동기화 중 Schema Drift 발생 시 데이터베이스 리셋 없이 해결하기 (1) | 2024.02.27 |
---|---|
Node.js, Express, Swagger를 이용한 RESTful API 설계 (4) JWT, OAuth2 구현하기 (0) | 2023.07.30 |
Node.js, Express, Swagger를 이용한 RESTful API 설계 (3) Swagger 사용하기 (0) | 2023.07.29 |
Node.js, Express, Swagger를 이용한 RESTful API 설계 (2) Express 프레임워크 사용하기 (0) | 2023.07.29 |