[SOPT] 2차 세미나 - 비동기 처리, HTTP, REST API, Express

📌 이 글은 THE SOPT 30기 서버파트 2차 세미나에서 학습한 내용을 다룹니다.

비동기 처리

동기 vs 비동기

  • 동기(Synchronous)

    • 어떤 작업이 끝나기 전까지 다음 작업 실행 X
    • 요청에 대한 응답이 완료될 때까지 기다림
    • 백그라운드 작업 완료 여부 계속 확인
  • 비동기(Asynchronous)

    • 작업의 완료 유무와 상관없이 동시에 다음 작업 수행 가능
    • 백그라운드가 완료 알림을 주면 미리 지정한 이벤트 처리

Node는 대부분의 메서드를 비동기 방식으로 처리합니다.

비동기 방식을 사용하는 이유

Javascript는 기본적으로 Single Thread 언어이기에 한번에 한 작업 처리

Multi Thread - 병렬처리

  • 요청을 Thread에서 처리하도록 CPU 자원을 분할
  • 자원은 한계가 존재하기 때문에 Load Balancing, 서버 업그레이드 등 자원 문제 해결 필요

비동기 처리가 필요한 이유

  • 동기 처리 시 백그라운드가 작업하는 동안 메인 스레드는 대기
  • 메인 스레드가 일하지 않고 노는 시간 발생 -> 비효율적
  • ex. DB에서 데이터 가져오기

Node에서는?

  • Node에서는 비동기 처리를 Thread로 해결하지 않음
  • Event 방식으로 해결!

비동기 처리 방식

Callback Function(콜백 함수)

  • 어떤 이벤트 발생 시, 특정 시점에 도달했을 때 시스템에서 호출하는 함수, 다른 함수의 인자로서 사용

  • 문제점 - Callback Hell 콜백 지옥

    setTimeout((name): void => {
      serverList = [...serverList, name];
      console.log(serverList);
    
      setTimeout((name): void => {
        serverList = [...serverList, name];
        console.log(serverList);
    
        setTimeout((name): void => {
          serverList = [...serverList, name];
          console.log(serverList);
        }, 500, '서호영');
      }, 500, '이동현');
    }, 500, '채정아');
    

Promise

  • Callback Hell 극복 위해 ES2015부터 Node, JS API들이 콜백 대신 Promise로 재구성됨

  • Promise의 3가지 상태

    1. Pending(대기) - 비동기 처리가 완료되지 않은 상태
      • 최초 생성 시점
      • new Promise() 호출 시 콜백함수 선언
      • resolve: 동작에 대한 결과를 올바르게 줄 수 있음
      • reject: 동작을 실패하면 호출
    2. Fulfilled(이행) - 비동기 처리가 완료되어 Promise 결과 반환
      • 작업을 성공적으로 완료
      • resolve() 실행이 정상적으로 이행(fulfilled)된 상태
      • then()을 통해 전달
    3. Rejected(실패) - 실패하거나 오류
      • 작업이 실패한 상태
      • catch()를 통해 전달
  • Promise Chaining

    • 여러개의 Promise를 연결하여 사용

    • <Promise>.then(): 비동기 작업 완료 시 결과에 따라 함수 호출

    • <Promise>.catch(): 체이닝 연결 상태에서 중간에 에러가 났을 때 호출

      const restaurant = (callback: () => void, time: number) => {
        setTimeout(callback, time);
      };
      
      const order = (): Promise<string> => {
        return new Promise((resolve, reject) => {
          restaurant(() => {
            console.log('[레스토랑 진행 상황 - 음식 주문]');
            resolve('음식 주문 시작');
          }, 1000);
        });
      };
      
      const cook = (progress: string): Promise<string> => {
        return new Promise((resolve, reject) => {
          restaurant(() => {
            console.log('[레스토랑 진행 상황 - 음식 조리 중]');
            resolve(`${progress} -> 음식 조리 중`);
          }, 2000);
        });
      };
      
      const serving = (progress: string): Promise<string> => {
        return new Promise((resolve, reject) => {
          restaurant(() => {
            console.log('[레스토랑 진행 상황 - 음식 서빙 중]');
            resolve(`${progress} -> 음식 서빙 중`);
          }, 2500);
        });
      };
      
      const eat = (progress: string): Promise<string> => {
        return new Promise((resolve, reject) => {
          restaurant(() => {
            console.log('[레스토랑 진행 상황 - 음식 먹는 중]');
            resolve(`${progress} -> 음식 먹는 중`);
          }, 3000);
        });
      };
      
      order()
        .then((progress) => cook(progress))
        .then((progress) => serving(progress))
        .then((progress) => eat(progress))
        .then((progress) => console.log(progress));
      출력
      
      [레스토랑 진행 상황 - 음식 주문]
      [레스토랑 진행 상황 - 음식 조리 중]
      [레스토랑 진행 상황 - 음식 서빙 중]
      [레스토랑 진행 상황 - 음식 먹는 중]
      음식 주문 시작 -> 음식 조리 중 -> 음식 서빙 중 -> 음식 먹는 중
    • Promise는 여러개여도 catch는 단일!

      Promise.resolve(123)
        .then((res) => {
          throw new Error('에러 발생');
          return 456;
        })
        .then((res) => {
          console.log(res); // 절대 실행 되지 않음!!!
          return Promise.resolve(789);
        })
        .catch((error) => {
          console.log(error.message);
        });
      출력
      
      에러 발생

