[Day 32] 클래스(2) + 큐, 스택, 트리 + 비동기 프로그래밍 + Cookie + Ajax + CORS + Access Token

클래스

클래스 상속 (Class Inheritance)

한 클래스의 기능을 다른 클래스에서 재사용할 수 있다.

class Parent {
  // ...
}

class Child extends Parent {
  // ...
}
  • extends 키워드를 통해 Child 클래스가 Parent 클래스를 상속하고 있다.
  • 이러한 관계를 부모 클래스 - 자식 클래스 또는 슈퍼(super) 클래스 - 서브(sub) 클래스 관계라고 한다.
  • 클래스 상속 관계가 설정되면 가능한 일
    • 자식 클래스 A를 통해 부모 클래스 B의 정적 메소드와 정적 속성을 사용할 수 있다.
    • 부모 클래스 B의 인스턴스 메소드와 인스턴스 속성을 자식 클래스 A의 인스턴스에서 사용할 수 있다.

super

  • 자식 클래스에 부모 클래스와 같은 이름의 속성(메소드)을 정의한 경우 부모 클래스의 속성에 접근할 수 없는 문제가 생긴다.

    • 부모 클래스의 메소드를 확장해서 사용하기 위해 자식 클래스에서 일부러 같은 이름으로 정의해서 사용하는 경우를 메소드 오버라이딩(Method Overriding) 이라고 한다.
    • 이러한 경우에, super 키워드를 통해 부모 클래스의 속성에 직접 접근할 수 있다.
    class Melon {
      getColor() {
        return '제 색깔은 초록색입니다.';
      }
    }
      
    class WaterMelon extends Melon {
      getColor() {
        return super.getColor() + ' 하지만 속은 빨강색입니다.';
      }
    }
      
    const waterMelon = new WaterMelon();
    waterMelon.getColor(); // 제 색깔은 초록색입니다. 하지만 속은 빨강색입니다.
    
  • super 키워드의 작동 방식

    • 자식 클래스의 생성자 내부에서 super를 함수처럼 호출하면 부모 클래스의 생성자가 호출된다.
    • 정적 메소드 내부에서는 super.prop 처럼 써서 부모 클래스의 prop 정적 속성에 접근할 수 있다.
    • 인스턴스 메소드 내부에서는 super.prop 처럼 써서 부모 클래스의 prop 인스턴스 속성에 접근할 수 있다.
    class Person {
      constructor({name, age}) {
        this.name = name;
        this.age = age;
      }
      introduce() {
        return `제 이름은 ${this.name}입니다.`
      }
    }
      
    class Student extends Person {
      constructor({grade, ...rest}) {
        // 부모 클래스의 생성자를 호출할 수 있습니다.
        super(rest);
        this.grade = grade;
      }
      introduce() {
        // 부모 클래스의 `introduce` 메소드를 호출할 수 있습니다.
        return super.introduce() + ` 저는 ${this.grade}학년입니다.`;
      }
    }
      
    const s = new Student({grade: 3, name: '윤아준', age: 19});
    s.introduce(); // 제 이름은 윤아준입니다. 저는 3학년입니다.
    

클래스 상속과 프로토타입 상속

  • 클래스 상속은 내부적으로 프로토타입 상속 기능을 활용하고 있다.

    class Person {}
    class Student extends Person
    const student = new Student();
    

    클래스 상속의 프로토타입 체인

큐, 스택, 트리

