[작성자:] 수수께끼

  • [JavaScript] try…catch를 이용한 에러 핸들링

    [JavaScript] try…catch를 이용한 에러 핸들링

    자바스크립트에서 에러가 발생하면 코드는 멈추게 되고, 콘솔에 에러가 출력된다. 하지만 try...catch 문법을 사용하면 스크립트가 죽는 것을 방지하고, 에러 상황을 잡아 예외처리를 할 수 있게 한다. 기본적인 형태는 두 블록으로 구성되며 예시 코드는 아래와 같다.

    기본 형태

    try {
    
      // 이 구간에서 에러가 발생하면 catch로 이동
    
    } catch (err) {
    
      // 에러 핸들링
    
    }
    
    • 먼저 try 블록의 코드가 실행된다.
    • try 블록 안에 에러가 없다면 catch 블록은 건너 뛴다.
    • try 블록 안에서 에러 코드를 만나면 try 블록의 실행이 중단되고 catch 블록의 코드가 실행된다.
    • err 객체에는 에러에 대한 정보가 있다.

    실제 작동 코드 살펴보자.

    try {
      console.log("아직 에러 없음");
    
      a; // 에러 시작
    
      console.log("이곳은 실행 안됨");
    
    } catch (err) {
    
      console.log(err); // a is not defined
    
      console.log("에러가 나도 이곳의 코드는 실행됨");
    
    }
    

    try 블록에서 에러가 나면 아래의 console.log()는 실행이 안되며 바로 catch 블록으로 넘어간다. err 객체가 콘솔창에 어떤 에러인지 표시해 주며, 에러가 발생해도 catch 블록의 코드는 계속 실행되는 것을 확인할 수 있다. 이 부분에 에러 예외 처리를 작성해 주면 된다.

    try...catch는 런타임 에러에만 작동한다

    try...catch는 실행이 가능한 코드에만 동작하며, 중괄호가 들어가는 등 자바스크립트 엔진이 해석할 수 없는 문법적 오류(SyntaxError)같은 경우는 작동하지 않는다.

    try {
        
        {}{ // SyntaxError
    
      console.log("자바스크립트 엔진은 이 코드를 이해할 수 없어 실행 자체가 안됨");
    
    } catch (err) {
    
      console.log("여기도 실행 안됨");
    
    }
    

    try...catch는 동기적으로 동작한다

    try...catchsetTimeout와 같이 비동기적으로 실행되는 코드의 에러는 잡아낼 수 없다.

    try {
        
      setTimeout(() => {
    
        a; // 에러가 발생하지만 catch가 잡아낼 수 없음
    
      }, 1000);
    
    } catch (err) {
    
      console.log("try 블록의 에러를 잡아낼 수 없음");
    
    }
    

    1초뒤 setTimeout의 익명함수가 실행되고 에러가 발생하지만 이미 자바스크립트 엔진은 try...catch 블록을 떠났기 때문에 오류를 잡아낼 수 없다. 비동기로 실행되는 코드의 에러를 잡으려면 반드시 해당 함수 안에서 try...catch 구문을 사용해야 한다.

    setTimeout(() => {
    
      try {
    
        a; // 에러가 발생하지만 catch가 잡아낼 수 있음
    
      } catch (err) {
    
        console.log("try 블록의 에러를 잡아냄");
    
      }
      
    }, 1000);
    

    에러 객체

    에러가 발생하면 자바스크립트는 에러 내용이 담긴 객체를 생성하고 catch 블록의 인수로 전달한다.

    try {
    
      a; // 에러 시작
    
    } catch (err) {
    
      console.log(err); // ReferenceError: a is not defined
      console.log(err.name); // ReferenceError
      console.log(err.message); // a is not defined
      
    }
    

    name 프로퍼티는 에러의 이름을 나타내고 message는 에러에 대한 상세 내용을 가지고 있다.

    직접 에러를 생성해 던지기

    throw 연산자

    throw 연산자는 예외를 던질 수 있으며, catch 블록에 전달된다.

    throw 연산자는 함수의 실행을 중단한다는 표현과 같다.

    try {
    
      throw "예외 처리를 던짐";
    
      console.log("여긴 실행 안됨");
    
    } catch (err) {
    
      console.log(err); // 예외 처리를 던짐
      
    }
    

    위 코드에서 throw는 예외를 던지고 있으며, throw 아래의 로직은 실행이 안된다. throw 연산자자와 에러 객체 생성자를 이용하여 예외 처리를 해보자.

    에러 객체 생성자

    const error = new Error("에러 발생");
    const syntaxError = new SyntaxError("문법 에러 발생");
    
    console.log(error); // Error: 에러 발생
    console.log(syntaxError); // SyntaxError: 문법 에러 발생
    

    자바스크립트는 Error, SyntaxError, ReferenceError, TypeError 등 표준 애러 객체 생성자를 지원하며, 이 생성자들을 이용해 에러 객체를 만들 수 있다.

    생성한 에러 던지기

    const person = {
      name: "foo",
      age: 20,
    };
    
    try {
    
      if (!person.gender) {
        throw new SyntaxError("성별이 없음");
      }
    
      console.log("이곳은 실행이 안됨"); // person.gender 값이 없기 때문에 실행이 안됨
    
    } catch (err) {
    
      console.log(err); // Error: 성별이 없음
      
    }
    

    위 코드의 try 블록에서는 성별 값이 있는지 체크하고 있는데, 체크 대상인 person 객체에는 이름과 나이만 있고 성별이 없다. 때문에 성별이 없을 때 직접 생성한 에러를 던지고 있고, catch 블록에서 에러를 받아 출력하고 있다.

    참고로 위 에러는 직접 생성한 에러이기 때문에 실제 SyntaxError 에러는 아니다.

    에러 다시 던지기

    try...catch는 애초에 try 블록에서 발생한 모든 에러를 잡는 목적으로 만들어졌다. 에러의 종류와 상관 없이 모든 에러를 잡는것은 디버깅에 어려움을 주기 때문에 예상치 못한 에러를 다시 던져서 에러의 종류에 따라 대응을 해줘야 한다.

    const person = {
      name: "foo",
      age: 20,
    };
    
    const getError = () => {
      try {
        if (!person.gender) {
          throw new SyntaxError("성별이 없음");
          // throw new ReferenceError("성별이 없음");
        }
      } catch (err) {
        if (err instanceof SyntaxError) {
          console.log("이 에러는 " + err); // SyntaxError일 경우
        } else {
          throw err; // SyntaxError가 아닐 경우 밖으로 다시 던짐
        }
      }
    };
    
    try {
      getError();
    } catch (err) {
      if (err instanceof ReferenceError) {
        console.log("이 에러는 " + err); // ReferenceError일 경우
      }
    }
    

    getError()가 실행되면서 SyntaxError를 잡아내고 있으며, 만약 SyntaxError 에러가 아닐 경우는 다시 에러를 함수 밖으로 던지며, 함수 외부에서 에러를 다시 잡고 있다. 함수 밖에서는 ReferenceError일 경우를 잡아내고 있다. SyntaxError일 경우는 함수 내부에서 에러를 잡고 ReferenceError일 경우는 함수 외부에서 잡는다고 보면 된다.

    finally

    finally절은 에러의 유무와 상관없이 마지막으로 사용되는 블록이며, 마지막 제어가 필요할 때 사용하면 된다.

    try {
        
      throw new Error("에러 발생");
    
    } catch (err) {
    
      console.log(err); // 에러 발생
    
    } finally {
    
      console.log("항상 실행"); // 항상 실행
    
    }
    

    References

    ‘try..catch’와 에러 핸들링
    에러 처리를 어떻게 하면 좋을까? – 1
    예외 ( throw,[try/catch/finally])

  • [JavaScript] try…catch를 이용한 에러 핸들링

    [JavaScript] try…catch를 이용한 에러 핸들링

    자바스크립트에서 에러가 발생하면 코드는 멈추게 되고, 콘솔에 에러가 출력된다. 하지만 try...catch 문법을 사용하면 스크립트가 죽는 것을 방지하고, 에러 상황을 잡아 예외처리를 할 수 있게 한다. 기본적인 형태는 두 블록으로 구성되며 예시 코드는 아래와 같다.

    기본 형태

    try {
    
      // 이 구간에서 에러가 발생하면 catch로 이동
    
    } catch (err) {
    
      // 에러 핸들링
    
    }
    
    • 먼저 try 블록의 코드가 실행된다.
    • try 블록 안에 에러가 없다면 catch 블록은 건너 뛴다.
    • try 블록 안에서 에러 코드를 만나면 try 블록의 실행이 중단되고 catch 블록의 코드가 실행된다.
    • err 객체에는 에러에 대한 정보가 있다.

    실제 작동 코드 살펴보자.

    try {
      console.log("아직 에러 없음");
    
      a; // 에러 시작
    
      console.log("이곳은 실행 안됨");
    
    } catch (err) {
    
      console.log(err); // a is not defined
    
      console.log("에러가 나도 이곳의 코드는 실행됨");
    
    }
    

    try 블록에서 에러가 나면 아래의 console.log()는 실행이 안되며 바로 catch 블록으로 넘어간다. err 객체가 콘솔창에 어떤 에러인지 표시해 주며, 에러가 발생해도 catch 블록의 코드는 계속 실행되는 것을 확인할 수 있다. 이 부분에 에러 예외 처리를 작성해 주면 된다.

    try...catch는 런타임 에러에만 작동한다

    try...catch는 실행이 가능한 코드에만 동작하며, 중괄호가 들어가는 등 자바스크립트 엔진이 해석할 수 없는 문법적 오류(SyntaxError)같은 경우는 작동하지 않는다.

    try {
        
        {}{ // SyntaxError
    
      console.log("자바스크립트 엔진은 이 코드를 이해할 수 없어 실행 자체가 안됨");
    
    } catch (err) {
    
      console.log("여기도 실행 안됨");
    
    }
    

    try...catch는 동기적으로 동작한다

    try...catchsetTimeout와 같이 비동기적으로 실행되는 코드의 에러는 잡아낼 수 없다.

    try {
        
      setTimeout(() => {
    
        a; // 에러가 발생하지만 catch가 잡아낼 수 없음
    
      }, 1000);
    
    } catch (err) {
    
      console.log("try 블록의 에러를 잡아낼 수 없음");
    
    }
    

    1초뒤 setTimeout의 익명함수가 실행되고 에러가 발생하지만 이미 자바스크립트 엔진은 try...catch 블록을 떠났기 때문에 오류를 잡아낼 수 없다. 비동기로 실행되는 코드의 에러를 잡으려면 반드시 해당 함수 안에서 try...catch 구문을 사용해야 한다.

    setTimeout(() => {
    
      try {
    
        a; // 에러가 발생하지만 catch가 잡아낼 수 있음
    
      } catch (err) {
    
        console.log("try 블록의 에러를 잡아냄");
    
      }
      
    }, 1000);
    

    에러 객체

    에러가 발생하면 자바스크립트는 에러 내용이 담긴 객체를 생성하고 catch 블록의 인수로 전달한다.

    try {
    
      a; // 에러 시작
    
    } catch (err) {
    
      console.log(err); // ReferenceError: a is not defined
      console.log(err.name); // ReferenceError
      console.log(err.message); // a is not defined
      
    }
    

    name 프로퍼티는 에러의 이름을 나타내고 message는 에러에 대한 상세 내용을 가지고 있다.

    직접 에러를 생성해 던지기

    throw 연산자

    throw 연산자는 예외를 던질 수 있으며, catch 블록에 전달된다.

    throw 연산자는 함수의 실행을 중단한다는 표현과 같다.

    try {
    
      throw "예외 처리를 던짐";
    
      console.log("여긴 실행 안됨");
    
    } catch (err) {
    
      console.log(err); // 예외 처리를 던짐
      
    }
    

    위 코드에서 throw는 예외를 던지고 있으며, throw 아래의 로직은 실행이 안된다. throw 연산자자와 에러 객체 생성자를 이용하여 예외 처리를 해보자.

    에러 객체 생성자

    const error = new Error("에러 발생");
    const syntaxError = new SyntaxError("문법 에러 발생");
    
    console.log(error); // Error: 에러 발생
    console.log(syntaxError); // SyntaxError: 문법 에러 발생
    

    자바스크립트는 Error, SyntaxError, ReferenceError, TypeError 등 표준 애러 객체 생성자를 지원하며, 이 생성자들을 이용해 에러 객체를 만들 수 있다.

    생성한 에러 던지기

    const person = {
      name: "foo",
      age: 20,
    };
    
    try {
    
      if (!person.gender) {
        throw new SyntaxError("성별이 없음");
      }
    
      console.log("이곳은 실행이 안됨"); // person.gender 값이 없기 때문에 실행이 안됨
    
    } catch (err) {
    
      console.log(err); // Error: 성별이 없음
      
    }
    

    위 코드의 try 블록에서는 성별 값이 있는지 체크하고 있는데, 체크 대상인 person 객체에는 이름과 나이만 있고 성별이 없다. 때문에 성별이 없을 때 직접 생성한 에러를 던지고 있고, catch 블록에서 에러를 받아 출력하고 있다.

    참고로 위 에러는 직접 생성한 에러이기 때문에 실제 SyntaxError 에러는 아니다.

    에러 다시 던지기

    try...catch는 애초에 try 블록에서 발생한 모든 에러를 잡는 목적으로 만들어졌다. 에러의 종류와 상관 없이 모든 에러를 잡는것은 디버깅에 어려움을 주기 때문에 예상치 못한 에러를 다시 던져서 에러의 종류에 따라 대응을 해줘야 한다.

    const person = {
      name: "foo",
      age: 20,
    };
    
    const getError = () => {
      try {
        if (!person.gender) {
          throw new SyntaxError("성별이 없음");
          // throw new ReferenceError("성별이 없음");
        }
      } catch (err) {
        if (err instanceof SyntaxError) {
          console.log("이 에러는 " + err); // SyntaxError일 경우
        } else {
          throw err; // SyntaxError가 아닐 경우 밖으로 다시 던짐
        }
      }
    };
    
    try {
      getError();
    } catch (err) {
      if (err instanceof ReferenceError) {
        console.log("이 에러는 " + err); // ReferenceError일 경우
      }
    }
    

    getError()가 실행되면서 SyntaxError를 잡아내고 있으며, 만약 SyntaxError 에러가 아닐 경우는 다시 에러를 함수 밖으로 던지며, 함수 외부에서 에러를 다시 잡고 있다. 함수 밖에서는 ReferenceError일 경우를 잡아내고 있다. SyntaxError일 경우는 함수 내부에서 에러를 잡고 ReferenceError일 경우는 함수 외부에서 잡는다고 보면 된다.

    finally

    finally절은 에러의 유무와 상관없이 마지막으로 사용되는 블록이며, 마지막 제어가 필요할 때 사용하면 된다.

    try {
        
      throw new Error("에러 발생");
    
    } catch (err) {
    
      console.log(err); // 에러 발생
    
    } finally {
    
      console.log("항상 실행"); // 항상 실행
    
    }
    

    References

    ‘try..catch’와 에러 핸들링
    에러 처리를 어떻게 하면 좋을까? – 1
    예외 ( throw,[try/catch/finally])

  • [JavaScript] 함수형 프로그래밍의 순수 함수

    [JavaScript] 함수형 프로그래밍의 순수 함수

    함수형 프로그래밍

    부수 효과를 없애고 순수 함수를 만들어 모듈화 수준을 높이는 프로그래밍 패러다임

    • 부수 효과: 외부 상태를 변경하거나 함수로 들어온 인자 상태를 변경하는 것
    • 순수 함수
      • 동일한 입력에 대해 항상 동일한 출력을 반환하는 함수
      • 외부의 상태를 변경하거나 영향을 받지 않는 함수

    순수한 함수

    function func(a, b) {
      return a + b;
    }
    
    console.log(func(2, 2)); // 4
    

    func 함수는 순수하다. 언제나 이 함수를 수백번 실행시켜도 입력값이 2, 2면 출력값이 4로 동일하기 때문이다. 또한 이 함수는 외부의 값에 영향을 주거나 받지 않는다.

    순수하지 않은 함수

    let c = 1;
    
    function func(a, b) {
      return a + b + c;
    }
    
    console.log(func(2, 2)); // 5
    
    c = 2; // c 값이 변경됨
    
    console.log(func(2, 2)); // 6
    

    위 함수는 외부 값인인 c에 영향을 받기 때문에 순수함수가 아니다. c가 변하면 동일한 입력에 대해 출력이 다르기 때문이다.

    let c = 1;
    
    function func(a, b) {
      c += 1; // 외부의 값에 변화를 주며, 이를 부수효과라 함
      return a + b;
    }
    
    func(2, 2); // 함수 실행
    
    console.log(c); // c 값이 2로 변화됨
    

    위 함수도 함수가 실행되면 외부값인 c를 변경시키기 때문에 순수 함수가 아니며, 이를 부수 효과라 한다.

    let obj = {
      a: 1
    };
    
    function func(obj) {
      return obj.b = 1; // 인자로 받은 객체에 b 값을 추가하여 리턴
    }
    
    func(obj);
    
    console.log(obj); // { a: 1, b: 1 }
    

    객체의 경우도 살펴보자. 위 함수도 외부 obj 객체에 b가 추가되었기 때문에 순수함수가 아니다.

    let obj = {
      a: 1
    };
    
    function func(obj) {
      return obj; // 인자로 받은 객체를 그대로 리턴
    }
    
    let obj2 = func(obj);
    

    위의 경우는 어떨까? 함수 안에서는 객체를 받고 아무런 변화를 주지 않고 리턴하였으며, 새로운 변수에 리턴된 객체를 할당했다. 위 함수에서는 객체에 아무런 변화를 주지 않았으니 순수 함수라고 할 수 있을까?

    let obj = {
      a: 1
    };
    
    function func(obj) {
      return obj; // 인자로 받은 객체를 그대로 리턴
    }
    
    let obj2 = func(obj); // 새로운 변수에 리턴된 객체를 할당
    
    obj2.a = 2; // 새로운 객체 obj2의 a 값을 변경
    
    console.log(obj2); // { a: 2 }
    console.log(obj); // { a: 2 }
    

    정답은 아니다. func 함수를 실행하여 새로운 변수에 리턴받은 객체를 할당했으며, 새로운 객체 obj2a 값을 변경하였다. 그랬더니 기존의 obj 객체의 값도 변경이 되었다. 바로 객체의 참조(주소) 값도 같이 복사되어 새롭게 만든 obj2가 변화함에 따라 기존의 obj 객체도 변경되기 때문이다. 이처럼 함수 내에서 직접 값을 변경하지 않았더라도 함수에 들어온 인자값을 그대로 사용하면 순수 함수가 아니다.

    let obj = {
      a: 1
    };
    
    function func(obj) {
    
      // 객체의 값만 참조하여 새로운 객체를 리턴
      return {
        a: obj.a,
        b: 2
      };
    }
    
    let obj2 = func(obj);
    
    obj2.a = 2;
    
    console.log(obj2); // { a: 2, b: 2 }
    console.log(obj); // { a: 1 }
    

    위에서는 인자로 받은 객체를 직접 사용하지 않고 obj.a 값만 참조해서 새로운 객체를 생성하여 리턴하고 있다. 이럴 경우는 참조(주소)값이 복사가 안되기 때문에 obj2 객체의 값이 변경되도 obj의 값이 변경되지 않는다. 그러므로 위 함수는 순수 함수라 할 수 있다.

    결론

    모든 함수가 순수 할수일 수는 없다. 모든 함수가 순수 함수라면 외부의 어떤 데이터에도 변형을 주지 않기 때문에 프로그램은 구동되지 않을 것이다. 단지, 이런 스타일로 코딩하는것이 함수형 프로그래밍의 패러다임 이며, 이 패러다임의 목적은 외부 상태의 변화를 최소함으로 유지하고, 함수 실행 결과 예측을 용이하게 하여 버그 발생 가능성을 줄이는 것에 목적이 있다.

    [번역] JavaScript 함수형 프로그래밍 3단계로 설명하기
    순수 함수란 무엇인가요… 별거 없음…
    JS 함수형 프로그래밍을 위한 사전 지식 : 순수함수, 일급함수
    자바스크립트의 함수형 프로그래밍 1 : 순수 함수란?
    순수 함수란? (함수형 프로그래밍의 뿌리, 함수의 부수효과를 없앤다)

  • [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] 얕은 복사, 깊은 복사

    [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 :: 마이구미

  • [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

  • [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


  • [Git] 터미널 계정 변경 방법(Windows)

    [Git] 터미널 계정 변경 방법(Windows)

    1. 현재 깃에 설정된 계정/메일 확인

    $ git config user.name
    $ git config user.email
    

    터미널에서 위 명령어로 현재 설정된 계정/메일을 확인할 수 있다.

    2. 원하는 깃 계정/메일 변경

    $ git config --global user.name 변경을 희망하는 계정
    $ git config --global user.email 변경을 희망하는 이메일
    

    위 명령어로 계정/메일을 변경할 수 있다.

    3. Push 오류 확인

    계정을 변경하고 푸쉬를 하면 아래와 비슷한 오류가 나온다.
    아래 오류를 해결하러면 Windows 자격 증명을 변경해야 한다.

    remote: Permission to bitnaleeeee/bitnaleeeee.github.io.git denied to recordboy.
    fatal: unable to access 'https://github.com/bitnaleeeee/bitnaleeeee.github.io.git/': The requested URL returned error: 403
    

    4. Windows 자격 증명 변경

    자격 증명 변경 경로

    제어판 > 사용자 계정 > 자격 증명 관리자 > Windows 자격 증명

    자격 증명 변경 과정

    1. 제어판 > 사용자 계정

    2. 제어판 > 사용자 계정 > 자격 증명 관리자

    3. 자격 증명 관리자 > Windows 자격 증명 > 일반 자격 증명 > git:https://github.com 제거

    4. 자격 증명 관리자 > Windows 자격 증명 > 일반 자격 증명 > 일반 자격 증명 추가

    5. 일반 자격 증명 추가 > 정보 입력하고 확인

    1. 인터넷 또는 네트워크 주소: https://api.github.com/변경할 계정
    2. 사용자 이름: 변경할 계정
    3. 암호: 변경할 계정 암호

    6. 깃허브 인증 팝업 출력 > Sign in with your browser 클릭

    7. 깃허브 인증 팝업 출력 > Authorize GitCredentialManager 클릭하면 인증 성공

    8. 다시 Git Push 제대로 되는지 확인

    References

    [Git] Git Bash 터미널 계정 변경 방법! (Windows)

  • [Server] Vercel에 JSON Server 배포

    [Server] Vercel에 JSON Server 배포

    Heroku는 2022년 11월을 기준으로 유로로 변경되었기 때문에 무료로 서버를 배포할 플랫폼을 찾던중 Vercel를 알게되었다. JSON Server와 Vercel를 이용하면 간략한 서버를 구축 및 배포할 수 있다.

    JSON Server

    내부적으로 Low DB 라는 단순한 데이터베이스를 이용하며 최소한의 REST API를 기본 지원한다. 프로토타입을 만들거나 토이프로젝트를 만들때 유용하게 사용할 수 있다.

    공식 홈페이지: json-server

    Vercel

    Next.js 개발 팀에서 만든 클라우트 플랫폼으로 빌드/배포/호스팅 서비스를 제공한다. GitHub 저장소와 연결하면 매번 Push를 진행할 때 마다 배포할 수 있다.

    공식 홈페이지: Vercel

    JSON Server 만들기

    1. 프로젝트 생성

    $ mkdir json-server-exam && cd json-server-exam
    $ npm init -y
    $ npm install json-server --save-dev
    

    json-server-exam 디렉토리 생성 && 프로젝트 초기화를 한 뒤 JSON Server를 설치한다.

    2. server.js 추가

    const jsonServer = require('json-server')
    const server = jsonServer.create()
    const router = jsonServer.router('db.json')
    const middlewares = jsonServer.defaults()
    
    // Set default middlewares (logger, static, cors and no-cache)
    server.use(middlewares)
    
    // Add custom routes before JSON Server router
    server.get('/echo', (req, res) => {
      res.jsonp(req.query)
    })
    
    // To handle POST, PUT and PATCH you need to use a body-parser
    // You can use the one used by JSON Server
    server.use(jsonServer.bodyParser)
    server.use((req, res, next) => {
      if (req.method === 'POST') {
        req.body.createdAt = Date.now()
      }
      // Continue to JSON Server router
      next()
    })
    
    // Use default router
    server.use(router)
    server.listen(3001, () => {
      console.log('JSON Server is running')
    })
    

    서버로 사용할 server.js파일을 루트 경로에 추가해 준다.

    3. db.json 추가

    {
      "todos": [
        {
          "id": 1,
          "content": "HTML",
          "completed": true
        },
        {
          "id": 2,
          "content": "CSS",
          "completed": false
        },
        {
          "id": 3,
          "content": "Javascript",
          "completed": true
        }
      ],
      "users": [
        {
          "id": 1,
          "name": "bitna",
          "role": "developer"
        },
        {
          "id": 2,
          "name": "seongju",
          "role": "designer"
        }
      ]
    }
    

    DB로 사용할 db.json 파일을 루트 경로에 추가해준다.

    4. 서버 실행

    $ npm start
    

    서버를 실행하고 http://localhost:3001경로에 들어가면 실행된 JSON Server를 확인할 수 있다. 이제 아래와 같은 REST API를 이용할 수 있다.

    REST API

    기능MethodPath
    목록조회GET/todos
    추가POST/todos
    삭제DELETE/todos/:id
    수정PUT/todos/:id

    REST API 요청 예시(Fetch API 사용)

    GET(조회)

    fetch("http://localhost:3001/todos",)
    .then((response) => response.json())
    .then((data) => {
        // code..
    });
    

    POST(추가)

    fetch("http://localhost:3001/todos", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            "id": 5,
            "content": "React",
            "completed": true
        }),
    })
    .then((response) => response.json())
    .then((data) => {
        // code..
    });
    

    DELETE(삭제)

    fetch("http://localhost:3001/todos/1", {
        method: "DELETE"
    });
    

    PUT(수정)

    fetch("http://localhost:3001/todos/2", {
        method: "PUT",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            "id": 2,
            "content": "Linux",
            "completed": false
        }),
    })
    .then((response) => response.json())
    .then((data) => {
        // code..
    });
    

    Vercel 배포하기

    1. 배포하기 전에 추가 설정파일 만들기

    배포하기 전에 몇가지 설정파일을 추가해 준다.

    깃 이그노 추가

    GitHub 저장소에 올리기전에 루트 경로에 꼭 .gitignore 파일을 추가하고 아래 리스트를 추가해 준다.

    /node_modules
    

    Vercel 설정 파일 추가

    Vercel 배포 설정을 위해 루트 경로에 vercel.json 파일을 추가하고 아래 내용을 넣어준다.

    {
      "version": 2,
      "builds": [
        {
          "src": "server.js",
          "use": "@vercel/node",
          "config": {
            "includeFiles": ["db.json"]
          }
        }
      ],
      "routes": [
        {
          "src": "/(.*)",
          "dest": "server.js"
        }
      ]
    }
    

    2. GitHub 저장소에 올리기

    GitHub 저장소에 새로운 저장소를 만든 뒤 프로젝트를 올려준다.(GitHub에 프로젝트를 올리는 설명은 생략한다.)

    3. 회원 가입

    Vercel 공식 홈페이지(https://vercel.com)에서 회원가입을 한다. GitHub 계정으로 로그인 해준다.

    4. 새로운 프로젝트 추가하기

    Add New >> Project 버튼을 클릭한다.

    추가했던 프로젝트(저장소)를 import 한다.

    5. 옵션 설정 및 배포하기

    Build and Output Settings 메뉴에서 빌드 및 배포 설정을 할 수 있다. 여기서는 따로 vercel.json 파일을 추가했으니 설정은 건너 뛰고 바로 배포(Deploy) 버튼을 클릭한다.

    6. 배포 완료

    배포가 잘 되었다면 위처럼 화면을 확인할 수 있다.

    References

    JSON Server
    How to deploy an Express API to Vercel
    14.3 JSON Server
    30초 안에 RESTful API서버 만들기

  • [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

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