Async/Await

  • ES2017부터 제공되는 혁신적인 기능!

  • Promise가 어느정도 콜백 지옥을 해결했지만, 여전히 코드가 복잡하고 어려움

  • 동기 코드와 아주 유사

  • 이해하기 쉬움

  • Async: Async는 암묵적으로 Promise를 반환

  • Await: Promise를 기다림(resolve/reject), async 정의된 내부에서만 사용

  • Async 함수 형태

    // 함수 표현식
    const asyncFunction1 = async() => {
    
    }
    
    // 함수 선언식
    async function asyncFunction2() {
    
    }

HTTP

HyperText Transfer Protocol

하이퍼텍스트 문서를 주고 받을 수 있는 프로토콜 (규칙)

Stateless Protocol

무상태 프로토콜

  • 모든 요청이 상호 독립적
  • 서버가 request, response 간에 어떠한 데이터도 보존하지 않음
  • 중간에 요청이 다른 서버로 들어가도 전혀 문제 없음

HTTP Request

HTTP Method

HTTP MethodActionRequest Body
GET조회X
POST생성O
PUT수정O
PATCH일부 수정O
DELETE삭제X

HTTP Response

HTTP Status

응답 코드상태
200성공 OK
201성공, 리소스 생성 Created
204성공, 응답 데이터 없음 No Content
400요청을 이해하지 못함 Bad Request
401인증 필요 Unauthorized
403요청 거부 Forbidden
404리소스를 찾을 수 없음 Not Found
409요청 충돌 Conflict
500서버 내부 오류 Internal Server Error
503일시적 서버 이용 불가 Service unavailable

HTTP Request Response


REST API

REpresentational State Transfer

서버의 자원을 정의하고, 자원에 대한 주소를 지정하는 방법
리소스 지향 아키텍처 -> 모든 것을 가급적 리소스, 명사로 표현

REST 아키텍처를 준수하는 API

API

Application Programming Interface

서버 어플리케이션의 기능을 사용하기 위한 방법
구현 방식을 몰라도 서비스가 서로 통신 가능!

그래서 REST API?
URI는 정보의 자원을 표현
자원에 대한 행위HTTP Method(GET, POST, PUT, PATCH, DELETE)으로 표현

URI vs URL

URI

Uniform Resource Identifier

통합 자원 식별자 (자원을 나타내는 주소)
자원을 나타내는 유일한 주소

URL

Uniform Resource Locator

통합 자원 지시자
특정 서버의 한 리소스에 대한 구체적인 위치 서술

REST API 기준

  1. 클라이언트, 서버 및 리소스로 구성되었으며 요청이 HTTP를 통해 관리되는 클라이언트-서버 아키텍처
  2. 스테이트리스(stateless) 클라이언트-서버 커뮤니케이션
    -> 요청 간에 클라이언트 정보가 저장되지 않으며, 각 요청이 분리되어 있고 서로 연결되어 있지 않음
  3. 클라이언트-서버 상호 작용을 간소화하는 캐시 가능 데이터
  4. 요청된 정보를 검색하는 데 관련된 서버(보안, 로드 밸런싱 등을 담당)의 각 유형을
    클라이언트가 볼 수 없는 계층 구조로 체계화하는 계층화된 시스템
  5. 정보가 표준 형식으로 전송되도록 하기 위한 구성 요소 간 통합 인터페이스