큐 (Queue)

  • 데이터를 집어넣을 수 있는 선형(linear) 자료형

  • 먼저 집어넣은 데이터가 먼저 나온다. (First In First Out)

  • 데이터를 집어넣는 enqueue, 데이터를 추출하는 dequeue 작업이 가능하다.

  • 큐는 순서대로 처리해야 하는 작업을 임시로 저장해두는 버퍼(buffer)로서 많이 사용된다.

    • 동영상 파일을 재생하는 스트리밍을 할 때 버퍼에 영상을 조금씩 잘라서 저장해놓고 하나씩 보여주는데, 이 때의 버퍼가 큐 형식으로 되어있다.
  • JS에서 큐를 구현한 사례

    class Queue {
      constructor() {
        this._arr = [];
      }
      enqueue(item) {
        this._arr.push(item);
      }
      dequeue() {
        return this._arr.shift();
      }
    }
      
    const queue = new Queue();
    queue.enqueue(1);
    queue.enqueue(2);
    queue.enqueue(3);
    queue.dequeue(); // 1
    

스택 (Stack)

  • 데이터를 집어넣을 수 있는 선형(linear) 자료형

  • 나중에 집어넣은 데이터가 먼저 나온다. (Last In First Out)

  • 데이터를 집어넣는 push, 데이터를 추출하는 pop, 맨 나중에 집어넣은 데이터를 확인하는 peek 등의 작업을 할 수 있다.

  • 스택은 서로 관계가 있는 여러 작업을 연달아 수행하면서 이전의 작업 내용을 저장해 둘 필요가 있을 때 사용된다.

    • 윈도우에서 ctrl + z 기능이 스택을 통해 작동한다.
  • JS에서 스택을 구현한 사례

    class Stack {
      constructor() {
        this._arr = [];
      }
      push(item) {
        this._arr.push(item);
      }
      pop() {
        return this._arr.pop();
      }
      peek() {
        return this._arr[this._arr.length - 1];
      }
    }
      
    const stack = new Stack();
    stack.push(1);
    stack.push(2);
    stack.push(3);
    stack.pop(); // 3
    

트리 (Tree)

  • 여러 데이터가 계층 구조 안에서 서로 연결된 형태를 나타낼 때 사용된다.

  • JS에서 트리를 구현한 사례

    class Node {
      constructor(content, children = []) {
        this.content = content;
        this.children = children;
      }
    }
      
    const tree = new Node('hello', [
      new Node('world'),
      new Node('and'),
      new Node('fun', [
        new Node('javascript!')
      ])
    ]);
      
    function traverse(node) {
      console.log(node.content);
      for (let child of node.children) {
        traverse(child);
      }
    }
      
    traverse(tree);
    // hello world and fun javascript!
    

비동기 프로그래밍

Motivation - 타이머 API

  • 웹 브라우저에는 함수를 특정 시간이 지난 뒤에 실행시키거나, 함수를 주기적으로 실행시키는 작업을 할 수 있게 해주는 함수가 내장되어 있다.

    setTimeout(() => {
      console.log('setTimeout이 실행된 지 2초가 지났습니다.');
    }, 2000);
      
    setInterval(() => {
      console.log('3초마다 출력됩니다.');
    }, 3000);
    
  • setTimeoutsetInterval은 각각 타이머 식별자를 리턴한다. 이 식별자로 실행 중인 타이머를 취소할 수 있다.

    const timeoutId = setTimeout(() => {
      console.log('setTimeout이 실행된 지 2초가 지났습니다.');
    }, 2000);
      
    const intervalId = setInterval(() => {
      console.log('3초마다 출력됩니다.');
    }, 3000);
      
    clearTimeout(timeoutId);
    clearInterval(intervalId);
      
    // 아무것도 출력되지 않습니다.
    
  • 타이머 사용 시 주의할 점

    • 정확한 지연시간을 보장하지 않는다. (실제 지연시간과 약간의 차이가 존재한다)

      • 정확한 시간이 매우 중요한 게임이나 주식 관련 App에서는 JS를 쓰면 안된다 ㅠㅠ
    • 지연시간을 0으로 줘도 바로 실행시키는 것이 아니다. (JS 코드 실행 과정에 따라 실행된다.)

      setTimeout(() => {
        console.log('hello');
      }, 0);
          
      console.log('world');
          
      // 출력 결과:
      // world
      // hello
      

