github-blog.png


✍️ Today I Learned


1. CORS(Cross-Origin Resource Sharing)


  • 교차 출처 리소스 공유라 불리는 CORS는 브라우저에서 다른 출처의 리소스를 공유하는 방법이다.

    출처에 대해서 설명하기 전, URL의 구조에 대해서 짚고 넘어가야한다. 앞서 URL에 대하여 학습한적이 있다. 다시 들여다 보면,

    Notes_210918_133331

    이중 출처(Origin)는 Protocol, Host, Port(생략가능)를 합친 것을 말한다. Origin은 모든 웹페이지 개발자도구에서 location.origin을 통하여 확인 할 수 있다.

    따라서 Protocal, Host, Port 동일한 경우는 출처가 동일하다고 볼 수 있다.



1-1. CORS가 왜 필요한가?


  • 보안상의 이유로 자바스크립트 엔진 표준 스펙에는 동일 출처 정책(Same-Origin Policy)이 포함되어있다. (만약 다른 서버의 내용까지 읽을 수 있게된다면, 사용자의 키보드 입력을 가로채는 스크립트를 다른 페이지에 심을 수도 있으므로 보안 이슈가 발생할 수 있다.)

    하지만 우리는 양질의 웹페이지 개발을 하기 위해서는 타 오리진의 리소스가 필요하다.

    스크린샷, 2021-09-18 14-02-06

    기상청, 아큐웨더, 웨더채널의 API를 가져오는 네이버날씨

  • 이러한 이유들로 타 오리진의 리소스를 필요로 하는 경우가 생겨났으며, 특정 규약을 통해서 보안에 위배되지 않게 리소스를 공유하는 방법이 CORS이다.



1-2. CORS 동작 원리


  • CORS의 동작 원리는 세종류가 있다. 두가지 내용만 우선 언급하자면 서버에게 바로 본 요청을 직접 보내는 방법인 Simple request,

    simple-req-updated

    그리고 서버에 예비 요청을 보내서 안전한지 판단한 후 본 요청을 보내는 방법인 Preflight request이 있다.

    preflight_correct

  • 자세한 내용은 MDN CORS 접근 제어 시나리오 예제를 참조하자. 참조 : MDN



2. Mini Node Server (http모듈)


  • node.js의 http모듈 혹은 Express를 이용하여 웹 서버를 만드는 예제이다.

    우선 http모듈은 HTTP 요청, 응답을 다룬다. HTTP 지식이 선행되어야 했기 때문에 먼저 HTTP 트랜잭션 해부공식 문서를 언급하고 넘어가야했다.



2-1. HTTP 트랜잭션 해부


  • 우선 node.js HTTP 처리 과정을 공식문서를 참고로 학습하였다.
  1. 서버 생성 : 모든 node 웹 서버 애플리케이션은 웹 서버 객체를 만들어야 한다. 이 때 createServer를 이용한다.

    const http = require('http');
    
    const server = http.createServer((request, response) => {
      // 여기서 작업이 진행됩니다!
    });

    이 서버로 오는 HTTP 요청마다 createServer에 전달된 함수가 한 번씩 호출된다.

    HTTP 요청이 서버에 오면 node가 트랜잭션을 다루려고 requestresponse 객체를 전달하며 요청 핸들러 함수를 호출한다.

    요청을 실제로 처리하려면 listen 메서드가 server 객체에서 호출되어야 한다.

    대부분은 서버가 사용하고자 하는 포트 번호를 listen에 전달하기만 하면 된다.

  2. 메서드, URL, 헤더 : 요청을 처리할 때, 메서드URL을 확인한 후 이와 관련된 적절한 작업을 실행해야 한다.

    Noderequest 객체에 유용한 프로퍼티를 넣어두었으므로 구조분해 할당으로 해당 값들을 쉽게 가져올 수 있다.

    const { method, url } = request;

    여기서 method는 항상 일반적인 HTTP 메서드/동사이며 url은 전체 URL에서 서버, 프로토콜, 포트를 제외한 나머지이다.

    헤더도 많이 다르지 않다. requestheaders라는 전용 객체가 있으므로, 해당 프로퍼티를 사용한다.

    const { headers } = request;
    const userAgent = headers['user-agent'];

    requestheaders라는 전용 객체가 있고, 클라이언트가 어떻게 헤더를 설정했는지에 관계없이 모든 헤더는 소문자로만 표현된다는 것을 기억하자. 이는 어떤 목적이든 헤더를 파싱하는 작업을 간편하게 해준다.

  3. 요청 바디 : POSTPUT 요청을 받을 때 애플리케이션에 요청 바디는 중요할 것이다.

    핸들러에 전달된 request 객체는 ReadableStream 인터페이스를 구현하고 있는데, 이 스트림의 'data''end' 이벤트에 이벤트 리스너를 등록해서 데이터를 받을 수 있다.

    'data' 이벤트에서 발생시킨 청크는 Buffer이다. 이 청크는 문자열 데이터이므로 이 데이터를 배열에 담고, 'end' 이벤트에서 이어 붙인 다음 문자열로 만드는 것이 가장 좋다.

    let body = [];
    request
      .on('data', (chunk) => {
        body.push(chunk);
      })
      .on('end', () => {
        body = Buffer.concat(body).toString();
        // 여기서 `body`에 전체 요청 바디가 문자열로 담겨있습니다.
      });