중심 규칙

  1. '/' (슬래시)는 계층 관계 표현
  2. '/' (슬래시)는 URI 마지막에 포함되지 않음
    https://sopt.org - (O)
    https://sopt.org/ - (X)
  3. 가독성을 높이기 위해 '-' 하이픈 사용
    https://sopt.org/server-part - (O)
    https://sopt.org/serverpart - (X)
  4. 언더바는 URI에 사용하지 않음
    https://sopt.org/server-part - (O)
    https://sopt.org/server_part - (X)
  5. URI에는 소문자가 적합
    https://sopt.org/server-part - (O)
    https://sopt.org/serverPart - (X)
  6. 파일 확장자는 URI에 포함시키지 않음
    https://sopt.org/appjam.jpeg - (X)
  7. 리소스 간의 연관관계 표현
    https://sopt.org/users/{userId}/device

Express

Node.js를 위한 서버 프레임워크

yarn add express

yarn add -D @types/node @types/express

yarn add -D nodemon

tsconfig.json 파일 생성

tsc --init

Typescript를 Javascript로 컴파일하는 옵션 설정

{
  "compilerOptions": {
    "target": "es6", // 어떤 버전으로 컴파일
    "allowSyntheticDefaultImports": true, // default export가 없는 모듈에서 default imports를 허용
    "experimentalDecorators": true, // decorator 실험적 허용
    "emitDecoratorMetadata": true, // 데코레이터가 있는 선언에 대해 특정 타입의 메타 데이터를 내보내는 실험적인 지원
    "skipLibCheck": true, // 정의 파일 타입 체크 여부
    "moduleResolution": "node", // commonJS -> node 에서 동작
    "module": "commonjs", // import 문법
    "strict": true, // 타입 검사 엄격하게
    "pretty": true, // error 메시지 예쁘게
    "sourceMap": true, // 소스맵 파일 생성 -> .ts가 .js 파일로 트랜스 시 .js.map 생성
    "outDir": "./dist", // 트랜스 파일 (.js) 저장 경로
    "allowJs": true, // js 파일 ts에서 import 허용
    "esModuleInterop": true, // ES6 모듈 사양을 준수하여 CommonJS 모듈을 가져올 수 있게 허용
    "typeRoots": [
      "./src/types/express.d.ts", // 타입(*.d.ts)파일을 가져올 디렉토리 설정
      "./node_modules/@types" // 설정 안할시 기본적으로 ./node_modules/@types
    ]
  },
  "include": [
    "./src/**/*" // build 시 포함
  ],
  "exclude": [
    "node_modules", // build 시 제외
    "tests"
  ]
}

서버 만들어보기

import express, { Request, Response, NextFunction } from 'express';

const app = express(); // express 객체 받아옴

app.get('/', (req: Request, res: Response, next: NextFunction) => {
  res.send('Hi! My name is HyeokJoon!');
}); // get -> http method

app.listen('8000', () => {
  console.log(`
        #############################################
            🛡️ Server listening on port: 8000 🛡️
        #############################################
    `);
}); // 8000 번 포트에서 서버를 실행하겠다 ~

package.json 수정

{
  "name": "express-example",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "dev": "nodemon",
    "build": "tsc && node dist"
  },
  "dependencies": {
    "express": "^4.17.3"
  },
  "devDependencies": {
    "@types/express": "^4.17.13",
    "@types/node": "^17.0.23",
    "nodemon": "^2.0.15"
  }
}

서버 실행

yarn run dev // nodemon으로 서버 실행

yarn run build // 프로젝트 build

라우팅

애플리케이션 엔드 포인트(URI)의 정의, 그리고 URI가 클라이언트 요청에 응답하는 방식

import express, { Request, Response, Router } from 'express';

const router: Router = express.Router();

router.get('/', (req: Request, res: Response) => {
  return res.status(200).json({
    status: 200,
    message: '유저 조회 성공',
  });
});

module.exports = router;

좋은 웹페이지 즐겨찾기