브라우저의 JavaScript 코드 실행 과정

  • 호출 스택 (Call Stack)
    • 호출 스택은 스택 형태의 저장소로, JavaScript 엔진은 함수 호출과 관련된 정보를 이곳에서 관리한다.
    • 호출 스택에 저장되는 각 항목을 실행 맥락(execution context)라고 부른다.
      • 실행 맥락에 저장되는 정보
        • 함수 내부에서 사용되는 변수
        • 스코프 체인
        • this가 가리키는 객체
    • 호출 스택에 실행 맥락이 존재하는 동안, 브라우저는 먹통이 된다. 브라우저는 60fps(1초에 60번)로 동작(화면을 그리기)하기 때문에, 대략 16ms 안에 코드의 실행을 완료하지 못하면 브라우저의 애니메이션이 끊기는 것 처럼 보인다.
  • 작업 큐 (Task Queue)
    • 모든 작업을 16ms 안에 처리할 수는 없다. 어떤 이벤트가 일어날 때까지 기다리거나 큰 데이터에 대한 계산이 완료될 때까지 기다리는데는 시간이 오래걸린다.
    • 그래서 오래 기다려야 하는 일을 처리하기 위해 임시 저장소인 작업 큐를 이용한다.
    • 처리 순서
      • 기다려야 하는 일을 JavaScript 엔진에서 직접 처리하는 것이 아니라 API를 통해 브라우저에 위임한다. 이 때, 일이 끝나면 실행시킬 콜백을 같이 등록한다.
      • 위임된 일이 끝나면, 그 결과와 콜백을 작업 큐에 추가한다.
      • 브라우저는 호출 스택이 비워질 때마다 작업 큐에서 가장 오래된 작업을 꺼내와서 해당 작업에 대한 콜백을 실행시킨다.
      • 브라우저는 이러한 과정을 끊임없이 반복한다. 이를 이벤트 루프(event loop)라고 한다.
  • 호출 스택과 작업 큐의 성질
    • 각 작업은 작업 큐에 쌓인 순서대로 실행된다.
    • 이미 작업 큐에 작업이 쌓여있다면, 뒤늦게 추가된 작업은 앞서 추가된 작업이 모두 실행된 다음에(호출 스택이 비워진 다음에) 실행된다.
    • 호출 스택이 비워지지 않는다면, 작업 큐에 쌓여 있는 작업을 처리할 수 없다.
    • 각 작업 사이에 브라우저는 화면을 새로 그릴 수 있다. (호출 스택이 비워지지 않으면 화면을 새로 그릴 수 없다.)

비동기 프로그래밍 (Asynchronous Programming)

  • 비동기 프로그래밍(Asynchronous Programming)

    • 어떤 일이 완료되기를 기다리지 않고(브라우저에 위임하고) 다음 코드를 실행해 나가는 프로그래밍 방식
    • 브라우저에서의 비동기 프로그래밍은 주로 통신과 같이 오래 걸리는 작업들을 브라우저에 위임할 때 이루어진다.
  • 동기식 프로그래밍(Synchronous Programming)

    • 어떤 일이 완료될 때까지 코드의 실행을 멈추고 기다리는 프로그래밍 방식
  • 콜백 (Callback)

    • 다른 함수의 인수로 넘기는 함수를 말한다.
    • map, forEach 등의 Array Function 들도 콜백이지만, 이때는 콜백이 동기식으로 호출된다. 이러한 콜백들은 주의해서 사용해야 한다.
    • 콜백은 복잡한 비동기 데이터 흐름을 표현하기가 어렵다는 단점이 있다. => 콜백 지옥(callback hell)
  • 프라미스(Promise)

    • 콜백의 문제를 해결하기 위한 여러 라이브러리들 중 Promise 패턴을 사용한 라이브러리들이 ES2015에서 표준화되었다.

    • 프라미스는 ‘언젠가 끝나는 작업’의 결과값을 담는 통과 같은 객체이다.

      • 프라미스 객체가 만들어지는 시점에는 그 통 안에 무엇이 들어갈지 모를수도 있다.
    • 비동기 작업을 하는 프라미스 객체는 Promise 생성자를 통해 만들 수 있다.

      const p = new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('2초가 지났습니다.');
          resolve('hello');
        }, 2000);
      });
      
      • 콜백 안에서 resolve를 호출하면 resolve에 인수로 준 값이 곧 프라미스 객체의 궁극적인 결과값이 된다.
    • 프라미스 객체의 결과값을 이용해 추가 작업을 하려면 then 메소드를 호출해야 한다. then 메소드에 콜백을 넘겨서 첫번째 인수로 들어온 결과값을 가지고 추가 작업을 할 수 있다.

      p.then(msg => {
        console.log(msg); // hello
      });
      
    • 중요!!!

      • then 메소드 자체도 프라미스 객체를 반환한다. (메소드 체인 가능)
      • 콜백에서 반환한 값이 곧 프라미스의 결과값이 된다.

