[카테고리:] IT

다양한 IT 기술에 대한 내용을 공유합니다.

  • [Network] REST(Representational State Transfer)

    [Network] REST(Representational State Transfer)

    REST

    REST는 웹에서 데이터를 전송하고 처리하는 방법을 정의한 인터페이스를 말하며, 모든 데이터 구조와 처리 방식은 REST에서 URL을 통해 정의된다. 때문에 매우 직관적이고 이해하기 쉬우며 사용자에게 더 쉽게 서비스를 제공할 수 있다.

    RESTfull

    RESTfull API라고도 하며, HTTP 프로트콜과 REST의 원칙을 사용하여 구현된 웹 서비스이다. 리소스(Resource)는 모든 인터넷 환경에서 사용이 가능한 표준화 된 형식(일반적으로 XML 또는 JSON)으로 표현된다.

    REST 중심 규칙

    URL는 정보의 자원을 표현해야 한다

    URL은 의미를 명확히 전달하기 위해 명사로 구성한다. 예를 들어 요청 URL이 /user면 사용자 정보에 관한 요청이며, /post라면 게시글에 관한 요청하는 것으로 추측이 가능하다.

    자원에 대한 행위는 HTTP Method로 표현한다

    REST에선 HTTP Method를 사용하며 주요 메서드는 아래와 같다.

    HTTP Method

    • GET(자원 정보 조회): 서버의 자원을 가져올 때 사용한다. 요청의 본문에 데이터를 넣지 않으며 쿼리스트링을 사용하여 서버에 데이터를 보낸다. 또한 GET 메서드는 브라우저에서 캐싱(기억)할 수도 있다.


      쿼리스트링
      URL에 미리 협의된 데이터를 파라미터를 통해 넘기는 것을 말한다.


    • POST(자원 생성): 서버에 자원을 새로 등록할 때 사용된다. 요청의 본문에 새로 등록할 데이터를 넣어 보낸다.


    • PUT(자원 업데이트): 서버의 자원을 요청에 있는 자원으로 치환할 때 보낸다. 요청의 본문에 치환할 데이터를 넣어 보낸다.


    • PATCH(자원 일부 업데이트): PUT이 자원 전체를 업데이트한다면 PATCH는 자원 일부를 수정할 때 사용한다. 요청의 본문에 수정할 데이터를 넣어 보낸다.


    • DELETE(자원 삭제): 자원을 삭제할 때 사용되며, 요청의 본문에 데이터를 넣지 않는다.


    • OPTIONS(옵션 설명): 요청을 하기 전에 통신 옵션을 설명하기 위해 사용된다.


    REST 구성 요소

    구성 요소내용표현 방법
    Resource자원HTTP URI
    Verb자원에 대한 행위HTTP Method
    Representations자원에 대한 행위의 내용HTTP Message Pay Load

    REST 특징

    클라이언트/서버 구조(Client – Server)

    자원이 있는 서버와 자원을 요청하는 클라이언트의 구조를 가진다. REST 서버는 클라이언트에게 API만 제공, 클라이언트는 사용자 인증이나 컨텍스트(세션, 로그인 정보)등을 직접 관리하는 구조로 각각의 역할이 확실하게 구분되어 일관적인 인터페이스로 분리되고 작동할 수 있게 한다.

    무상태성(Stateless)

    HTTP는 무상태 프로토콜 이므로 REST 역시 무상태성을 가진다. 다시 말해 작업을 위한 상태 정보를 따로 저장하고 관리하지 않는다. 세션이나 쿠키를 별도로 저장, 관리하지 않기 때문에 API 서버는 들어오는 요청만 단순히 처리하면 된다. 그래서 서비스의 자유도가 높아지며, 불편한 정보를 관리하지 않음으로써 구현이 단순해진다.

    캐시 처리 가능(Cachealble)

    REST에서는 웹 표준 HTTP 프로토콜을 그대로 사용하므로, 웹의 기존의 인프라를 그대로 활용 가능하다. 때문에 REST에서도 캐싱 기능을 사용할 수 있다.

    계층화(Layered System)

    REST 서버는 다중 계층으로 구성될 수 있으며 보안, 로드 밸런싱, 암호화 계층을 추가해 구조를 변경할 수 있다. 또한 Proxy, Gateway와 같은 네트워크 기반의 중간매체를 사용할 수 있다. 하지만 클라이언트는 서버와 직접 통신하는지 중간 서버와 통신하는지 알 수 없다.

    자체 표현 구조(Self-descriptiveness)

    REST는 JSON 메세지 포멧을 이용하여 직관적으로 이해할 수 있고, 그 요청이 어떤 행위를 하는지 쉽게 알수 있는 자체 표현 구조로 되어있다.

    JSON은 하나의 옵션일뿐, 메시지 포맷을 꼭 JSON으로 적용해야할 필요는 없다.

    유니폼 인터페이스(Uniform Interface)

    URL에 대한 요청을 통일되고 한정적으로 수행하는 이키텍쳐 스타일을 의미하며, HTTP 표준에만 따른다면 모든 플랫폼에서 사용이 가능하다.

    REST 장점

    쉬운 사용

    HTTP 프로토콜 인프라를 그대로 사용하므로 REST API 사용을 위한 별도의 인프라를 구축할 필요가 없다.

    클라이언트와 서버의 명확한 역할 분리

    클라이언트는 REST API만을 통해 서버와 정보를 주고받는다. 때문에 REST의 무상태성에 따라 사용자의 컨텍스트를 따로 관리할 필요가 없다.

    특정 데이터 표현 사용가능

    REST는 헤더 부분에 URL 처리 메서드를 명시하고 실제 필요한 데이터는 BODY에 표현할 수 있도록 분리하여 JSON , XML 등 원하는 언어로 사용이 가능하다.

    REST 단점

    메서드의 한계

    REST API는 HTTP 메서드를 이용하여 URI를 표현하기 때문에 쉬운 사용이 가능한 장점이 있지만 반대로 메서드 형태가 제한적이라는 단점이 있다.

    표준이 없음

    REST는 설계 가이드일 뿐 표준이 아니며 명확한 표준이 없다.

    References

    REST
    Node.js 교과서
    REST란
    REST API의 이해와 설계-#1 개념 소개
    [Server] Restful API란?
    REST 아키텍처를 훌륭하게 적용하기 위한 몇 가지 디자인 팁

  • [JavaScript] 전개연산자(Spread Operator)

    [JavaScript] 전개연산자(Spread Operator)

    정의

    ECMAScript6(2015)에서 새로 추가된 전개연산자(Spread Operator)란 객체나 배열의 값을 하나 하나 넘기는 용도로 사용할 수 있다. 전개연산자를 사용하는 방법은 점 세개(...)를 붙이면 된다.

    직관적이고, 배열의 아무 곳에 추가 가능하다.

    ES5 배열 내용 조합

    ES5 에서는 배열의 내용을 합쳐 새로운 배열을 만들기 위해서 concat 메서드를 활용한다.

    const arr1 = [1, 2, 3];
    const arr2 = [4, 5, 6];
    const arr3 = [7, 8, 9];
    const arrWrap = arr1.concat(arr2, arr3);
    
    console.log(arrWrap); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
    

    arr1 배열에 concat 메서드를 사용하여, 배열 arr2와 arr3를 배열에 이어붙였다.

    ES6 배열 내용 조합

    const arr1 = [1, 2, 3];
    const arr2 = [4, 5, 6];
    const arr3 = [7, 8, 9];
    const arrWrap = [...arr1, ...arr2, ...arr3];
    
    console.log(arrWrap); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
    

    전개연산자를 활용하여 새로운 배열을 만들었다. concat 메서드를 사용한 코드보다 간결하고, 가독성도 개선되었다.

    const arr = [4, 5, 6];
    const arrWrap = [1, 2, 3, ...arr, 7, 8, 9]
    
    console.log(arrWrap); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
    

    concat 메서드는 인자로 전달받은 값 순으로 기존 배열 끝에서부터 값을 추가하지만, 전개연산자는 위처럼 배열의 아무 곳에나 추가 할 수 있다.

    전개연산자로 할당하면 2차원 형태가 되지 않는다.

    배열의 경우

    concat 메서드로 새로운 배열을 만들어내는 것이 아닌, 기존 배열 요소에 값을 추가한다면 push 메서드를 사용할 것이다.

    const arr1 = [1, 2, 3];
    const arr2 = [4, 5];
    arr1.push(arr2);
    
    console.log(arr1); // [1, 2, 3, [4, 5]]
    

    arr1 배열에 arr2 배열을 할당했지만 arr2 배열 전체가 들어가 2차원 배열이 되었다. 이 경우 기존 자바스크립트에서는 배열 객체의 프로토타입 매서드인 push.apply를 사용해야 한다.

    const arr1 = [1, 2, 3];
    const arr2 = [4, 5];
    Array.prototype.push.apply(arr1, arr2);
    
    console.log(arr1); // [1, 2, 3, 4, 5]
    

    원하는 결과를 얻었지만 코드가 복잡하다. 하지만 전개연산자를 활용하면 쉽게 구현이 가능하다.

    const arr1 = [1, 2, 3];
    const arr2 = [4, 5];
    arr1.push(...arr2);
    
    console.log(arr1); // [1, 2, 3, 4, 5]
    

    객체의 경우

    객체의 경우도 같다.

    const obj1 = {
      a: 'A',
      b: 'B'
    };
    const obj2 = {
      c: 'C',
      d: 'D'
    };
    const objWrap = {obj1, obj2};
    console.log(objWrap);
    
    {
      obj1: {
        a: 'A',
        b: 'B'
      },
      obj2: {
        c: 'C',
        d: 'D'
      }
    }
    

    obj1 객체와 obj2 객체를 하나의 objWrap 객체로 묶으면 객체 각각의 값이 아닌, 객체 자체가 들어가 2차원 객체가 되었다.

    const obj1 = {
      a: 'A',
      b: 'B'
    };
    const obj2 = {
      c: 'C',
      d: 'D'
    };
    const objWrap = {...obj1, ...obj2};
    console.log(objWrap);
    
    {
      a: 'A',
      b: 'B',
      c: 'C',
      d: 'D'
    }
    

    전개연산자를 사용하면 객체 자체가 할당되는 것이 아닌, 각각의 값이 할당 된다.

    전개연산자를 이용한 복사에는 1차원에서만 유효하다.

    위에서 전개연산자로 할당하면 2차원 배열이 되지 않는다고 했다. 하지만 2차원 이상의 배열을 할당할 땐 1차원 요소만 같은 1차원 레벨로 할당이 되고 2차원 이상의 배열은 그대로 들어간다.

    const arr1 = [4, 5, [6, 7]];
    const arr2 = [1, 2, 3, ...arr1];
    console.log(arr2); // [1, 2, 3, 4, 5, [6, 7]]
    

    기존 배열을 보존해야 할 때 유용하다.

    ES5 배열 요소를 역순으로 변경

    전개연산자는 원본 배열을 그대로 유지하면서 새로운 배열을 만든다. 예를 들어 reverse 메서드는 배열의 각 요소를 역순으로 바꾸는 메서드인데, 기존 배열도 바꿔버리는 단점이 있다.

    const arr1 = [1, 2, 3];
    const arr2 = arr1.reverse();
    
    console.log(arr1); // [3, 2, 1]
    console.log(arr2); // [3, 2, 1]
    

    원본 배열을 수정할 의도가 있었으면 문제가 되지 않지만, 원본 배열은 그대로 두고 배열 요소의 순서를 뒤집은 새로운 배열이 필요하다면 상황이 복잡해진다. 이 상황에서 전개연산자를 사용하면 편리해진다.

    ES6 배열 요소를 역순으로 변경

    const arr1 = [1, 2, 3];
    const arr2 = [...arr1].reverse();
    
    console.log(arr1); // [1, 2, 3]
    console.log(arr2); // [3, 2, 1]
    

    원본 배열은 그대로 유지하면서 새로운 배열을 만들었다.

    배열의 나머지 요소를 할당할 수 있다.

    비구조화 할당과 전개연산자를 사용하여 배열의 나머지 요소를 할당 받을 수 있다.

    const [first, second, ...rest] = [1, 2, 3, 4, 5];
    console.log(first); // 1
    console.log(second); // 2
    console.log(rest); // [3, 4, 5]
    

    변수 first과 second의 각각의 인덱스 값에 맞는 값이 차례로 들어가고(1, 2), rest에는 할당 받지 못한 나머지 값들이 대입된다.

    References

    ES6 문법으로 다시 시작하는 자바스크립트
    [번역]ES6 축약코딩 기법 19가지
    [JavaScript] 전개연산자 ( Spread Operator )
    3. 배열을 좀 더 직관적으로 활용, 전개연산자 (spread operator)
    전개 구문

  • [JavaScript] 얕은 복사, 깊은 복사

    [JavaScript] 얕은 복사, 깊은 복사

    깂은 복사와 얕은 복사에 대해 알아보겠다. 이 글의 초반 내용은 이전 포스팅의 (원시 타입과 참조 타입의 차이)과 맥락이 비슷하며, 위 포스팅은 원시 타입과 참조 타입의 차이점이라면 아래는 참조 타입의 깊은 복사하는 방법에 대해 알아보도록 하겠다.

    얕은 복사(shallow copy)

    얖은 복사는 참조(주소)값의 복사를 나타낸다.

    const obj = { vaule: 1 }
    const newObj = obj;
    
    newObj.vaule = 2;
    
    console.log(obj.vaule); // 2
    console.log(obj === newObj); // true
    

    obj 객체를 새로운 newObj 객체에 할당하였으며 이를 참조 할당이라 부른다. 복사 후 newObj 객체의 value값을 변경하였더니 기존의 obj.value값도 같이 변경된 것을 알 수 있다. 두 객체를 비교해도 true로 나온다. 이렇게 자바스크립트의 참조 타입은 얕은 복사가 된다고 볼 수 있으며, 이는 데이터가 그대로 생성되는 것이 아닌 해당 데이터의 참조 값(메모리 주소)를 전달하여 결국 한 데이터를 공유하는 것이다.

    깊은 복사(deep copy)

    깊은 복사는 값 자체의 복사를 나타낸다.

    let a = 1;
    let b = a;
    
    b = 2;
    
    console.log(a); // 1
    console.log(b); // 2
    console.log(a === b); // false
    

    변수 a를 새로운 b에 할당하였고 b 값을 변경하여도 기존의 a의 값은 변경되지 않는다. 두 값을 비교하면 false가 출력되며 서로의 값은 단독으로 존재하다는 것을 알 수 있다. 이렇게 자바스크립트의 원시 타입은 깊은 복사가 되며, 이는 독립적인 메모리에 값 자체를 할당하여 생성하는 것이라 볼 수 있다.

    객체의 깊은 복사

    객체를 그대로 복사하여 사용할 경우 기존 객체의 원본 데이터가 더럽혀 질 수 있기 때문에 객체의 깊은 복사는 매우 중요하다. 객체를 깊이 복사하는 방법에 대해 몇가지 알아보자.

    Object.assign()

    Object.assign() 메서드를 활용하는 방법이다.

    문법 Object.assign(생성할 객체, 복사할 객체) 메서드의 첫번째 인수로 빈 객체를 넣어주며, 두번째 인수로 할당할 객체를 넣으면 된다.

    const obj = { a: 1 };
    const newObj = Object.assign({}, obj);
    
    newObj.a = 2;
    
    console.log(obj); // { a: 1 }
    console.log(obj === newObj); // false
    

    새로운 newObj 객체를 Object.assign() 메서드를 사용하여 생성하였으며, newObj.a 값을 변경하여도 기존의 obj는 변하지 않았다. 서로의 객체를 비교해도 false로 뜨며 서로 참조값이 다르다는 것을 알 수 있다.

    Object.assign()는 2차원 객체는 깊은 복사가이루어지지 않는다

    하지만 Object.assign()를 활용한 복사는 완벽한 깊은 복사가 아니다.

    const obj = {
      a: 1,
      b: {
        c: 2,
      },
    };
    

    위처럼 obj 객체의 b 프로퍼티의 값으로 { c: 2 } 객체를 가진 2차원 객체일 경우는 경우는 어떨까?

    const obj = {
      a: 1,
      b: {
        c: 2,
      },
    };
    
    const newObj = Object.assign({}, obj);
    
    newObj.b.c = 3;
    
    console.log(obj); // { a: 1, b: { c: 3 } }
    console.log(obj.b.c === newObj.b.c); // true
    

    2차원 객체를 newObj에 복사하고, newObj.b.c의 값을 변경하였다. 기존 obj 객체를 출력해보면 newObj.b.c의 값도 3으로 변경되었다. 복사된 하위 객체 { c: 2 }도 결국 객체이기 때문에 얕은 복사가 이루어진 것이다. 이는 Object.assign() 메서드의 한계이며, 전개연산자(Spread Operator) 를 이용한 객체의 복사에도 같은 문제가 있다.

    전개연산자(Spread Operator)

    const obj = { a: 1 };
    const newObj = Object.assign({}, obj);
    
    newObj.a = 2;
    
    console.log(obj); // { a: 1 }
    console.log(obj === newObj); // false
    

    전개연산자를 활용해도 객체의 깊은 복사가 가능하다.

    const obj = {
      a: 1,
      b: {
        c: 2,
      },
    };
    
    const newObj = { ...obj };
    
    newObj.b.c = 3;
    
    console.log(obj); // { a: 1, b: { c: 3 } }
    console.log(obj.b.c === newObj.b.c); // true
    

    하지만 Object.assign()와 마찬가지로 2차원 객체는 얕은 복사가 되는 것을 확인할 수 있다.

    JSON 객체 메서드를 이용

    객체의 깊은 복사를 위해 JSON 객체의 stringify(), parse() 메서드를 사용할 수 있다.

    문법 JSON.stringify() 메서드는 인수로 객체를 받으며 받은 객체는 문자열로 치환되며, JSON.parse() 메서드는 문자열을 인수로 받으며, 받은 문자열을 객체로 치환한다.

    const obj = {
      a: 1,
      b: {
        c: 2,
      },
    };
    
    const newObj = JSON.parse(JSON.stringify(obj));
    
    newObj.b.c = 3;
    
    console.log(obj); // { a: 1, b: { c: 2 } }
    console.log(obj.b.c === newObj.b.c); // false
    

    obj 객체를 JSON.stringify() 메서드를 이용하여 문자열로 변환한 뒤 다시 JSON.parse() 메서드로 객체로 변환하였다. 문자열로 변환한 뒤 다시 객체로 변환하였기에 2차원 객체에 대한 참조가 사라졌다. 하지만 이 방법도 2가지 문제가 있는데, 다른 방법에 비해 성능이 느린 점과 JSON.stringify() 메서드는 함수를 만났을 때 undefined로 처리한다는 점이다.

    const obj = {
      a: 1,
      b: {
        c: 2,
      },
      func: function() {
          return this.a;
      }
    };
    
    const newObj = JSON.parse(JSON.stringify(obj));
    
    console.log(newObj.func); // undefined
    

    복사된 newObjfunc가 없고 undefined로 출력되고 있다.

    커스텀 재귀 함수

    이 문제를 원칙적으로 해결하려면 직접 깊은 복사를 구현하는 커스텀 재귀 함수를 사용하는 것이다.

    function deepCopy(obj) {
      if (obj === null || typeof obj !== "object") {
        return obj;
      }
    
      let copy = {};
      for (let key in obj) {
        copy[key] = deepCopy(obj[key]);
      }
      return copy;
    }
    
    const obj = {
      a: 1,
      b: {
        c: 2,
      },
      func: function () {
        return this.a;
      },
    };
    
    const newObj = deepCopy(obj);
    
    newObj.b.c = 3;
    console.log(obj); // { a: 1, b: { c: 2 }, func: [Function: func] }
    console.log(obj.b.c === newObj.b.c); // false
    

    deepCopy 함수의 인수로 obj 객체를 넣었다. 인수값이 객체가 아닌 경우는 그냥 반환하며, 객체인 경우 객체의 값 만큼 루프를 돌며 재귀를 호출하여 복사된 값을 반환한다. 복사된 newObj 객체를 보면 2차원 객체의 값도 깊은 복사가 이루어 졌으며, 객체의 함수도 제대로 표현되는 것을 확인할 수 있다. 하지만 이미 객체의 깊은 복사를 위한 오픈 소스가 존재하며 lodash 모듈의 cloneDeep()을 이용하면 된다.

    lodash 모듈의 cloneDeep()

    lodash 모듈의 cloneDeep() 메서드를 이용하여 객체의 깊은 복사가 가능하다. 해당 모듈을 설치해 준 뒤 아래 코드를 실행시켜 보자.

    & npm i lodash
    
    const lodash = require("lodash");
    
    const obj = {
      a: 1,
      b: {
        c: 2,
      },
      func: function () {
        return this.a;
      },
    };
    
    const newObj = lodash.cloneDeep(obj);
    
    newObj.b.c = 3;
    console.log(obj); // { a: 1, b: { c: 2 }, func: [Function: func] }
    console.log(obj.b.c === newObj.b.c); // false
    

    간단히 객체의 깊은 복사를 구현할 수 있다. 실제로 웹 개발을 하다보면 lodash 모듈은 흔히 사용되며, 가장 손쉽게 객체의 깊은 복사를 해결하는 방법이라 할 수 있다.

    References

    자바스크립트 객체 복사하기
    Javascript 깊은 복사의 함정
    [Java Script] 얕은 복사와 깊은 복사
    JavaScript로 Deep Copy 하는 여러 방법
    Javascript:Shallow and Deep Copy :: 마이구미

  • [JavaScript] 화살표 함수(Arrow Function)

    [JavaScript] 화살표 함수(Arrow Function)

    정의

    ECMAScript6(2015)에서 새로 추가된 화살표 함수(Arrow Function)는 function 키워드 대신 화살표(=>)를 사용하여 보다 간략한 방법으로 함수를 선언할 수 있다.

    기존 함수

    함수 표현식

    var func = function() {
      ...
    };
    

    함수 선언식

    function func() {
      ...
    }
    

    화살표 함수

    const func = () => {
      ...
    };
    

    화살표 함수의 기본 문법

    // 매개 변수가 없을 경우
    const func = () => {
      ...
    };
    
    // 매개변수가 한 개인 경우, 소괄호를 생략할 수 있다.
    const func = x => {
      ...
    };
    
    // 매개변수가 여러 개인 경우, 소괄호를 생략할 수 없다.
    const func = (x, y) => {
      ...
    }
    
    // 간단하게 한줄로 표현할 땐 중괄호를 생략할 수 있으며 암묵적으로 값이 반환된다. 아래 두 표현은 같다.
    const func = (x, y) => x + y;
    const func = (x, y) => { return x + y; }
    
    // 객체를 반환시 중괄호를 생략하면 소괄호를 사용한다. 아래 두 표현은 같다.
    const func = (x) => { return { a: x } };
    const func = (x) => ({ a: x });
    

    화살표 함수의 호출

    화살표 함수는 익명 함수로만 사용할 수 있다. 따라서 함수를 호출하기 위해서는 함수 표현식을 사용한다.

    함수 표현식

    // ES5
    var func = function(x, y) {
      return x + y;
    }
    console.log(func(5, 10)); // 15
    
    // ES6
    const func = (x, y) => {
      return x + y;
    }
    console.log(func(5, 10)); // 15
    

    콜백 함수

    콜백 함수의 경우 함수 표현식보다 표현이 간결하다.

    // ES5
    var arr = [1, 2, 3];
    var result = arr.map(function(x) {
      return x + x;
    });
    console.log(result); // [2, 4, 6]
    
    // ES6
    const arr = [1, 2, 3];
    const result = arr.map(x => x + x);
    console.log(result); // [2, 4, 6]
    

    this

    기존의 function 키워드로 생선된 일반 함수와 화살표 함수의 큰 차이점 중 하나는 this이다.

    function 키워드 함수의 this

    function 키워드로 생성된 함수는 함수가 어떻게 호출되었는지에 따라 this가 바인딩할 객체가 동적으로 결정된다.

    일반 함수의 this

    일반 함수(여기서 일반 함수란 중첩 함수나 객체의 함수인 메서드, 콜백 함수가 아닌 전역 스코프에 있는 함수를 말한다.)를 호출하게 되면 this는 전역 객체인 window를 바인딩 한다.

    function func() {
        console.log(this); // window
    }
    func();
    

    생성자 함수의 this

    하지만 new 키워드를 사용하여 생성자함수 호출 방식으로 obj 객체를 생성하면 함수 안에서의 this는 생성자 함수를 바인딩 한다.

    function Func() {
        console.log(this); // Func {}
    }
    var obj = new Func();
    

    메서드의 this

    function 키워드로 만들어진 메서드의 this는 자신을 포함하는 객체를 바인딩 한다.

    var obj = {
        myName: '나나',
        getName: function() {
            console.log(this); // {myName: "나나", getName: ƒ}
            console.log(this.myName); // 나나
        }
    }
    obj.getName();
    

    obj 객체의 getName 메서드 안에서의 this는 obj 객체를 바인딩 하고 있다. this.myName 호출하면 값이 제대로 출력된다.

    화살표 함수의 this

    화살표 함수는 자신의 this를 바인딩하지 않고 언제나 상위 스코프인 this를 바인딩 한다. 이를 Lexical this 라고 한다.

    일반 함수의 this

    일반 함수의 경우 function 키워드와 마찬가지로 전역 객체인 window를 바인딩 한다.

    const func = () => {
        console.log(this); // window
    }
    func();
    

    생성자 함수의 this

    화살표 함수는 생성자 함수로 사용할 수 없다.

    const Func = () => {
        console.log(this);
    }
    const obj = new Func(); // Uncaught TypeError: Func is not a constructor
    

    기존의 function 키워드로 만든 일반적인 생성자 함수는 prototype 프로퍼티를 가지며, prototype 프로퍼티가 가르키는 프로토 타입 객체의 constructor를 사용한다.

    function Func() {}
    console.log(Func.prototype); // {constructor: ƒ}
    

    하지만 화살표 함수는 prototype 프로퍼티를 가지고 있지 않다.

    const Func = () => {}
    console.log(Func.prototype) //undefined
    

    메서드의 this

    var obj = {
        myName: '나나',
        getName: () => {
            console.log(this); // window
            console.log(this.myName); // undefined
        }
    }
    obj.getName();
    

    화살표 함수로 만들어진 메서드의 this는 자신을 포함하는 객체를 바인딩하지 않고 window를 바인딩하기 때문에 화살표 함수는 메서드에 적합하지 않다.

    References

    화살표 함수
    JavaScript – 화살표 함수(Arrow function)
    Arrow Function(화살표 함수)이란? :: 마이구미
    ES6 화살표 함수(arrow function) 변경점 요약 (사용법, this등)

  • [React] Puppeteer, React, Express를 활용한 크롤러 및 Heroku에 배포

    [React] Puppeteer, React, Express를 활용한 크롤러 및 Heroku에 배포

    Puppeteer를 활용하면 웹 크롤러 패널을 만들 수 있다. 클라이언트는 React, 서버는 Express를 사용하고 로컬에서 작업이 끝나면 Heroku에 배포까지 해보자. 결과물과 소스는 아래에서 확인할 수 있다.

    결과물은 헤로쿠에 배포되었기 때문에 처음 페이지가 열릴때 로딩시간이 10초에서 30초정도 걸릴수 있다.

    Puppeteer

    Puppeteer는 Google Chrome 개발팀에서 직접 개발한 Chrome(혹은 Chromium) 렌더링 엔진을 이용하는 node.js 라이브러리이다. Puppeteer는 headless 모드를 지원하며, 이는 브라우저를 띄우지 않고 렌더링 작업을 가상으로 진행하고 실제 브라우저와 동일하게 동작한다. Puppeteer는 다양한 기능을 가지고 있으며 아래와 같은 기능들이 있다.

    • 웹페이지의 스크린샷과 PDF를 생성한다.
    • SPA(단일 페이지)를 크롤링하고 미리 렌더링된 콘텐츠(예: SSR)를 생성한다.
    • 폼 입력, UI 테스트, 키보드 입력 등을 자동화 할 수 있다.
    • 최신 자바스크립트 및 브라우저 기능을 이용해 최신버전의 크롬에서 직접 테스트할 수 있다.
    • 사이트의 Timeline Trace를 기록하여 성능이나 문제를 진단할 수 있다.
    • 크롬 확장 프로그램을 테스트 할 수 있다.

    프로젝트 초기화

    이 부분은 [Express] Express + React 연동 및 Heroku에 배포하기 포스팅과 비슷한 부분이 많기 때문에 각 단계의 추가 설명 없이 진행하도록 하겠다.

    디렉토리 생성 및 필요 모듈 설치

    디렉토리를 생성하고 이그노 파일을 생성한 뒤 npm 초기화 및 필요한 모듈을 설치한다.

    $ mkdir my-app
    $ cd my-app 
    $ echo node_modules > .gitignore
    $ npm init -y
    $ npm install express nodemon concurrently
    

    이제 서버로 사용할 index.js 파일을 생성하고 아래 내용을 입력한다.

    // express 모듈 불러오기
    const express = require("express");
    
    // express 객체 생성
    const app = express();
    
    // 기본 포트를 app 객체에 설정
    const port = process.env.PORT || 5000;
    app.listen(port);
    
    // 미들웨어 함수를 특정 경로에 등록
    app.use("/api/data", function (req, res) {
      res.json({ greeting: "Hello World" });
    });
    
    console.log(`server running at http ${port}`);
    

    package.json 파일을 열고 scripts 항목에 "start": "nodemon index.js"를 추가한다.

    "scripts": {
      "start": "nodemon index.js"
    }
    

    리액트 초기화

    이제 클라이언트로 사용할 리액트를 생성하며, 이름은 client로 한다.

    $ create-react-app client --use-npm --template typescript
    

    프록시 설정

    설치가 완료되면 client 디렉토리로 이동해서에 아래 모듈을 설치한다.

    $ cd client
    $ npm install http-proxy-middleware
    

    설치한 뒤 /client/src/ 디렉토리로 가서 setupProxy.js 파일을 생성하고 아래 코드를 입력해준다.

    const { createProxyMiddleware } = require("http-proxy-middleware");
    
    module.exports = function (app) {
      app.use(
        createProxyMiddleware("/api/data", {
          target: "http://localhost:5000",
          changeOrigin: true,
        })
      );
    };
    

    서버(express), 클라이언트(react) 동시 시작 설정

    루트로 가서 package.jsonscripts항목을 아래처럼 수정해준다.

    "scripts": {
      "start": "nodemon index.js",
      "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
      "dev:server": "npm start",
      "dev:client": "cd client && npm start"
    }
    

    이제 아래 명령어로 서버와 클라이언트를 동시에 시작할 수 있다.

    $ npm run dev
    

    이제 작업하기 위한 전반적인 준비가 끝났다. 우선 클라이언트 영역부터 작업해보자.

    클라이언트에서 요청 작업

    검색 폼 및 리스트 추가

    /client/src/ 디렉토리에 components 폴더를 생성하고 SearchForm.tsx, SearchList.tsx 파일을 생성하고 각각 아래처럼 입력해 준다.

    SearchForm.tsx

    import React from "react";
    
    const SearchForm = () => {
      return (
        <div className="form">
          <input type="text" className="form-text" />
          <button
            type="button"
            className="form-btn"
            onClick={() => {
              fetch("api/data")
                .then((res) => {
                  return res.json();
                })
                .then((data) => {
                  console.log(data);
                });
            }}
          >
            search
          </button>
        </div>
      );
    };
    
    export default SearchForm;
    

    SearchList.tsx

    import React from "react";
    
    const SearchList = () => {
      return (
        <div className="card-list"></div>
      );
    };
    
    export default SearchList;
    

    App.tsx은 아래처럼 변경해준다.

    import React from "react";
    import SearchForm from "./components/SearchForm";
    import SearchList from "./components/SearchList";
    
    function App() {
      return (
        <div className="App">
          <SearchForm />
          <SearchList />
        </div>
      );
    }
    
    export default App;
    

    search 버튼을 클릭하면 fetch 함수로 서버(http://localhost:5000/api/data)에 요청을 하게 되고 응답값으로 콘솔창에 { greeting: "Hello World" }가 출력되는 것을 확인할 수 있다.

    fetch 함수를 App 컴포넌트로 이동

    이제 검색키워드를 서버에 보내기 위헤 SearchForm, App 컴포넌트를 아래처럼 수정해 준다.

    SearchForm.tsx

    import React, { useState } from "react";
    
    const SearchForm = (props: { getData: any }) => {
      const { getData } = props;
      const [keyword, setKeyword] = useState("");
      return (
        <div className="form">
          <input
            type="text"
            className="form-text"
            onChange={(e: any) => {
              setKeyword(e.target.value);
            }}
            onKeyPress={(e: any) => {
              if (e.charCode === 13) {
                if (keyword) {
                  getData(keyword);
                }
              }
            }}
          />
          <button
            type="button"
            className="form-btn"
            onClick={() => {
              if (keyword) {
                getData(keyword);
              }
            }}
          >
            search
          </button>
        </div>
      );
    };
    
    export default SearchForm;
    

    App.tsx

    import React from "react";
    import SearchForm from "./components/SearchForm";
    import SearchList from "./components/SearchList";
    
    function App() {
      const getData = (keyword: string) => {
        console.log("검색 키워드: " + keyword);
        fetch(`api/data?keyword=${keyword}`)
          .then((res) => {
            return res.json();
          })
          .then((data) => {
            console.log(data);
          });
      };
      return (
        <div className="App">
          <SearchForm getData={getData} />
          <SearchList />
        </div>
      );
    }
    
    export default App;
    

    SearchForm에 있던 fetch함수를 상위 App컴포넌트의 getData 함수에 넣어놨다. 이 함수를 SearchForm에 전달하였고, 검색버튼을 클릭하면 getData가 실행되며, input 태그의 검색 키워드가 쿼리스트링에 할당되어 서버에 전달되게 된다. 검색폼에서 엔터를 눌러도 요청할 수 있도록 onKeyPress 이벤트도 추가해 주자. 이제 응답값을 받기 위해 서버 작업을 해보자.

    서버에서 요청 받기

    이제 루트로 가서 index.js를 아래처럼 수정해 준다.

    // express 모듈 불러오기
    const express = require("express");
    
    // express 객체 생성
    const app = express();
    
    // 기본 포트를 app 객체에 설정
    const port = process.env.PORT || 5000;
    app.listen(port);
    
    // 미들웨어 함수를 특정 경로에 등록
    app.use("/api/data", function (req, res) {
      console.log("검색 키워드: " + req.query.keyword);
      res.json({ greeting: "Hello World" });
    });
    
    console.log(`server running at http ${port}`);
    

    요청을 하면 서버 터미널에 검색 키워드가 출력될 것이다. 위 코드를 보면 미들웨어 함수에서 요청값(req.query.keyword)을 받기 때문이다.

    Puppeteer 설치

    이제 브라우저로 검색하기 위해 루트 디렉토리에 Puppeteer를 설치해주자.

    $ npm install puppeteer
    
    • Puppeteer는 기본적으로 Chrome 혹은 Chromium 런더링 엔진을 사용하기 때문에 기본적으로 Chromium 브라우저를 내장하고 있다.
    • 따로 Chromium 브라우저를 다운받지 않으려면 $ npm install puppeteer-core 명령어를 사용하면 되며, Puppeteer는 로컬에 있는 Chrome 혹은 Chromium을 사용하게 될 것이다.

    검색해보기

    Puppeteer를 설치했으면 이제 브라우저를 실행해 검색을 해보자. index.js를 아래처럼 수정해준다.

    // express 모듈 불러오기
    const express = require("express");
    
    // express 객체 생성
    const app = express();
    
    // 기본 포트를 app 객체에 설정
    const port = process.env.PORT || 5000;
    app.listen(port);
    
    // 미들웨어 함수를 특정 경로에 등록
    app.use("/api/data", function (req, res) {
      console.log("검색 키워드: " + req.query.keyword);
      openBrowser(req.query.keyword);
    });
    
    console.log(`server running at http ${port}`);
    
    // puppeteer 모듈 불러오기
    const puppeteer = require("puppeteer");
    
    /**
     * 브라우저 오픈 함수
     * @param {string} keyword 검색 키워드
     */
    async function openBrowser(keyword) {
    
      // 브라우저 실행 및 옵션, 현재 옵션은 headless 모드 사용 여부
      const browser = await puppeteer.launch({ headless: false });
    
      // 브라우저 열기
      const page = await browser.newPage();
    
      // 포탈로 이동
      await page.goto("https://www.google.com/");
    
      // 키워드 입력
      await page.type("input[class='gLFyf gsfi']", keyword);
      
      // 키워드 검색
      await page.type("input[class='gLFyf gsfi']", String.fromCharCode(13));
    }
    

    puppeteer 모듈을 불러온 뒤 openBrowser 함수를 추가하였으며, 포탈 이동 및 응답값을 받기 위해 async 함수로 감싸주었다. 브라우저 실행 옵션에서 headless 모드를 true로 설정하면 브라우저가 화면에 노출이 되지 않고 백그라운드에서 작동된다. 지금은 브라우저 작동 순서를 보기 위해 임시로 false로 설정해 준다. 위처럼 수정해 준 뒤 클라이언트 화면으로 가서 검색해 보면 아래 순서대로 작동된다.

    1. Chromium 브라우저가 실행되고
    2. Google 사이트로 이동한 뒤
    3. Google 검색창에 검색 키워드를 넣고
    4. 엔터를 눌러 검색을 시작한다.

    검색 내용 크롤링하기

    이제 검색결과를 크롤링을 해보자.

    크롤링할 내용 형태

    {
      title: "제목",
      link: "링크",
      text: "내용",
      kategorie: "카테고리"
    }
    

    크롤링으로 가져올 정보는 위 형태로 가져올 것이며, index.js를 아래처럼 코드를 수정한다.

    // express 모듈 불러오기
    const express = require("express");
    
    // express 객체 생성
    const app = express();
    
    // 기본 포트를 app 객체에 설정
    const port = process.env.PORT || 5000;
    app.listen(port);
    
    // 미들웨어 함수를 특정 경로에 등록
    app.use("/api/data", async function (req, res) {
      console.log("검색 키워드: " + req.query.keyword);
      const resultList = await openBrowser(req.query.keyword);
      console.log(resultList);
      res.json(resultList);
    });
    
    console.log(`server running at http ${port}`);
    
    // puppeteer 모듈 불러오기
    const puppeteer = require("puppeteer");
    
    /**
     * 브라우저 오픈 함수
     * @param {string} keyword 검색 키워드
     * @return {array} 검색 결과
     */
    async function openBrowser(keyword) {
      // 브라우저 실행 및 옵션, 현재 옵션은 headless 모드 사용 여부
      const browser = await puppeteer.launch({ headless: true });
    
      // 브라우저 열기
      const page = await browser.newPage();
    
      // 포탈로 이동
      await page.goto("https://www.google.com/");
    
      // 키워드 입력
      await page.type("input[class='gLFyf gsfi']", keyword);
    
      // 키워드 검색
      await page.type("input[class='gLFyf gsfi']", String.fromCharCode(13));
    
      // 예외 처리
      try {
        // 해당 콘텐츠가 로드될 때까지 대기
        await page.waitForSelector("#rso div.g", { timeout: 10000 });
      } catch (error) {
        // 해당 태그가 없을 시 검색결과 없음 반환
        console.log("에러 발생: " + error);
        return [
          {
            title: "검색결과 없음",
            link: "",
            text: "",
            kategorie: "",
          },
        ];
      }
    
      // 호출된 브라우저 영역
      const searchData = await page.evaluate(() => {
        // 검색된 돔 요소를 배열에 담음
        const contentsList = Array.from(document.querySelectorAll("#rso div.g"));
        let contentsObjList = [];
    
        // 검색결과 크롤링
        contentsList.forEach((item) => {
          if (item.className === "g") {
            const title = item.querySelector("h3");
            const link = item.querySelector(".yuRUbf");
            const text = item.querySelector(".VwiC3b");
            const kategorie = item.querySelector(".iUh30 ");
    
            if (title && link && text && kategorie) {
              contentsObjList.push({
                title: title.textContent, // 타이틀
                link: link.children[0].href, // 링크
                text: text.textContent, // 내용
                kategorie: kategorie.textContent, // 카테고리
              });
            }
          }
        });
    
        // 호출된 브라우저 영역 콘솔창에서 확인할 수 있음
        console.log(contentsList); // 검색한 엘리먼트 리스트
        console.log(contentsObjList); // 검색한 콘텐츠 오브젝트 리스트
    
        return contentsObjList;
      });
    
      // 브라우저 닫기
      browser.close();
    
      // 검색결과 반환
      return searchData;
    }
    

    요소 대기

    headless 모드는 이제 true로 설정해준다. 브라우저가 크롤링하는 모습을 직접 확인하고 싶으면 false로 그냥 두면 된다. 이제 순서대로 코드를 살펴보자. page.waitForSelector 메서드를 추가했으며, 인자로 쿼리 셀렉터와 옵션이 들어간다. 이 메서드는 셀렉터 요소가 로드될 때 까지 대기하며, timeout로 대기 시간을 설정할 수 있다. 대기시간이 끝나도 해당 요소를 로드하지 못하면 에러를 뱉어내며, 이 경우 title에 검색결과가 없다는 값을 리턴해 준다.

    브라우저 영역

    page.evaluate 메서드는 Puppeteer로 호출한 브라우저에서 실행되는 함수로써 여기다가 크롤링 코드를 작성하면 된다. 구글 검색결과의 각 엘리먼트 셀렉터는 #rso div.g이며, 해당 요소들을 Array.from 메서드를 통해 배열로 담았다. 필요한 정보만 가져오기 위해 forEach을 돌려 오브젝트에 내용을 담고 리턴해 준 다음 브라우저는 종료가 된다.

    검색결과를 응답해주기

    이제 이 응답값을 미들웨어 함수에서 받아서 클라이언트의 응답값으로 보내줘야한다. 위 코드의 미들웨어 함수를 보면 콜백함수를 async로 감싸고 결과값을 await 키워드로 받아 응답값으로 보내주고 있다. 클라이언트의 콘솔창을 보면 크롤링한 리스트를 출력하는걸 확인할 수 있다.

    연속 검색

    지금까지 구현된건 첫 페이지만 크롤링한 것이며, 다음 페이지를 추가로 크롤링을 하려면 아래 순서가 필요하다.

    1. 검색결과 맨아래 다음버튼이 있는지 찾기
    2. 다음 버튼 있는 경우 다음 페이지로 이동
    3. 다음 페이지 내용이 불러올때까지 대기
    4. 다음 페이지 크롤링

    아래처럼 코드를 수정한다.

    // express 모듈 불러오기
    const express = require("express");
    
    // express 객체 생성
    const app = express();
    
    // 기본 포트를 app 객체에 설정
    const port = process.env.PORT || 5000;
    app.listen(port);
    
    // 미들웨어 함수를 특정 경로에 등록
    app.use("/api/data", async function (req, res) {
      console.log("검색 키워드: " + req.query.keyword);
      const resultList = await openBrowser(req.query.keyword);
      console.log(resultList);
      res.json(resultList);
    });
    
    console.log(`server running at http ${port}`);
    
    // puppeteer 모듈 불러오기
    const puppeteer = require("puppeteer");
    
    /**
     * 브라우저 오픈 함수
     * @param {string} keyword 검색 키워드
     * @return {array} 검색 결과
     */
    async function openBrowser(keyword) {
      // 모든 검색결과
      let searchAllData = [];
    
      // 브라우저 실행 및 옵션, 현재 옵션은 headless 모드 사용 여부
      const browser = await puppeteer.launch({ headless: true });
    
      // 브라우저 열기
      const page = await browser.newPage();
    
      // 포탈로 이동
      await page.goto("https://www.google.com/");
    
      // 키워드 입력
      await page.type("input[class='gLFyf gsfi']", keyword);
    
      // 키워드 검색
      await page.type("input[class='gLFyf gsfi']", String.fromCharCode(13));
    
      // 검색하고 싶은 페이지 수 만큼 반복
      for (let i = 0; i < 10; i++) {
        // 처음 검색
        if (i === 0) {
          // 예외 처리
          try {
            // 해당 콘텐츠가 로드될 때까지 대기
            await page.waitForSelector("#rso div.g", { timeout: 10000 });
    
            // 크롤링해서 검색 결과들을 담음
            searchAllData.push(...(await crawlingData()));
          } catch (error) {
            // 해당 태그가 없을 시 검색결과 없음 반환
            console.log("에러 발생: " + error);
            return [
              {
                title: "검색결과 없음",
                link: "",
                text: "",
                kategorie: "",
              },
            ];
          }
    
        // 처음 이후 검색
        } else {
          // 예외 처리
          try {
            // 다음 버튼이 로드될때까지 대기
            await page.waitForSelector("#pnnext", { timeout: 10000 });
    
            // 브라우저를 호출해 다음 버튼을 클릭
            await page.evaluate(() => {
              const nextBtn = document.querySelector("#pnnext");
              if (nextBtn) {
                nextBtn.click();
              }
            });
    
            // 크롤링해서 검색 결과들을 담음
            searchAllData.push(...(await crawlingData()));
    
            // 다음 버튼이 더이상 없는 경우 지금까지 크롤링한 모든 검색결과 반환
          } catch (error) {
            return searchAllData;
          }
        }
      }
    
      /**
       * 크롤링 함수
       * @return {array} 검색 결과
       */
      async function crawlingData() {
        // 해당 콘텐츠가 로드될 때까지 대기
        await page.waitForSelector("#rso div.g", { timeout: 10000 });
    
        // 호출된 브라우저 영역
        const searchData = await page.evaluate(() => {
          // 검색된 돔 요소를 배열에 담음
          const contentsList = Array.from(document.querySelectorAll("#rso div.g"));
          let contentsObjList = [];
    
          // 검색결과 크롤링
          contentsList.forEach((item) => {
            if (item.className === "g") {
              const title = item.querySelector("h3");
              const link = item.querySelector(".yuRUbf");
              const text = item.querySelector(".VwiC3b");
              const kategorie = item.querySelector(".iUh30 ");
    
              if (title && link && text && kategorie) {
                contentsObjList.push({
                  title: title.textContent, // 타이틀
                  link: link.children[0].href, // 링크
                  text: text.textContent, // 내용
                  kategorie: kategorie.textContent, // 카테고리
                });
              }
            }
          });
    
          // 호출된 브라우저 영역 콘솔창에서 확인할 수 있음
          console.log(contentsList); // 검색한 엘리먼트 리스트
          console.log(contentsObjList); // 검색한 콘텐츠 오브젝트 리스트
    
          return contentsObjList;
        });
    
        // 검색결과 반환
        return searchData;
      }
    
      // 브라우저 닫기
      browser.close();
    
      // 모든 검색결과 반환
      return searchAllData;
    }
    

    이제 추가된 코드들을 살펴보자.

    모든 검색결과를 담을 배열 설정

    31라인을 보면 searchAllData 배열을 추가하였으며, 이 배열안에 각 페이지마다 크롤링한 데이터가 들어간다.

    브라우저 호출하는 영역을 함수로 묶음

    각 페이지마다 크롤링을 반복해줘야되기 때문에 101번째 라인을 보면 crawlingData 함수로 따로 묶어줬다. 그리고 리턴값은 searchData로 해주며, 기존 코드의 예외 처리(try)안에 있던 page.waitForSelector 메서드만 함수 상단에 추가해 준다. 대략적인 형태는 아래와 같다.

    /**
     * 크롤링 함수
     * @return {array} 검색 결과
     */
    async function crawlingData() {
    
      // 해당 콘텐츠가 로드될 때까지 대기
      await page.waitForSelector("#rso div.g", { timeout: 10000 });
    
      // 호출된 브라우저 영역
      const searchData = await page.evaluate(() => {
    
        // ... 기존 크롤링 코드
    
        return contentsObjList;
      }
    
      // 검색결과 반환
      return searchData;
    }
    

    처음 검색 후 다음 페이지로 이동하여 검색

    코드 49번 라인을 보면 반복문이 실행되며, 검색하고 싶은 페이지만큼 반복되도록 되어있다. 위 코드는 10번만 반복하게 했으며, 반복 횟수를 늘리면 더 많은 페이지를 크롤링할 수 있다. 각 예외 처리는 2개 분기로 되어있으며, 이유는 계속 페이지마다 검색을 하는 경우 결과가 여러가지 있기 때문이다.

    검색 결과의 경우들

    1. 처음 검색에 아무 검색결과가 없는 경우
    2. 처음 검색과는 있는데 다음 버튼이 없어서 검색결과가 첫 페이지만 있는 경우
    3. 다음 페이지가 존재해 계속 검색을 진행하다 마지막 페이지에 도달해서 다음 버튼이 없는 경우

    처음 검색은 51라인부터 시작되며 검색결과가 없다면 59라인에서 에러를 캐치하며 클라이언트에 검색결과 없음을 리턴해 준다. 검색 결과가 있으면 두번째 반복이 73라인에서 실행되며, 다음 버튼이 있는 경우 83라인 함수에서 다음버튼을 클릭하고 crawlingData 함수가 실행되어 크롤링을 계속 반복하게 된다. 크롤링 결과값은 searchAllData 배열에 전개연산자를 활용하여 차곡차곡 쌓이도록 해준다. 이렇게 계속 다음 페이지로 이동하고 더이상 다음버튼이 없으면 91라인에서 에러를 캐치하여 지금까지 모은 searchAllData를 리턴하게 된다. headless 모드를 false로 설정하면 브라우저가 각 페이지를 돌면서 크롤링을 하는 모습을 직접 볼 수 있다.

    결과 확인

    서버 터미널이나 클라이언트 콘솔창을 확인하면 각 페이지마다 크롤링한 데이터를 정상적으로 응답해주는것을 확인할 수 있다. 지금까지 클라이언트, 서버 셋팅 및 연속 크롤링하는 것까지 알아봤으며 이제 이 응답값을 클라이언트에 뿌려주는 작업을 해보자.

    클라이언트에서 받은 데이터 출력하기

    출력될 컴포넌트 추가

    데이터가 들어갈 영역을 만들어주자. 우선 /client/src/components 디렉토리에 SearchItem.tsx 파일을 생성하고 아래 코드를 입력해준다.

    import React from "react";
    
    const SearchItem = (props: { item: any }) => {
      const { item } = props;
      return (
        <div className="card">
          <div className="top">
            <div className="kategorie">{item.kategorie}</div>
            <div className="title">{item.title}</div>
          </div>
          <div className="bottom">
            <div className="text">{item.text}</div>
            <a href={item.link} className="link" target="_blank" rel="noreferrer">
              더보기
            </a>
          </div>
        </div>
      );
    };
    
    export default SearchItem;
    

    컴포넌트에 데이터 전달하기

    SearchList.tsx 파일과 App.tsx 파일도 각각 아래처럼 수정해준다.

    SearchList.tsx

    import React from "react";
    import SearchItem from "./SearchItem";
    
    const SearchList = (props: { searchData: [] }) => {
      const { searchData } = props;
    
      return (
        <div className="card-list">
          {searchData.map((item: any, idx: number): JSX.Element => {
            return <SearchItem key={idx} item={item} />;
          })}
        </div>
      );
    };
    
    export default SearchList;
    

    App.tsx

    import React, { useState } from "react";
    import SearchForm from "./components/SearchForm";
    import SearchList from "./components/SearchList";
    import "./App.css";
    
    function App() {
      const [searchData, setSearchData] = useState<any>([]);
      const getData = (keyword: string) => {
        console.log("검색 키워드: " + keyword);
        fetch(`api/data?keyword=${keyword}`)
          .then((res) => {
            return res.json();
          })
          .then((data) => {
            setSearchData(data);
            console.log(data);
          });
      };
      return (
        <div className="App">
          <SearchForm getData={getData} />
          <SearchList searchData={searchData} />
        </div>
      );
    }
    
    export default App;
    

    과정을 살펴보자, 서버에서 응답값이 오면 App.tsx의 15번째 라인에서 받고 이것을 SearchList 컴포넌트에 전달해 준다. SearchList.tsx를 보면 이 응답값을 props로 전달받았으며, 이 값은 배열이기 때문에 Array.map 메서드를 사용하여 SearchItem 컴포넌트를 리턴하고 있다. 이렇게 리턴받은 SearchItem 컴포넌트는 SearchItem.tsx 파일의 코드처럼, 카테고리, 제목, 본문내용, 링크를 출력하고 있는걸 확인할 수 있다.

    스타일 꾸며주기

    CSS 적용이 안되었기 때문에 실제 화면은 이상하게 보일것이다. App.css 파일에 간단히 스타일을 추가해 주자.

    .App {
      margin: 0 auto;
      max-width: 500px;
      padding: 10px;
    }
    .App .form {
      position: relative;
      margin-bottom: 10px;
    }
    .App .form-text {
      border: 1px solid #e0e0e0;
      border-radius: 5px;
      box-sizing: border-box;
      padding: 0 65px 0 5px;
      width: 100%;
      height: 35px;
      box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
      outline: none;
    }
    .App .form-btn {
      position: absolute;
      top: 0;
      right: 0;
      border: none;
      border-radius: 0 5px 5px 0;
      width: 60px;
      height: 35px;
      background-color: #13424b;
      color: #fff;
    }
    .App .card-list {
      column-count: 2;
      column-gap: 10px;
      margin-top: 10px;
      padding: 0;
    }
    .App .card {
      display: inline-block;
      position: relative;
      margin: 0 0 10px;
      border: 1px solid #e0e0e0;
      border-radius: 5px;
      word-break: break-all;
      box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
    }
    .App .card .top {
      border-radius: 5px 5px 0 0;
      padding: 10px;
      background-color: #13424b;
      color: #fff;
    }
    .App .card .top .kategorie {
      font-size: 11px;
    }
    .App .card .top .title {
      margin-top: 5px;
      font-weight: bold;
    }
    .App .card .bottom {
      padding: 10px;
      font-size: 13px;
    }
    .App .card .bottom .link {
      display: inline-block;
      margin-top: 10px;
      padding: 3px 5px;
      border-radius: 3px;
      text-decoration: none;
      background-color: #e5e5e5;
      color: #000;
    }
    

    추가 화면 및 비활성 처리

    마무리 단계이며, 아래 단계가 남았다.

    1. 검색하는 동안 출력될 화면 추가
    2. 검색하는 동안 폼 비활성(disable) 처리
    3. 검색결과 없는 경우 추가

    SearchLoading.tsx 파일을 생성하고 아래처럼 입력해 준다. 이 컴포넌트가 검색하는 동안 보여지는 부분이다.

    import React from "react";
    
    const SearchLoading = (props: { isOnLoading: boolean }) => {
      const { isOnLoading } = props;
      return (
        <div className={isOnLoading ? "loading on" : "loading"}>loading..</div>
      );
    };
    
    export default SearchLoading;
    

    그리고 SearchForm.tsx, SearchList.tsx, App.tsx, App.css 파일들을 아래처럼 코드를 수정한다.

    SearchForm.tsx

    import React, { useState } from "react";
    
    const SearchForm = (props: { getData: any; isOnLoading: boolean }) => {
      const { getData, isOnLoading } = props;
      const [keyword, setKeyword] = useState("");
      return (
        <div className={isOnLoading ? "form disable" : "form"}>
          <input
            type="text"
            className="form-text"
            disabled={isOnLoading ? true : false}
            onChange={(e: any) => {
              setKeyword(e.target.value);
            }}
            onKeyPress={(e: any) => {
              if (e.charCode === 13) {
                if (keyword) {
                  getData(keyword);
                }
              }
            }}
          />
          <button
            type="button"
            className="form-btn"
            disabled={isOnLoading ? true : false}
            onClick={() => {
              if (keyword) {
                getData(keyword);
              }
            }}
          >
            search
          </button>
        </div>
      );
    };
    
    export default SearchForm;
    

    SearchList.tsx

    import React from "react";
    import SearchItem from "./SearchItem";
    
    const SearchList = (props: { searchData: []; isOnLoading: boolean }) => {
      const { searchData, isOnLoading } = props;
      return (
        <div className={isOnLoading ? "card-list disable" : "card-list"}>
          {searchData.map(
            (item: any, idx: number): JSX.Element => {
              if (item.kategorie && item.kategorie && item.text) {
                return <SearchItem key={idx} item={item} />;
              } else {
                return (
                  <div key={idx} className="none">
                    검색결과 없음
                  </div>
                );
              }
            }
          )}
        </div>
      );
    };
    export default SearchList;
    

    App.tsx

    import React, { useState } from "react";
    import SearchForm from "./components/SearchForm";
    import SearchLoading from "./components/SearchLoading";
    import SearchList from "./components/SearchList";
    import "./App.css";
    
    function App() {
      const [searchData, setSearchData] = useState<any>([]);
      const [isOnLoading, setIsOnLoading] = useState(false);
      const getData = (keyword: string) => {
        setIsOnLoading(true);
        console.log("검색 키워드: " + keyword);
        fetch(`api/data?keyword=${keyword}`)
          .then((res) => {
            return res.json();
          })
          .then((data) => {
            setSearchData(data);
            setIsOnLoading(false);
            console.log(data);
          });
      };
      return (
        <div className="App">
          <SearchForm getData={getData} isOnLoading={isOnLoading} />
          <SearchLoading isOnLoading={isOnLoading} />
          <SearchList searchData={searchData} isOnLoading={isOnLoading} />
        </div>
      );
    }
    
    export default App;
    

    App.css

    .App {
      margin: 0 auto;
      max-width: 500px;
      padding: 10px;
    }
    .App .form {
      position: relative;
      margin-bottom: 10px;
    }
    .App .form-text {
      border: 1px solid #e0e0e0;
      border-radius: 5px;
      box-sizing: border-box;
      padding: 0 65px 0 5px;
      width: 100%;
      height: 35px;
      box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
      outline: none;
    }
    .App .form-btn {
      position: absolute;
      top: 0;
      right: 0;
      border: none;
      border-radius: 0 5px 5px 0;
      width: 60px;
      height: 35px;
      background-color: #13424b;
      color: #fff;
    }
    .App .form.disable .form-btn {
      background-color: #b3b3b3;
    }
    .App .card-list {
      column-count: 2;
      column-gap: 10px;
      margin-top: 10px;
      padding: 0;
    }
    .App .card {
      display: inline-block;
      position: relative;
      margin: 0 0 10px;
      border: 1px solid #e0e0e0;
      border-radius: 5px;
      word-break: break-all;
      box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
    }
    .App .card .top {
      border-radius: 5px 5px 0 0;
      padding: 10px;
      background-color: #13424b;
      color: #fff;
    }
    .App .card-list.disable .top {
      background-color: #b3b3b3;
    }
    .App .card .top .kategorie {
      font-size: 11px;
    }
    .App .card .top .title {
      margin-top: 5px;
      font-weight: bold;
    }
    .App .card .bottom {
      padding: 10px;
      font-size: 13px;
    }
    .App .card .bottom .link {
      display: inline-block;
      margin-top: 10px;
      padding: 3px 5px;
      border-radius: 3px;
      text-decoration: none;
      background-color: #e5e5e5;
      color: #000;
    }
    .App .card-list.disable .link {
      color: #fff;
    }
    .App .card-list .none {
      font-size: 13px;
    }
    .App .loading {
      overflow: hidden;
      text-align: center;
      height: 0;
      line-height: 18px;
      font-size: 0;
      transition: 0.3s;
    }
    .App .loading.on {
      font-size: 13px;
      height: 20px;
    }
    

    검색 시작 및 끝을 나타내는 값

    컴포넌트에 검색중 여부 전달

    코드를 하나씩 살펴보자. App.tsx 파일의 9번째 라인을 보면 isOnLoading값이 있는데 요청을 보내면 12번째 라인에서 true로 변경, 검색중을 나타내고, 응답을 받으면 20번째 라인에서 false로 변경되며 검색이 끝났다는 것을 의미한다. 이 값을 SearchForm, SearchLoading, SearchList 컴포넌트에 전달해 주었다.

    컴포넌트에서 검색중 여부 처리

    SearchLoading.tsx 파일 6번째 라인을 보면 isOnLoadingtrue일 경우 on 클래스를 추가해 준다. 평상시에 이 엘리먼트는 보이지 않다가 on 클래스가 추가되면 보여지도록 App.css에 설정되어있다.

    다른 컴포넌트도 마찬가지로 SearchForm.tsx에서 11번째 라인을 보면 isOnLoadingtrue면 폼들은 disabled 처리가 되며, 7번째 라인에서 disable 클래스를 주고있다. SearchList.tsx 컴포넌트도 검색중이면 disable 클래스를 주고있으며, 이 클래스로 검색중에 스타일을 변경하도록 App.css에 설정되어있다.

    검색결과 없는 경우

    SearchList.tsx파일의 10번째 라인을 보면 전달받은 데이터 값이 모두 있을경우 데이터를 노출하고 없는 경우는 검색결과가 없다는 내용을 출력하고 있다.

    배포하기

    이제 로컬에서 작업한 결과물을 헤로쿠에 배포를 해보자.

    깃 초기화

    헤로쿠는 깃을 통해 업로드하기 때문에 루트 경로에서 아래 명령어로 깃을 초기화 해준다.

    $ git init
    

    빌드 설정

    정적 파일 생성

    client 디렉토리에서 아래 명령어를 입력해 배포용 정적 파일을 생성한다.

    $ npm run build
    

    정적 파일 경로 설정

    다음에 index.js 파일 최하단에 아래 코드를 추가해준다.

    // ... 기존 코드
    
    // path 모듈 불러오기
    const path = require('path');
    
    // 리액트 정적 파일 제공
    app.use(express.static(path.join(__dirname, 'client/build')));
    
    // 라우트 설정
    app.get('*', (req, res) => {
      res.sendFile(path.join(__dirname+'/client/build/index.html'));
    });
    

    헤로쿠 빌드 명령어 설정

    루트경로의 package.json 파일로 가서 heroku-postbuild를 아래처럼 추가해준다.

    "scripts": {
      "start": "nodemon index.js",
      "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
      "dev:server": "npm start",
      "dev:client": "cd client && npm start",
      "heroku-postbuild": "cd client && npm install && npm run build"
    }
    

    헤로쿠 연동하기

    헤로쿠에 대한 간략한 설명은 [이 포스팅]https://recordboy.github.io/2020/11/05/express-react-heroku-init/)을 참고하면 된다. 기존에 회원이 아니면 헤로쿠 홈페이지에서 회원가입을 하고 이곳에서 헤로쿠 CLI를 설치하면 된다.

    로그인 및 프로젝트 생성

    아래 명령어를 입력하고 아무키나 입력하면 로그인 하라는 브라우저가 뜨고 로그인을 해주자.

    $ heroku login
    

    아래 명령어로 헤로쿠에 프로젝트를 생성하며 프로젝트 이름은 다른 프로젝트와 중복되지 않게 정한다. git remote -v 명령어로 저장소가 제대로 연결되었는지 확인한다.

    $ heroku create 프로젝트이름
    $ git remote -v
    

    헤로쿠 프로젝트 주소와 로컬에서 바라보는 주소가 다를경우 $ git remote set-url heroku 프로젝트주소 명령어를 사용하여 동일하게 맞춰주면 된다.

    빌드팩 추가

    한가지 또 추가해줘야 하는 것이 있는데 Puppeteer를 헤로쿠에서 사용하려면 프로젝트에 빌드팩을 추가해줘야 한다. 아래 명령어를 입력해주자.

    $ heroku buildpacks:clear
    $ heroku buildpacks:add --index 1 https://github.com/jontewks/puppeteer-heroku-buildpack
    $ heroku buildpacks:add --index 1 heroku/nodejs
    

    그리고 index.js 파일로 가서 34번째 라인의 브라우저 실행 옵션에 args값을 아래처럼 추가해 준다.

    // 브라우저 실행 및 옵션, 현재 옵션은 headless 모드 사용 여부
    const browser = await puppeteer.launch({ 
      headless: true,
      args: [
        "--no-sandbox",
        "--disable-setuid-sandbox",
        "--window-size=1600,2000",
      ]
    });
    

    업로드

    이제 배포를 위한 모든 준비가 끝났다. 깃 명령어를 입력하여 푸쉬해주자.

    $ git add .
    $ git commit -m '커밋 메세지'
    $ git push heroku master
    

    이제 배포된 페이지를 확인해 보자. url은 https://프로젝트이름.herokuapp.com/로 가면 확인할 수 있다. 정상적으로 배포된 페이지를 화인할 수 있다.

    지금까지 만들어본건 검색하는 기능만 있는 아주 기본적인 형태이지만 잘만 활용하면 요청, 응답값으로 여러가지 형태로 발전시킬 수 있다. 예를들어 검색 포탈명을 요청해 여러 포탈의 검색결과를 한번에 찾아보거나 각 다음 페이지를 넘기는 index값을 응답을 받아 검색 로딩시간을 알아보는 등 여러가지 활용이 가능하다.

    마지막으로 주의할 점이 있는데, 헤로쿠 서버의 무료 용량은 500MB로 제한된다. Puppeteer는 자체적으로 Chromium을 내장하고 있는데 이것이 꽤 용량이 나간다.(대략 300MB 조금 넘게) 그래서 프로젝트를 크게 불려 배포를 하면 가끔 용량이 부족하다고 에러가 나오는 경우가 있다. 또 한가지는 Puppeteer를 많이 사용하다보면 구글에서 봇으로 판단하여 ‘로봇이 아닙니다’ 체크를 해야하는 경우도 있었다. 아무튼 Puppeteer는 크롤링 말고 여러가지 강력한 기능이 있기 때문에 잘 활용하면 좋은 도구가 될 수 있을 것이다.

    References

    Puppeteer
    Puppeteer unable to run on heroku

  • [Algorithm] 코딩테스트 풀이 – 해시

    [Algorithm] 코딩테스트 풀이 – 해시

    코딩테스트 플랫폼인 프로그래머스에서 해시 문제 및 풀이를 포스팅해보았다. 언어는 자바스크립트를 기준으로 작성하였다.

    해시
    해시란 데이터를 다루는 기술중 하나로서 해시함수를 이용하여 임의의 길이를 갖는 데이터를 고정된 길이의 데이터로 매핑하는 것을 말한다.

    완주하지 못한 선수

    문제 설명

    수많은 마라톤 선수들이 마라톤에 참여하였습니다. 단 한 명의 선수를 제외하고는 모든 선수가 마라톤을 완주하였습니다.
    마라톤에 참여한 선수들의 이름이 담긴 배열 participant와 완주한 선수들의 이름이 담긴 배열 completion이 주어질 때, 완주하지 못한 선수의 이름을 return 하도록 solution 함수를 작성해주세요.

    제한 사항

    • 마라톤 경기에 참여한 선수의 수는 1명 이상 100,000명 이하입니다.
    • completion의 길이는 participant의 길이보다 1 작습니다.
    • 참가자의 이름은 1개 이상 20개 이하의 알파벳 소문자로 이루어져 있습니다.
    • 참가자 중에는 동명이인이 있을 수 있습니다.

    입출력 예

    participantcompletionreturn
    [“leo”, “kiki”, “eden”][“eden”, “kiki”]“leo”
    [“marina”, “josipa”, “nikola”, “vinko”, “filipa”][“josipa”, “filipa”, “marina”, “nikola”]“vinko”
    [“mislav”, “stanko”, “mislav”, “ana”][“stanko”, “ana”, “mislav”]“mislav”

    입출력 예 설명

    예제 #1 “leo”는 참여자 명단에는 있지만, 완주자 명단에는 없기 때문에 완주하지 못했습니다.

    예제 #2 “vinko”는 참여자 명단에는 있지만, 완주자 명단에는 없기 때문에 완주하지 못했습니다.

    예제 #3 “mislav”는 참여자 명단에는 두 명이 있지만, 완주자 명단에는 한 명밖에 없기 때문에 한명은 완주하지 못했습니다.

    풀이

    코드

    const participant = ["mislav", "stanko", "mislav", "ana"];
    const completion = ["stanko", "ana", "mislav"];
    
    function solution(participant, completion) {
      let answer = "";
    
      // 각 배열을 이름순으로 정렬
      participant.sort();
      completion.sort();
    
      // 루프를 돌며 이름이 일치하지 않을 경우 결과를 담고 리턴
      for (let i = 0; i < participant.length; i++) {
        if (participant[i] !== completion[i]) {
          answer = participant[i];
          break;
        }
      }
    
      return answer;
    }
    
    console.log(solution(participant, completion));
    

    설명

    반복문을 돌며 이름을 비교할 것이며 그 전에 동명이인이 있을 수 있기 때문에, 각 배열에 sort 함수를 사용하여 이름순으로 정렬한다. 반복문이 돌면서 이름을 매칭해보고 이름이 다를경우 해당 참여자를 응답값에 담고, 완주하지 못한 선수는 단 한명이기 때문에 값을 리턴하고 로직을 빠져나온다.

    위장

    문제 설명

    스파이들은 매일 다른 옷을 조합하여 입어 자신을 위장합니다.
    예를 들어 스파이가 가진 옷이 아래와 같고 오늘 스파이가 동그란 안경, 긴 코트, 파란색 티셔츠를 입었다면 다음날은 청바지를 추가로 입거나 동그란 안경 대신 검정 선글라스를 착용하거나 해야 합니다.

    종류이름
    얼굴동그란 안경, 검정 선글라스
    상의파란색 티셔츠
    하의청바지
    겉옷긴 코트

    스파이가 가진 의상들이 담긴 2차원 배열 clothes가 주어질 때 서로 다른 옷의 조합의 수를 return 하도록 solution 함수를 작성해주세요.

    제한 사항

    • clothes의 각 행은 [의상의 이름, 의상의 종류]로 이루어져 있습니다.
    • 스파이가 가진 의상의 수는 1개 이상 30개 이하입니다.
    • 같은 이름을 가진 의상은 존재하지 않습니다.
    • clothes의 모든 원소는 문자열로 이루어져 있습니다.
    • 모든 문자열의 길이는 1 이상 20 이하인 자연수이고 알파벳 소문자 또는 ‘_’ 로만 이루어져 있습니다.
    • 스파이는 하루에 최소 한 개의 의상은 입습니다.

    입출력 예

    clothesreturn
    [[“yellowhat”, “headgear”], [“bluesunglasses”, “eyewear”], [“green_turban”, “headgear”]]5
    [[“crowmask”, “face”], [“bluesunglasses”, “face”], [“smoky_makeup”, “face”]]3

    입출력 예 설명

    예제 #1 headgear에 해당하는 의상이 yellow_hat, green_turban이고 eyewear에 해당하는 의상이 blue_sunglasses이므로 아래와 같이 5개의 조합이 가능합니다.

    예제 #2 face에 해당하는 의상이 crow_mask, blue_sunglasses, smoky_makeup이므로 아래와 같이 3개의 조합이 가능합니다.

    1. crow_mask
    2. blue_sunglasses
    3. smoky_makeup

    풀이

    코드

    const clothes = [
      ["yellowhat", "headgear"],
      ["bluesunglasses", "eyewear"],
      ["green_turban", "headgear"],
    ];
    
    function solution(clothes) {
      let answer = 1; // 리턴값, 마지막에 곱셉을 하기 때문에 0으로 하면 안됨
      let obj = {}; // 의상 종류마다 갯수를 담을 객체
    
      for (let i = 0; i < clothes.length; i++) {
        // 중복되는 키값이 존재한다면 1 더함
        if (obj[clothes[i][1]] >= 1) {
          obj[clothes[i][1]] += 1;
    
          // 처음 등장하는 의상이면 1로 초기화
        } else {
          obj[clothes[i][1]] = 1;
        }
      }
    
      // 경우의 수 곱셈, (2 + 1) X (1 + 1)
      for (let key in obj) {
        answer *= obj[key] + 1;
      }
    
      // 아무것도 입지 않는 경우는 빼줌
      return answer - 1;
    }
    
    console.log(solution(clothes));
    

    설명

    리턴값은 다른 옷의 조합수로서 이럴땐 경우의 수 공식을 사용하면 된다. 예제 #1로 예를 들면 모자의 경우의수는 2개며 안경의 경우의수는 1개다.

    • headgear: yellowhat, green_turban
    • eyewear: bluesunglasses

    경우의 수 공식에 따라, 2 X 1 = 2가 되야하지만, 모자나 안경을 안쓰는 경우도 있으니 각 경우의 수에 1씩 더해주서 (2 + 1) X (1 + 1) = 6이 된다. 여기서 아무것도 입지 않는 경우는 빼야 되므로 최종 경우의 수에 1을 빼준다. 최종적으로 (2 + 1) X (1 + 1) - 1 = 5 수식이 된다. 이 방법으로 코드를 위처럼 작성하면 된다.

  • [JavaScript] 문자열이 공백인지 체크

    [JavaScript] 문자열이 공백인지 체크

    문자열이 공백인지 체크하는 함수

    검색창 구현하다 필요해서 구글링해서 줍줍

    /**
     * 정규표현식을 활용하여 문자열이 공백일경우 'true' 반환
     * @param {string} str 
     * @returns {boolean} 공백일경우 'true' 아닐경우 'false'
     */
    function checkSpaceStr(str) {
    
        let bln = false;
        let blankPattern = /^\s+|\s+$/g;
        if (str.replace(blankPattern, '') === '') {
            bln = true;
        }
        return bln;
    }
    

    References

    회원가입에서 공백, 특수문자 검사하기

  • [TypeScript] 타입스크립트 타입 선언

    [TypeScript] 타입스크립트 타입 선언

    타입 기본

    타입 지정

    타입스크립트는 일반 변수, 매개 변수, 객체 속성 등에: TYPE과 같은 형태로 타입을 지정할 수 있다.

    let a: string = 'text'; // 문자열
    let b: number = 0; // 숫자형
    let c: boolean = true; // 논리형
    let d: any = true; // 어떤 타입이 올지 모를 때
    let e: string | number = '0'; // 문자열이나 숫자가 올 때
    

    타입 에러

    만약 아래와 같이 타입을 선언하고 다른 타입의 값을 대입하거나 값이 선언된 후에 다른 타입의 값이 대입되면 에러를 발생한다.

    // 문자열로 선언하고 숫자를 대입하면 에러 발생
    let a: string = 0; // error
    
    // 문자열 값이 들어가고 이후에 숫자가 대입하면 에러 발생
    let b: string = 'text';
    b = 1; // error Type '1' is not assignable to type 'string'.  TS2322
    

    출력창에서 TS2322라는 에러 코드를 볼 수 있으며, 이를 검색하면 쉽게 에러 코드에 대한 정보를 얻을 수 있다.

    타입 선언

    타입스크립트는 ES5, ES6의 상위 확장인 언어이므로 자바스크립트의 타입을 그대로 사용하며, 이외에도 타입스크립트의 고유의 타입이 추가로 제공된다.

    TypeJSTSDescription
    booleantrue와 false
    null값이 없다는 것을 명시
    undefined값을 할당하지 않은 변수의 초기값
    number숫자(정수와 실수, Infinity, NaN)
    string문자열
    symbol고유하고 수정 불가능한 데이터 타입이며 주로 객체 프로퍼티들의 식별자로 사용(ES6에서 추가)
    object객체형(참조형)
    array배열
    tuple고정된 요소수 만큼의 타입을 미리 선언후 배열을 표현
    enum열거형. 숫자값 집합에 이름을 지정한 것
    any타입 추론(type inference)할 수 없거나 타입 체크가 필요없는 변수에 사용, var 키워드로 선언한 변수와 같이 어떤 타입의 값이라도 할당 가능
    void일반적으로 함수에서 반환값이 없을 경우 사용
    never결코 발생하지 않는 값

    논리형(Boolean)

    let bool01: boolean = true;
    let bool02: boolean = false;
    

    숫자형(Number)

    let num01: number = 5;
    let num02: number = 3.14;
    let num03: number = NaN;
    

    문자열(String)

    ES6의 템플릿 문자열도 지원한다.

    let str01: string = 'text';
    let str02: string = `my name is ${val}`;
    

    배열(Array)

    배열은 아래와 같이 두가지 방법으로 타입을 선언할 수 있다.

    // 문자열만 가지는 배열
    let arr01: string[] = ['a', 'b', 'c'];
    let arr02: Array<string> = ['a', 'b', 'c'];
    
    // 숫자형만 가지는 배열
    let arr03: number[] = [1, 2, 3];
    let arr04: Array<number> = [1, 2, 3];
    
    // Union 타입(다중 타입)의 문자열과 숫자를 동시에 가지는 배열
    let arr05: (string | number)[] = [1, 'a', 2, 'b', 'c', 3];
    let arr06: Array<string | number> = [1, 'a', 2, 'b', 'c', 3];
    
    // 배열이 가지는 값의 타입을 추측할 수 없을 때 any를 사용할 수 있다.
    let arr07: (any)[] = [1, 'a', 2, 'b', 'c', 3];
    let arr08: Array<any> = [1, 'a', 2, 'b', 'c', 3];
    

    함수(Function)

    함수는 파라미터에 각각 타입을 선언해 주며, 파라미터 우측에는 해당 함수의 리턴값 타입도 선언해 주면 된다.

    function sum(a: number, b: number): number {
      return a + b;
    }
    console.log(sum(2, 3)); // 5
    

    리턴값을 숫자형으로 선언하였는데 다른 값이 리턴된다면 역시 에러가 난다.

    function sum(a: number, b: number): number {
      return null; // error
    }
    

    객체(Object)

    기본적으로 typeof 연산자가 object로 반환하는 모든 타입을 나타낸다. 여러 타입의 상위 타입이기 때문에 그다지 유용하지 않다.

    let obj: object = {};
    let arr: object = [];
    let func: object = function() {};
    let date: object = new Date();
    

    보다 정확하게 타입 지정을 하기 위해 아래와 같이 객체 속성들에 대한 타입을 개별적으로 지정할 수 있다.

    let user: { name: string, age: number } = {
      name: 'a',
      age: 20
    };
    console.log(user); // {name: "a", age: 20}
    

    튜플(Tuple)

    배열과 유사하다. 차이점은 정해진 타입의 요소 개수 만큼의 타입을 미리 선언후 배열을 표현한다.

    let tuple: [string, number];
    tuple = ['a', 0];
    console.log(tuple); // ["a", 0]
    

    정해진 요소의 순서 및 개수가 다르면 오류를 출력한다.

    let tuple: [string, number];
    tuple = [0, 'a']; // error
    tuple = ['a', 0, 1]; // error
    

    아래와 같이 데이터를 개별 변수로 지정하지 않고, 단일 튜플 타입으로 지정해 사용할 수 있다.

    // 기존 변수
    let name: string = 'a';
    let age: number = 20;
    
    // 튜플
    let user: [string, number] = ['a', 20];
    console.log(user); // ["a", 20]
    

    값으로 타입을 대신할 수도 있다. 처음 선언할 때의 값과 다른 값이 할당 되면 에러가 출력된다.

    let user: ['a', number];
    user = ['a', 20]; // ["a", 20]
    user = ['a', 30]; // ["a", 30]
    user = ['b', 30]; // error
    

    튜플은 정해진 타입과 고정된 길이의 배열이지만, 값을 할당할 때만 해당된다. push나 splice같은 메서드를 통해 값을 넣는건 막을 수 없다.

    let user: [string, number];
    user = ['a', 20]
    console.log(user); // ["a", 20]
    user.push(30);
    console.log(user); // ["a", 20, 30]
    

    열거형(Enum)

    숫자 혹은 문자열 값 집합에 이름을 부여할 수 있는 타입으로, 값의 종류가 일정한 범위로 정해져 있는 경우 유용하다. 기본적으로 0부터 시작하며, 값은 1씩 증가한다.

    enum obj {
      a,
      b,
      c,
      d,
      e
    }
    console.log(obj);
    // 0: "a"
    // 1: "b"
    // 2: "c"
    // 3: "d"
    // 4: "e"
    // a: 0
    // b: 1
    // c: 2
    // d: 3
    // e: 4
    

    수동으로 값을 변경할 수 있으며, 변경한 부분부터 다시 1씩 증가한다.

    enum obj {
      a,
      b = 10,
      c,
      d,
      e
    }
    console.log(obj.b); // 10
    console.log(obj.c); // 11
    

    모든 타입(Any)

    Any는 모든 타입을 의미하며, 기존의 자바스크립트 변수와 마찬가지로 어떠한 타입의 값도 할당할 수 있다. 불가피하게 타입을 선언할 수 없는 경우, 유용할 수 있다.

    let any:any = 'String';
    any = 0;
    console.log(any); // 0
    any = true;
    console.log(any); // true
    

    빈 타입(Void)

    빈 타입인 Void는 리턴값이 없는 함수에서 사용된다. 리턴값의 타입을 명시하는 곳에 작성하며, 리턴값이 없는 함수는 undefined를 반환한다.

    function hello(): void {
      console.log("hello");
    }
    console.log(hello()); // undefined
    

    never

    Never 타입은 절대 발생할 수 없는 타입을 나타낸다.

    function errorMsg() {
      throw new Error("오류 발생!");
    }
    console.log(errorMsg()); // Uncaught Error: 오류 발생!
    

    errorMsgd 함수는 오류를 발생시키기 때문에 null, undefined를 포함한 어떠한 값도 반환하지 않는다. 이럴 경우 never 타입을 사용하면 된다.

    References

    한눈에 보는 타입스크립트(updated)
    정적 타이핑
    타입스크립트 기초 연습
    TypeScript-Handbook 한글 문서
    3.1 기본 타입
    타입스크립트 기초 연습

  • [React] ESlint, Prettier 초기 설정 샘플

    [React] ESlint, Prettier 초기 설정 샘플

    1. CRA 설치 및 eslint & pritere 설치$ create-react-app ./
      $ npm install -D prettier eslint-config-prettier eslint-plugin-prettier

    2. root 경로에 .eslintrc 파일 생성 및 아래 내용 입력(window && mac 같이 사용하는 경우)// .eslintrc
      {
      "extends": ["react-app", "plugin:prettier/recommended"],
      "rules": {
      "no-var": "warn", // var 금지
      "no-multiple-empty-lines": "warn", // 여러 줄 공백 금지
      "no-console": ["warn", { "allow": ["warn", "error"] }], // console.log() 금지
      "eqeqeq": "warn", // 일치 연산자 사용 필수
      "dot-notation": "warn", // 가능하다면 dot notation 사용
      "no-unused-vars": "warn", // 사용하지 않는 변수 금지
      "react/destructuring-assignment": "warn", // state, prop 등에 구조분해 할당 적용
      "react/jsx-pascal-case": "warn", // 컴포넌트 이름은 PascalCase로
      "react/no-direct-mutation-state": "warn", // state 직접 수정 금지
      "react/jsx-no-useless-fragment": "warn", // 불필요한 fragment 금지
      "react/no-unused-state": "warn", // 사용되지 않는 state
      "react/jsx-key": "warn", // 반복문으로 생성하는 요소에 key 강제
      "react/self-closing-comp": "warn", // 셀프 클로징 태그 가능하면 적용
      "react/jsx-curly-brace-presence": "warn", // jsx 내 불필요한 중괄호 금지
      "prettier/prettier": [
      "error",
      {
      "endOfLine": "auto"
      }
      ]
      }
      }

    3. root 경로에 .prettierrc 파일 생성 및 아래 내용 입력// .prettierrc
      {
      "tabWidth": 2,
      "endOfLine": "lf",
      "arrowParens": "avoid",
      "singleQuote": true
      }

    4. .vscode 폴더 생성 뒤 VSCode에 적용할 설정 JSON 추가// .vscode/settings.json
      {
      "editor.defaultFormatter": "esbenp.prettier-vscode",
      "editor.tabSize": 2,
      "editor.formatOnSave": true,
      "editor.codeActionsOnSave": {
      "source.fixAll.eslint": true
      },
      "javascript.format.enable": false,
      "eslint.alwaysShowStatus": true,
      "files.autoSave": "onFocusChange"
      }

    5. 라우터 && 스타일 컴포넌트 설치(선택사항)$ npm i react-router-dom
      $ npm i styled-components


  • [JavaScript] 랜덤 배열(Shuffle)

    [JavaScript] 랜덤 배열(Shuffle)

    아래는 배열을 무작위로 섞는 알고리즘이며, arr배열을 선언한 뒤 각 요소를 랜덤으로 추출하여 newArr라는 새로운 배열에 할당하도록 작성되었다.

    let arr = [1, 2, 3, 4, 5],
      newArr = [],
      len = arr.length,
      ranIdx = 0,
      ran = 0,
      target = 0;
    
    console.log(arr);
    // [ 1, 2, 3, 4, 5 ]
    
    for (let i = 0; i < len; i++) {
      ranIdx = Math.floor(Math.random() * arr.length);
      ran = arr[ranIdx];
      if (arr.indexOf(ran) !== -1) {
        target = arr.indexOf(ran);
        arr.splice(target, 1);
        newArr.push(ran);
      }
    }
    console.log(newArr);
    // 무작위로 배열 출력
    

    구글링을 하다 더욱 간결한 코드를 발견하였다. 코드는 아래와 같다.

    function shuffle(a) {
      let j, x, i;
      for (let i = a.length; i; i -= 1) {
        j = Math.floor(Math.random() * i);
        x = a[i - 1];
        a[i - 1] = a[j];
        a[j] = x;
      }
      console.log(a);
      // 무작위로 배열 출력
    }
    shuffle([1, 2, 3, 4, 5]);
    

    shuffle()에 배열을 인자로 넘겨 호출한다. 함수안 반복문의 인덱스 값은 내림차순으로 할당된다. Math.random()로 배열 요소의 난수를 구한 뒤 x에 배열의 마지막 요소를 할당하고, 마지막 요소 자리에 난수를 인덱스로 가지는 요소를 대입한다. 난수를 인덱스 값으로 가지는 요소 자리에는 아까 할당해둔 x값을 대입한다. 이 로직을 반복하여 랜덤 배열을 얻게 된다.