2-2. Mini Node Server 구현


  • 위 공식 문서를 토대로 작성하였다.

    const http = require("http");
    const PORT = 5000;
    const ip = "localhost";
    
    const server = http.createServer((request, response) => {
    if (request.method === "OPTIONS") { // CORS preflight request에 대한 응답
      response.writeHead(200, defaultCorsHeader);
      response.end();
    }
    if (request.method === "POST") {
      let body = [];
    
      request
        .on("data", (chunk) => {
          body.push(chunk);  // 요청바디. body 배열에 chunk를 push
        })
        .on("end", () => {
          body = Buffer.concat(body).toString();  // end 이벤트에서 이어 붙힌 뒤 문자열로 변환한다.
          response.writeHead(201, defaultCorsHeader);
    
          if (request.url === "/lower") {  // POST 메소드 Endpoint /lower에 따른 조건분기
            response.end(body.toLowerCase());
          } else if (request.url === "/upper") {
            response.end(body.toUpperCase());
          } else {
          }
        });
    }
    
    server.listen(PORT, ip, () => {  // 서버를 활성화하고 포트번호는 PORT(5000)로 아이피는 ip(localhost)로 받는다.
      console.log(`http server listen on ${ip}:${PORT}`);
    });
    
    const defaultCorsHeader = {  // CORS 응답헤더
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
      "Access-Control-Allow-Headers": "Content-Type, Accept",
      "Access-Control-Max-Age": 10,
    };


3. Mini Node Server (프레임워크 Express)


  • Express.js는 Node.js 환경에서 웹 서버, 또는 API 서버를 제작하기 위해 사용되는 인기 있는 프레임워크이다.

  • 위에서 Mini Node Server에서 http 모듈로 작성했던 서버를, 프레임워크 Express를 이용하는 방식으로 리팩토링하면 다음과 같다.

    const express = require('express'); // express 모듈을 불러온다.
    const cors = require('cors'); // cors 미들웨어를 불러온다.
    
    const app = express(); // 서버
    app.use(cors()); // OPTIONS, 모든 요청에 대해 CORS 허용
    app.use(express.json({ strict: false })); // 기본형은 true로 원시 자료형을 취급하지 않는다. false값을 줘서 원시자료형도 parsing 해주게끔 설정
    
    const PORT = 5000;
    const ip = 'localhost';
    
    app.post('/upper', function (request, response) {
      response.json(request.body.toUpperCase());
    });
    
    app.post('/lower', function (request, response) {
      response.json(request.body.toLowerCase());
    });
    
    app.listen(PORT, () => {
      console.log(`http server listen on ${ip}:${PORT}`);
    });


3-1. Express로 구현한 서버와 http 모듈로 작성돤 서버의 다른 점


  1. 미들웨어 추가가 편리하다 : 위 예제는 npm install cors 로 미들웨어 설치 후 사용만 해주면 되기에 편리하게 cors옵션을 설정할 수 있다.

    const express = require('express');
    const cors = require('cors');
    const app = express();
    
    app.use(cors());

    위 내용만으로 모든 cross-origin 요청에 대해 응답한다. 만약 특정 도메인 요청만 받거나, 특정 요청에만 응답하는 경우 그에 따른 옵션을 설정해야한다.

    const options = {
      origin: '*', // 접근 권한을 부여하는 도메인
      credentials: true, // 응답 헤더에 Access-Control-Allow-Credentials 추가
      methods: 'GET, POST, OPTIONS', // 메소드 접근 권한 부여
    };
    app.use(cors(options));
  2. 자체 라우터를 제공한다 : 자세한 설명은 참조링크로 대체.



🤔 Understanding

  • CORS(교차 출처 리소스 공유)는 아직 공부할 영역이 많이 남은 느낌이다…

  • http 모듈을 이용한 서버 구현과 express.js를 이용한 서버에 대해서는 구조는 감을 잡았다.

    조금더 많은 경우를 접해봐야 확실히 쓸 수 있을 듯하다. (아직 express에서의 cors 옵션 부분은 낯설다.)