쿠키의 필요성

  • 개별 클라이언트여러 요청에 걸친 정보의 유지
    • 장바구니
    • 로그인 / 로그아웃
    • 방문 기록
    • etc…
  • 서버가 응답을 통해 웹 브라우저에 저장하는 이름+값 형태의 정보
  • 웹 브라우저는 쿠키를 저장하기 위한 저장소를 가지고 있다.
  • 저장소는 자료의 유효기간접근 권한에 대한 다양한 옵션을 제공한다.

쿠키 전송 절차

  1. 서버는 브라우저에 저장하고 싶은 정보를 응답과 같이 실어 보낸다 (Set-Cookie 헤더)

    HTTP/1.1 200 OK
    Set-Cookie: cookieName=cookieValue; Secure; Max-Age=60000
    ...
    
  2. 브라우저는 같은 서버에 요청이 일어날 때마다 해당 정보를 요청에 같이 실어서 서버에 보낸다 (Cookie 헤더)

    GET / HTTP/1.1
    Cookie: cookieName=cookieValue; anotherName=anotherValue
    ...
    
  • Expires, Max-Age
    • 쿠키의 지속 시간 설정
  • Secure
    • HTTPS를 통해서만 쿠키가 전송되도록 설정
  • HttpOnly
    • 자바스크립트에서 쿠키를 읽지 못하도록 설정
    • JS로 쿠키를 조작하는 것은 위험한 일이다.
      • Cross-Site Scripting 공격 을 당할 수 있기 때문에 위험하다.
      • 해커가 스크립트를 넣어서 쿠키를 가로챌 수 있다.
  • Domain, Path
    • 쿠키의 scope 설정 (쿠키가 전송되는 URL을 제한)

쿠키의 한계점

  • US-ASCII 밖에 저장하지 못한다. 보통 percent encoding을 사용한다.
  • 4000 바이트 내외(영문 4000자, percent encoding된 한글 444자 가량) 밖에 저장하지 못한다.
  • 브라우저에 저장된다. 즉, 여러 브라우저에 걸쳐 공유되어야 하는 정보웹 브라우저가 아닌 클라이언트(모바일 앱)에 저장되어야 하는 정보를 다루기에는 부적절하다.
    • 보통, 인증토큰(Authentication Token)을 쿠키에 저장한다. (주로 로그인/로그아웃)

Ajax

  • 비동기적인 웹 애플리케이션의 제작을 위한 클라이언트측 웹 개발 기법
  • 요즘은 의미가 변형되어 웹 브라우저에서 XMLHttpRequest 혹은 fetch를 이용해서 보내는 HTTP 요청을 통칭하기도 한다.
  • HTTP Methods
    • POST (When Create)
    • GET (When Read)
    • PUT, PATCH (When Update)
    • DELETE (When delete)
  • Ajax Model

    Ajax model

  • Ajax의 장점

    • 화면 전체를 다시 로드하지 않고도 내용을 갱신할 수 있어 더 나은 사용자 경험을 제공한다.
    • 서버의 응답을 기다리는 동안에도 여전히 웹 애플리케이션을 사용할 수 있다.
    • 필요한 자원만 서버에서 받아오게 되므로 트래픽이 줄어든다.
  • Ajax의 단점

    • 클라이언트 구현이 굉장히 복잡해진다. (FE 개발자가 할 일이 많아진다… ㅠㅠ)
  • Axios

    • Promise based HTTP client
    • 브라우저와 Node.js에서 모두 사용 가능하다.
    • XMLHttpRequest, fetch에 비해 사용하기 편하고 기능이 더 많다.

CORS

Same-origin Policy (동일 출처 정책)

  • 웹페이지에서 리소스를 불러올 때, 리소스의 출처가 웹페이지의 출처와 같으면 안전하다고 보고, 출처가 다르면 해당 리소스는 안전하지 않다고 보는 원칙
  • 여기서 ‘출처’‘프로토콜 + 도메인 + 포트번호’의 결합을 가리킨다. 즉, 세 개가 다 같아야 동일 출처라고 할 수 있고, 셋 중에 하나라도 다르면 동일 출처로 간주되지 않는다.
  • 웹 보안의 기본 원칙으로, 웹 브라우저의 많은 요소에 적용된다.

Content-Security-Policy

  • Content-Security-Policy 헤더를 이용하면, 동일하지 않은 출처에 대한 리소스를 불러올지 말지 결정할 수 있다.
  • 특정 주소에서 불러오는 리소스만 허용하는 등의 방법을 이용한다.
    • 타 출처(주소)로부터 리소스를 불러오는 경우를 허용하지 않는다.
    • ex) 페이스북은 이미지를 업로드만 할 수 있고 주소를 통해 다른 출처의 이미지를 불러올 수 없다.

CORS (Cross-Origin Resource Sharing)

  • Ajax 요청을 보낼 때 적용되는 보안 정책
  • 클라이언트 측 cross-origin 요청을 안전하게 보낼 수 있는 방법을 정한 표준
  • 스크립트가 전혀 다른 출처를 갖는 API 서버를 사용하려고 하는 상황에서는 뭔가 추가적인 처리를 해줘야 한다는 것이다.

Cross-origin 요청

  • IE8 이상의 모던 웹 브라우저는 cross-origin 요청에 대해 여러가지 제한을 두고 있다.
  • cross-origin 요청을 허용하려면, 서버가 특별한 형태의 응답을 전송해야 한다.
  • 만약 서버가 cross-origin 요청을 허용하지 않으면, 웹 브라우저는 에러를 발생시킨다.

CORS에 관여하는 응답 헤더

  • Access-Control-Allow-Origin
  • Access-Control-Expose-Headers
  • Access-Control-Max-Age
  • Access-Control-Allow-Credentials
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers
  • 이러한 헤더와 관련된 에러메세지가 뜬다면 백엔드 개발자에게 CORS 문제라고 말하자!!

CORS에 관여하는 요청 헤더

  • Origin
  • Access-Control-Request-Method (preflighted 전용)
  • Access-Control-Request-Headers (preflighted 전용)

CORS - Safe, Unsafe

  • GET, HEAD 요청은 safe(읽기 전용)이기 때문에 서버에 요청이 도달한다고 해서 서버의 상태에 영향을 미칠 일은 없으므로 웹 브라우저는 일단 해당 요청을 보내본다. 만약 서버가 cross-origin 요청을 허용한다고 응답하면 응답을 그대로 사용하고, 그렇지 않으면 에러를 낸다.
  • POST, PUT, PATCH, DELETE 등의 메소드는 요청이 서버에 전송되는 것 자체가 위험하므로 실제 요청을 보내기 전에 서버가 cross-origin 요청을 허용하는지를 알아보기 위해 시험적으로 요청을 한 번 보내본다. 이 요청을 preflighted request 라고 한다.
    • 단, 기존 HTML form의 동작방식인 application/x-www-form-urlencoded 혹은 multipart/form-data 형태의 POST 요청은 preflighted request가 발생하지 않는다.

CORS with credentials

  • cross-origin 요청에는 기본적으로 쿠키가 포함되지 않으나, XMLHttpRequest 혹은 fetch를 통해서 요청을 보낼 때 쿠키를 포함시키는 옵션을 줄 수 있고, 이 때 CORS 요건이 더 엄격해진다.
    • Access-Control-Allow-Credentials 헤더 설정이 필요하다.
    • 단, `Access-Control-Allow-Origin 헤더에 와일드카드 허용 안된다.

CORS 관련 이슈 해결을 위해서는…

  1. 프론트엔드와 API 서버를 같은 도메인으로 제공한다. (둘을 똑같이 맞춘다.)
  2. 불가피하게 둘을 다른 도메인으로 제공해야 한다면
    • CORS를 허용한다. (CORS 미들웨어를 사용해서)
    • CORS를 허용하는 경우, 쿠키를 쓸 수는 있으나 보안상 허점이 생기기 쉽고 사용하기도 불편하므로 보통 JWT와 같은 토큰 방식의 인증을 사용한다.

Access Token & JWT

쿠키의 단점

  • 쿠키를 지원하는 클라이언트에서밖에 사용할 수 없다.
  • 적절히 관리되지 않은 쿠키는 보안에 취약하며, 관리를 하려고 해도 CORS 대응이 복잡하다.

Token Based Auth

  • 토큰이란, 사용자의 자격증명(아이디, 패스워드 등)을 통해 인증이 이루어진 후, 특정 자원에 대한 자격증명으로서 대신 사용되는 인증 수단을 말한다.
  • 서버에 요청을 할 때마다 토큰을 요청에 직접 포함시켜서 전송한다. (주로 Authorization 헤더에 넣어서 전송)

Cookie VS Token

토큰 사용의 장점

  • 쿠키를 지원하지 않는 클라이언트에서도 편하게 사용할 수 있다.
  • 쿠키를 사용하지 않음으로써 CORS 관련 문제를 회피할 수 있다.

토큰 사용의 단점

  • 매 요청에 토큰이 포함되어야 하므로 적당히 짧은 길이를 유지해야 한다.
  • 토큰 유출에 대한 대비책이 필요하다.
    • 토큰에 유효기간을 두거나, 유출된 토큰을 강제로 무효화하는 등의 방법을 사용한다.
  • 쿠키와는 다르게, 클라이언트 개발자가 직접 토큰을 저장하고 관리해야 한다.

Web Storage

  • 브라우저에서 키-값 쌍을 저장할 수 있는 저장소
  • 쿠키에 비해 사용하기 편리하고 저장 가능한 용량도 크다. (10MB 가량)
  • 브라우저 탭이 닫히면 내용이 삭제되는 sessionStorage, 브라우저 탭이 닫혀도 내용이 유지되는 localStorage가 있다.

보안 상 주의사항

  • (당연히) HTTPS를 사용해야 한다.
  • 토큰을 localStorage에 저장하게 되면 자바스크립트로 토큰을 탈취할 수 있게 되므로, 웹사이트에 악성 스크립트를 삽입하는 공격(XSS)에 노출되지 않도록 신경써야 한다.

JSON Web Token

  • jwt.io

  • 최근 널리 사용되고 있는 토큰 형식의 표준
  • 토큰 안에 JSON 형식으로 정보를 저장한다.
  • 보안을 위해 서명 또는 암호화를 사용할 수 있다.

참고 링크

댓글남기기