ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 노드 개념, 기능, 자바스트립트
    Node.js/백엔드 2020. 1. 4. 15:07

    본 글은 "Node.js 교과서: 기본기에 충실한 노드제이에스 10 입문서"를 토대로 작성되었습니다.

    노드란?

    Node.js는 Chrome V8 Javascript 엔진으로 빌드된 Javascript 런타임이다.

    런타임은 컴퓨터 프로그램이 실행되고 있는 동안의 동작을 말한다.
    런타임 환경은 컴퓨터가 실행되는 동안 프로세스나 프로그램을 위한 소프트웨어 서비스를 제공하는 가상 머신의 상태이다. ← 이 말이 Node.js의 정의에 좀 더 가깝다.
    JAVA는 JVM(Java Virtual Machine)으로 유명하다. JVM 을 깔면 어느 플랫폼에서나 동작한다.
    Node.js도 마찬가지로 가상머신을 포함하고 있기 때문에 어느 플랫폼에서나 자바스크립트 런타임을 사용할 수 있게 해준다.


    결국 자바스크립트 런타임이란, 자바스크립트를 웹브라우저 바깥 환경에서 돌릴 수 있게 해주는 프로그램이다.

    REPL

    Read, Evaluate, Print, Loop

    • Read → 입력한 명령을 읽는다.
    • Evaluate → 명령을 평가한다.
    • Print → 결과를 출력한다.
    • Loop → 다음 명령을 기다린다.

    호출 스택과 이벤트 루프

    호출 스택이 비어있을 때 여러 개의 태스크 큐에서 작업을 순서대로 가져와 수행시킨다.
    언제 태스크 큐에 들어가나? → setTimeout, setInterval, setImmediate, Promise resolve/reject, async/await, 이벤트리스너의 콜백
    이벤트 루프의 동작을 잘 알고 있으면 코드의 실행 순서를 조종할 수 있다.
    https://nodejs.org/ko/docs/guides/event-loop-timers-and-nexttick/

    이벤트 기반(이벤트 드리븐)

    이벤트리스너 → 태스크 큐 → 이벤트 루프가 태스크 큐에서 작업을 순서대로 호출 스택으로 옮김 → 함수 실행
    이벤트리스너에 달린 콜백 함수들이, 이벤트가 실행될 때 태스크 큐로 들어간다.
    콜백함수가 꼭 이벤트리스너에 등록되는 것은 아니고, 즉시 실행 함수로서 호출 스택에서만 머무를 수도 있다.

    image-20191228120107282

    논 블로킹 I/O

    태스크 큐로 보내서 실행 순서를 달라지게 하는 것.
    순서가 눈에 보이는 코드의 순서와 다르면 논 블로킹이라고 보면 된다.
    파일 시스템 : 알아서 멀티 스레드로 돌림
    네트워크


    제어권 문제 때문에 멀티쓰레드 프로그래밍은 어렵다.
    프로세스를 여러 개 만들어 멀티 프로세싱을 하는 것이 노드가 싱글스레드를 극복하는 중 방법이다.

    기능

    노드 모듈 시스템

    Screen Shot 2019-12-30 at 10 46 36 AMScreen Shot 2019-12-30 at 10 47 06 AM

    module.exports = 함수 이름;

    global 객체

    // globalA.js
    module.exports = () => global.message;
    // globalB.js
    const A = require('./globalA');
    
    global.message = '안녕하세요';
    console.log(A()); // 안녕하세요

    console 객체

    console.log(...); 에서 log는 console 객체의 메서드다.
    console 객체 안에 디버깅을 도와주는 많은 메서드가 있다.

    Screen Shot 2019-12-30 at 2 06 08 PM

    console.time('인자') / console.timeEnd('인자') : 인자가 같아야 그 사이의 시간을 잰다.
    console.dir(객체, { colors: true(false), depth: 2(1) }) : 객체 로깅에 사용
    console.trace(...) : 호출 스택 추적

    Screen Shot 2019-12-30 at 2 42 06 PMScreen Shot 2019-12-30 at 2 43 49 PM

    타이머(setTimeout, setInterval, setImmediate)

    const timeout = setTimeout(() => {
      console.log('1.5초 후 실행');
    }, 1500);
    
    const interval = setInterval(() => {
      console.log('1초마다 실행');
    }, 1000);
    
    const timeout2 = setTimeout(() => {
      console.log('실행되지 않습니다.');
    }, 3000);
    
    setTimeout(() => {
      clearTimeout(timeout2);
      clearInterval(interval);
    }, 2500);
    
    // 결과
    /* 1초마다 실행
         1.5초 후 실행
         1초마다 실행 */

    setTimeout, setInterval로 설정
    clearTimeout, clearInterval로 해제
    즉시 실행되는 setImmediate : 함수를 이벤트 루프로 보낼 때 사용. 비동기로 실행 순서가 달라지게 만들 수 있다.

    __filename, __dirname, process

    노드는 싱글스레드 단점을 극복하기 위해 멀티 프로세싱을 한다.
    global.process

    • process 객체에는 현재 실행중인 노드 프로그램 정보가 들어있다.
    • process.pid → 현재 실행중인 프로세스의 id
    • process.cwd() → 프로세스 실행 위치
    • __dirname → 파일의 위치
    • process.execPath → 노드가 설치된 경로
    • process.cpuUsage() → CPU 사용량
    • process.exit() → repl이 죽는다. 프로세스 종료. 에러가 발생했을 때, 서비스를 아예 죽이고 다시 실행하는 경우 사용.
    for (let i=0; i<100000; i++){
        console.log(i);
        process.exit();
    }  
    // 0

    Screen Shot 2019-12-31 at 12 13 44 AM

    os 모듈

    운영체제와 관련된 내장 모듈
    os.uptime() → 운영체제가 시작되고나서 흐른 시간
    process.uptime() → 노드 프로그램이 시작되고나서 흐른 시간
    os.cpus()
    노드는 싱글스레드이기 때문에 하나의 코어만 사용한다. 만약 코어가 4개면 나머지 3개가 논다.
    실무에서는 cpus()로 CPU 개수를 파악한 후, 반복문을 돌려 4개의 노드 프로세스를 만들어 싱글 스레드라는 단점을 극복한다 = 멀티 프로세스

    Screen Shot 2019-12-31 at 1 24 28 AM

    path 모듈

    path.sep → 경로 구분자 windows(\\), POSIX(/)
    path.delimiter → 환경변수 구분자 windows(;), POSIX(:)
    node나 npm같은 명령어를 칠 수 있는 이유 → 실제로 node나 npm 같은 프로그램이 존재하고, 그 프로그램의 경로가 환경변수에 존재하기 때문.

    // path.js
    const path = require('path');
    
    console.log(path.dirname(__filename));
    console.log(path.extname(__filename)); // path.js의 확장자
    console.log(path.basename(__filename)); // path.js의 파일 이름

    Screen Shot 2019-12-31 at 1 58 41 PM

    console.log(path.parse(__filename)); // parse 분해
    console.log(path.format({ // format 합쳐줌
        root: '/',
        dir: '/Users/rat2/Desktop/nodejs/examples',
        base: 'path.js',
        ext: '.js',
        name: 'path'
    }));

    Screen Shot 2019-12-31 at 8 36 21 PM

    console.log(path.normalize('/Users////rat2///path.js')); 
    /* /Users/rat2/path.js */

    ./ → 현재 폴더 상대 경로
    ../ → 부모 폴더 상대 경로
    / → 루트 절대 경로

      console.log(path.isAbsolute('/Users/rat2/Desktop/nodejs/examples/path.js')); // true
      console.log(path.relative('/Users/rat2/Desktop/nodejs/examples/path.js','/Users')); // ../../../../..

    path.relative('경로1', '경로2'); 로 경로1에서 2로 가는 상대 경로를 알 수 있다.

    console.log(path.join(__dirname, '..', '..', '/users', '.', '/rat2'));
    console.log(path.resolve(__dirname, '..', '..', '/users', '.', '/rat2'));

    path.join → 절대 경로를 무시하고 합친다. 절대경로도 상대경로로 취급한다.
    path.resolve → 절대 경로를 고려하고 합친다.

    Screen Shot 2019-12-31 at 9 30 31 PM

    url 모듈

    Screen Shot 2020-01-01 at 4 27 17 AMScreen Shot 2020-01-01 at 3 26 29 AM

    위쪽 → 기존 방식의 주소 체계(url.parse)

    Screen Shot 2020-01-01 at 4 22 55 AM

    • 호스트가 없을 때도 쓸 수 있다.
    • 주소가 /hello?page=10 만 있을 수 있다. 동일한 도메인에서 요청을 주고받는 경우, 도메인을 생략할 수 있기 때문.
    • 이런 주소를 파싱할 때는 url.parse 방식을 사용해야한다.

     

    아래쪽 → WHATWG 방식의 주소 체계(url.URL)

    Screen Shot 2020-01-01 at 4 22 32 AM

    • username, password, origin, searchParams 속성이 존재.
    • 로그인을 해야만 들어갈 수 있는 웹에서는 username, password 사용.
    • search 처리가 편리하다.
    • searchParamsScreen Shot 2020-01-01 at 4 17 30 AM Screen Shot 2020-01-01 at 4 17 51 AM
    • 노드 searchParams의 메서드는 FormData나 URLSearchParams 객체에도 비슷하게 쓰인다.

    querystring 모듈

    const url = require('url');
    const querystring = require('querystring');
    
    const parsedUrl = url.parse('http://www/gilbut.co.kr/?page=3&limit=10&category=nodejs&category=javascript');
    const query = querystring.parse(parsedUrl.query);
    
    console.log('querystring.parse():', query);
    console.log('querystring.stringify():', querystring.stringify(query));

    Screen Shot 2020-01-01 at 9 29 58 AM

    url.parse 방식과 자주 쓰인다.
    querystring.parse(쿼리): url의 query 부분을 자바스크립트 객체로 분해.
    querystring.stringify(객체): 분해된 query 객체를 문자열로 다시 조립.

    crypto 단방향 암호화(해시) - 복호화가 안 되는 암호화

    const crypto = require('crypto'); // 노드는 자체적으로 암호화를 도와주는 crypto 모듈 지원
    
    console.log(crypto.createHash('sha512').update('비밀번호').digest('base64'));

    Screen Shot 2020-01-01 at 10 37 02 AM

    createHash(알고리즘): 사용할 해시 알고리즘을 넣어준다. sha512.
    update(문자열): 변환할 문자열(비밀번호)을 넣어준다.
    digest(인코딩): 인코딩할 알고리즘을 넣어준다. base64, hex, latin1 주로 사용. base64가 결과 문자열이 가장 짧아 애용됨. 결과물로 변환된 문자열 반환.


    비밀번호는 hash방식으로 암호화 해 복호화되지 않는 문자열을 만든다. = 복호화 불가능
    암호문(해시)을 데이터베이스에 저장한 후, 사용자의 입력 비밀번호를 저장된 것과 비교한다.

    사용자가 로그인 시 아이디, 비밀번호를 입력하면 그 비밀번호를 다시 해시 방식으로 암호화하고, 암호화된 두 값(입력 값, DB에 저장된 값)이 일치하면 비밀번호가 일치하는 것. 즉, 원래 비밀번호는 저장될 필요가 없으므로 복호화 할 이유도 없다.

    가끔 nopqrst라는 문자열이 abcdefgh를 넣었을 때와 똑같은 출력 문자열로 바뀔 때도 있다. = 충돌 발생
    해킹용 컴퓨터의 역할은 어떠한 문자열이 같은 출력 문자열을 반환하는지 찾아내는 것이다.
    비밀번호가 짧고 쉬울수록 더욱 충돌 공격에 취약.

     

    pbkdf2
    노드에서는 createHash보다 강력한 pbkdf2를 지원한다.
    해시 충돌 공격을 어렵게 하기 위해 salt(소금)라는 문자열을 원래 비밀번호에 추가하고 iteration횟수를 높인다.

    const crypto = require('crypto');
    
    crypto.randomBytes(64,(err, buf) => {
        const salt = buf.toString('base64');
           console.log('salt', salt);
        console.time('암호화');
        crypto.pbkdf2('비밀번호', salt, 1301395, 64, 'sha512', (err, key) => {
          console.log('password', key.toString('base64'));
          console.timeEnd('암호화');
        }); // 콜백 지옥 발생 중 - 깊어지고 있다.
    });

    Screen Shot 2020-01-01 at 12 11 37 PM

    salt 는 random한 bytes이기 때문에 실행할 때마다 값이 다르다.
    salt 는 암호화된 비밀번호와 같이 저장하고, iteration은 1초 정도가 걸릴 때까지 올려주면 좋다.
    실무에서는 pbkdf2도 잘 안 쓰고 bcrypt나 scrypt 많이 사용

    crypto 양방향 암호화 - 암호화 + 복호화

    Screen Shot 2020-01-01 at 12 25 18 PMScreen Shot 2020-01-01 at 12 26 41 PM

    createCipher → utf8 평문을 base64 암호문으로
    createDecipher → base64 암호문을 utf8 평문으로

    util 모듈(deprecate, promisify)

    deprecate

    const util = require('util');
    
    const dontuseMe = util.deprecate((x,y) => {
      console.log(x+y);
    }, 'dontuseMe 함수는 deprecated 되었으니 더 이상 사용하지 마세요!');
    
    dontuseMe(1,2);

    Screen Shot 2020-01-01 at 1 50 30 PM

    deprecated → 지원이 조만간 중단될 메서드임을 알려줌.
    util.deprecate: 함수가 deprecated 처리 되었음을 알려준다. 첫 번째 인자로 넣은 함수를 사용했을 때 경고 메시지가 출력된다. 두 번째 인자로 경고 메시지 내용을 넣는다. 두 번째 인자(경고 메시지)를 생략하면,

    Screen Shot 2020-01-01 at 12 42 45 PM

    ERR_INVALID_ARG_TYPE 에러가 발생한다.

    promisify

    const util = require('util');
    const crypto = require('crypto');
    
    // 콜백 2개 중첩
    crypto.randomBytes(64, (err,buf) => {
      const salt = buf.toString('base64');
      console.log('salt', salt);
      crypto.pbkdf2('비밀번호', salt, 1301395, 64, 'sha512', (err,key) => {
        console.log('password', key.toString('base64'));
      });
    });
    
    // 콜백 패턴을 프로미스 패턴으로 변경 - util.promisify(변경할 함수); 이용
    const randomBytesPromise = util.promisify(crypto.randomBytes);
    const pbkdf2Promise = util.promisify(crypto.pbkdf2);
    
    randomBytesPromise(64)
        .then((buf) => {
          const salt = buf.toString('base64');
          return pbkdf2Promise('비밀번호', salt, 1301395, 64, 'sha512');
        })
        .then((key) => {
          console.log('password', key.toString('base64'));
        })
        .catch((err) => {
          console.error(err);
        });
    
    // async, await로 변경
    (async () => {
      try{
        const buf = await randomBytesPromise(64);
        const salt = buf.toString('base64');
          const key = await pbkdf2Promise('비밀번호', salt, 1301395, 64, 'sha512');
          console.log('password', key.toString('base64'));
      }catch(err){
        console.error(err);
      }
    })();

    util.promisify: randomBytes, pbkdf2 같이 프로미스를 지원하지 않는 함수들의 콜백 패턴을 프로미스 패턴으로 바꿔준다. 바꿀 함수를 인자로 제공.

    fs 모듈(동기와 비동기)

    비동기식에서, 메인 스레드는 수백 개의 I/O 요청이 들어와도 백그라운드에 요청 처리를 위임하고 그 후로도 얼마든지 요청을 더 받을 수 있다. 나중에 백그라운드가 각각의 요청 처리가 완료되었다고 알리면 그때 콜백 함수를 처리한다.

    • 동기/비동기: 함수가 바로 return 되는지 여부

      return 은, 현재 있는 함수에서 빠져나가 그 함수를 호출했던 곳으로 되돌아가 어떤 값을 반환하는 것이다. return 이 실행되는 즉시 그 함수는 무조건 실행이 종료된다.

    • 블로킹/논블로킹: 백그라운드 작업 완료 여부

     

    동기-블로킹 방식 → 백그라운드 작업 완료 여부를 계속 확인하며, 호출한 함수가 바로 return 되지 않고 백그라운드 작업이 끝나야 return 된다.
    비동기-논블로킹 방식 → 호출한 함수가 바로 return 되어 다음 작업으로 넘어가고, 백그라운드 작업 완료 여부는 신경쓰지 않고 나중에 백그라운드가 알림을 줄 때 처리한다.

    백그라운드 프로세스 는 사용자 간섭 없이 보이지 않는 뒷편에서 실행 중인 컴퓨터 프로세스이다. 이러한 프로세스를 위한 일반적인 작업에는 로그 처리, 시스템 모니터링, 스케줄링, 사용자 통보 등이 있다.
    출처: 위키백과

    <비동기 메서드>

    Screen Shot 2020-01-01 at 3 15 05 PMScreen Shot 2020-01-01 at 3 15 36 PM

    콜백 → 비동기
    실제로는 비동기 방식을 더 많이 쓴다. 동기식을 사용할 경우, 처리가 오래 걸리는 작업에서 블로킹이 발생하면 다른 요청들을 처리할 수 없기 때문에 문제가 발생하기 때문이다.

    <동기 메서드>

    Screen Shot 2020-01-01 at 10 11 05 PMScreen Shot 2020-01-01 at 10 11 40 PM

    fs 메서드들은 뒤에 Sync를 붙이면 동기식으로 작동한다.
    fs.writeFile((err, data) => {}); → const data = fs.writeFileSync();
    데스크탑 프로그램 또는 단 한 번만 실행되는 함수에서 사용해도 된다.

    파일 읽기, 만들기

    // writeFile.js
    
    const fs = require('fs');
    
    fs.writeFile('./writeme.txt', '눈이 오던 그 길로 한참을 왔을 때 돌아보면 너는 거기 있는 걸 여행이 끝날 때쯤 난 알게 될 거야 그때처럼 널 가슴에 가득 안고 I’m coming home', (err) => {
      if(err){
        throw err;
      }
      fs.readFile('./writeme.txt', (err, data) => {
        if(err){
          throw err;
        }
        console.log(data.toString);
      });
    });

    Screen Shot 2020-01-01 at 10 20 35 PM

    fs.readFile('읽을 파일의 경로', (err, data) => {...});
    readFile의 결과물은 버퍼 형식으로 제공된다. toString()을 사용해 문자열로 변환할 수 있다.
    fs.writeFile('생성될 파일의 경로', '파일 내용', (err) => {...});

    버퍼와 스트림

    노드에서 이미지나 파일을 업로드할 때 버퍼와 스트리밍이 사용이 된다.
    노드는 파일을 읽을 때 메모리에 파일 크기만큼 공간을 마련해두며, 파일 데이터를 메모리에 저장한 뒤 사용자가 조작할 수 있도록 해준다.
    메모리에 저장된 데이터 = 버퍼

    버퍼

    버퍼는 데이터를 한 곳에서 다른 한 곳 전송하는 동안 일시적으로 그 데이터를 보관하는 메모리의 영역이다.
    버퍼링이란 버퍼를 활용하는 방식 또는 버퍼를 채우는 동작을 말한다.
    다른 말로 '큐'라고도 표현한다.
    출처: 위키백과

    • 버퍼링: 영상을 재생할 수 있을 때까지 데이터를 모으는 동작.
    • Buffer → 노드 global 객체
    • from(문자열): 문자열을 버퍼로 바꾼다. length 속성은 버퍼의 크기(바이트 단위)를 알려준다.
    • toString(버퍼): 버퍼를 다시 문자열로 바꿀 수 있다. base64나 hex를 인자로 넣으면 해당 인코딩으로도 변환할 수 있다.
    • concat(배열): 배열 안에 든 버퍼들을 하나로 합친다.
    • alloc(바이트): 빈 버퍼를 생성한다. 바이트를 인자로 지정해주면 해당 크기의 버퍼가 생성된다.

    Screen Shot 2020-01-01 at 11 08 21 PMScreen Shot 2020-01-01 at 11 08 55 PM

    readFile() 방식의 버퍼가 편리하기는 하지만 문제점이 있다.

    • 용량이 100MB인 파일이 있으면 읽을 때 메모리에 100MB의 버퍼를 만들어야 한다.
    • 서버 같이 몇 명이 이용할지 모르는 환경에서는 메모리 문제가 발생할 수 있다.
    • 모든 내용을 버퍼에 다 쓴 후에야 다음 동작으로 넘어간다. 파일 읽기, 압축, 파일 쓰기 등의 조작을 연달아 할 때 매번 전체 용량을 버퍼로 처리해야 다음 단계로 넘어갈 수 있다.

    스트림

    readFile() 방식 버퍼의 문제점의 대안책으로 버퍼의 크기를 작게 만들어 여러 번에 나눠서 보내는 방식 이 등장했다.
    버퍼 1MB를 만든 후 100MB 파일을 백 번에 걸쳐 보내는 것.

     

    스트리밍: 방송인의 컴퓨터에서 시청자의 컴퓨터로 영상 데이터를 조금씩 전송하는 동작.
    스트리밍하는 과정에서 버퍼링을 할 수도 있다.
    전송이 너무 느리면 화면을 내보내기까지 최소한의 데이터를 모아야 하고, 영상 데이터가 재생 속도보다 빨리 전송되어도 미리 전송받은 데이터를 저장할 공간이 필요하기 때문.

    Screen Shot 2020-01-01 at 11 31 00 PM

    // createReadStream.js
    const fs = require('fs');
    
    const readStream = fs.createReadStream('./readme3.txt', {highWaterMark: 16}); // 16B 버퍼를 채우면 읽고, 또 채우면 읽는 식으로 스트리밍이 흘러간다.
    const data = [];
    
    readStream.on('data', (chunk) => {
      data.push(chunk); // 16B씩 전송받은 데이터를 data배열에 push한다.
      console.log('data :', chunk, chunk.length);
    });
    // 16B씩 떼서 옮기다가 다 끝났을 때 end이벤트 발생.
    readStream.on('end', () => {
      console.log('end :', Buffer.concat(data).toString());
    });
    
    readStream.on('error', (err) => {
      console.log('error :', err);
    });

    Screen Shot 2020-01-01 at 11 44 43 PM

    스트림은 이벤트 기반으로 동작.
    readStream에 이벤트 리스너를 붙여 사용한다. 보통 data, end, error 이벤트를 사용.
    파일 읽기가 시작되고, 버퍼(청크)들이 들어올 때마다 data 이벤트 발생.
    highWaterMark 옵션: 버퍼의 크기(바이트 단위)를 정할 수 있는 옵션. 기본값은 64KB.

    쓰기 스트림

    // createWriteStream.js
    
    const fs = require('fs');
    // createWriteStream으로 쓰기 스트림 만들어줌.
    const writeStream = fs.createWriteStream('./writeme2.txt');
    
    // finish 이벤트 리스너 - 파일 쓰기가 종료되면 콜백 함수 호출
    writeStream.on('finish', ()=>{
        console.log('파일 쓰기 완료');
    });
    // write() 메서드로 넣을 데이터를 쓴다.
    writeStream.write('체리밤\n');
    writeStream.write('Coming Home\n');
    writeStream.write('악몽');
    writeStream.end(); // 종료 알림. finish이벤트 발생.

    Screen Shot 2020-01-01 at 11 56 58 PMScreen Shot 2020-01-01 at 11 57 34 PM

    파이프

    스트림은 버퍼의 흐름이기 때문에 여러 개의 스트림을 이어 버퍼가 흘러가게 할 수 있다.
    createReadStream으로 파일을 읽고 그 스트림을 전달받아 createWriteStream으로 파일을 쓸 수 있다.
    스트림끼리 연결하는 것 = 파이핑한다
    복사

    // 예전 방법(pipe)
    const fs = require('fs');
    
    const readStream = fs.createReadStream('readme4.txt');
    const writeStream = fs.createWriteStream('writeme3.txt');
    
    readStream.pipe(writeStream);
    // 새로운 방법(copyFile)
    const fs = require('fs');
    
    const readStream = fs.copyFile('./readme4.txt', './writeme4.txt', (err) => {
        console.log(err);
    });

    파이프는 연달아 쓸 수 있다. 압축할 때 사용.

    Screen Shot 2020-01-02 at 12 24 05 AMScreen Shot 2020-01-02 at 12 24 26 AM

    기타 fs 메서드

    // 폴더, 파일 생성
    const fs = require('fs');
    
    fs.access('./folder', fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK, (err) => {
      if(err){
        if(err.code === 'ENOENT'){
          console.log('폴더 없음');
          fs.mkdir('./folder', (err) => {
            if(err){
              throw err;
            }
            console.log('폴더 만들기 성공');
            fs.open('./folder/file.js', 'w', (err, fd) => { // fd -> 파일 아이디
              if(err){
                  throw err;
                }
              console.log('빈 파일 만들기 성공', fd); 
              fs.rename('./folder/file.js', './folder/newfile.js', (err) => {
                if(err){
                      throw err;
                    }
                console.log('이름 바꾸기 성공');
              });
            });
          });
        }else{
          throw err;
        }
      }else{
        console.log('이미 폴더 있음');
      }
    });

    Screen Shot 2020-01-03 at 10 27 56 AMScreen Shot 2020-01-03 at 10 28 23 AM

    fs.access('경로', 옵션, 콜백) : 폴더나 파일에 접근할 수 있는지를 체크. 두 번째 인자로 상수(F_OK, R_OK, W_OK)를 넣을 수 있다.

    • F_OK(파일 존재 여부),
    • R_OK(읽기 여부),
    • W_OK(쓰기 여부)

     

    fs.mkdir(경로, 콜백) : 폴더를 만드는 메서드. 이미 폴더가 있다면 에러가 발생하므로 먼저 access() 메서드를 호출하여 확인하는 것이 중요.
    fs.open(경로, 옵션, 콜백) : 파일의 아이디(fd 변수)를 가져오는 메서드. 파일이 없다면 옵션에 w(쓰기)를 설정해서 파일을 생성한 뒤 그 아이디를 가져온다. 쓰려면 r, 읽으려면 r, 기존 파일에 추가하려면 a이다.

    // 폴더, 파일 삭제
    const fs = require('fs');
    
    fs.readdir('./folder', (err, dir) => {
      if(err){
        throw err;
      }
      console.log('폴더 내용 확인', dir);
      fs.unlink('./folder/newFile.js', (err) => {
        if(err){
          throw err;
        }
        console.log('파일 삭제 성공');
        fs.rmdir('./folder', (err) => {
          if(err){
              throw err;
            }
            console.log('폴더 삭제 성공');
        });
      });
    });

    Screen Shot 2020-01-03 at 10 47 02 AM

    fs.readdir(경로, 콜백) : 폴더 안의 내용물 확인. 배열 안에 내부 파일과 폴더명이 나온다.
    fs.unlink(경로, 콜백) : 파일을 지울 수 있다. 파일이 없다면 에러가 발생하므로 먼저 파일이 있는지를 확인해야 한다.
    fs.rmdir(경로, 콜백) : 폴더를 지울 수 있다. 폴더 안에 파일이 있다면 에러가 발생하므로 먼저 내부 파일을 모두 지우고 호출해야 한다.

    fs 프로미스

     const fsPromises = require('fs').promises;
    
    /*
    fs.access()
        .then()
        .catch()
    */

    fs모듈로부터 promises 객체를 불러와 사용

    <응용 : 폴더 안의 모든 파일 삭제>

    Screen Shot 2020-01-03 at 11 21 20 AMScreen Shot 2020-01-03 at 11 24 56 AM

     

    Screen Shot 2020-01-03 at 11 23 17 AM

    events 모듈

    const EventEmitter = require('events'); // 생성자
    const myEvent = new EventEmitter(); // myEvent라는 커스텀 객체 생성
    
    myEvent.addListener('방문', () => {
      console.log('방문해주셔서 감사합니다.');
      // res.sendFile(html파일);
    });
    
    myEvent.on('종료', () => {
      console.log('안녕히가세요.');
    });
    
    myEvent.on('종료', () => {
      console.log('제발 좀 가세요.');
    }); // 종료 이벤트 발생 시 '안녕히가세요.', '제발 좀 가세요.' 둘 다 출력됨.
    
    myEvent.once('특별이벤트', () => {
      console.log('한 번만 실행됩니다.');
    });
    
    myEvent.emit('방문');
    myEvent.emit('특별이벤트');
    myEvent.emit('특별이벤트');
    
    myEvent.on('계속', () => {
      console.log('계속 리스닝');
    });
    myEvent.removeAllListeners('종료');
    myEvent.emit('종료');
    
    myEvent.on('event1', () => {
      console.log('이벤트~');
    });
    
    const callback = () => {
      console.log('이벤트!');
    };
    myEvent.on('event1', callback);
    myEvent.removeListener('event1', callback); // callback 리스너 제거
    myEvent.emit('event1');
    console.log(myEvent.listenerCount('event1'));

    Screen Shot 2020-01-03 at 7 48 07 PM

    이벤트 리스너 → 특정 이벤트가 발생했을 때 어떤 동작을 할지 정의하는 부분.

    • 사람들이 서버에 방문(이벤트)하면 HTML 파일 전달(콜백함수).
    • 여러 개를 달 수 있다.
    • onaddEventListener는 같은 기능을 하는 별명(alias)이다.

     

    emit(이벤트명) : 이벤트를 호출하는 메서드. 미리 등록해뒀던 이벤트 콜백이 실행된다.
    once(이벤트명, 콜백) : 한 번만 실행되는 이벤트. myEvent.emit('특별이벤트');를 연속 호출해도 콜백이 한 번만 실행된다.
    removeAllListeners(이벤트명) : 이벤트에 연결된 모든 이벤트 리스너를 제거한다.
    removeListener(이벤트명, 리스너) : 이벤트에 연결된 리스너를 하나씩 제거한다.
    off(이벤트명, 콜백) : removeListener와 기능이 같다.
    listenerCount(이벤트명) : 현재 리스너가 몇 개 연결되어 있는지 확인한다.

     

    on('data')와 on('end')의 경우, 겉으로는 이 이벤트들을 호출하는 코드가 없지만 내부적으로는 chunk를 전달할 때마다 data 이벤트를 emit하고 있다. 완료되었을 경우 end 이벤트를 emit한다.

    예외 처리하기

    예외 → 처리하지 못한 에러. 실행 중인 노드 프로세스를 멈추게 만든다.
    멀티 스레드 프로그램에서는 스레드 하나가 멈추면 그 일을 다른 스레드가 대신한다. 하지만 노드는 스레드가 하나뿐이므로 하나뿐인 스레드를 소중히 보호해야 한다.
    하나뿐인 스레드가 에러로 인해 멈춘다 = 전체 서버가 멈춘다.

    setInterval(() => {
      console.log('시작');
      throw new Error('서버를 고장내주마'); // 에러를 강제로 발생시킴.
    }, 1000);

    Screen Shot 2020-01-03 at 9 11 45 PM

    try catch로 에러를 잡으면 setInterval도 직접 멈추기 전까지 계속 실행된다. 에러가 발생할 것 같은 부분을 미리 try catch로 감싸준다.

    • try catch는 권장하지 않는다. 에러가 날 상황이면, 에러를 내보내지 않고 그것에 대한 처리를 해야한다. 에러를 낸 다음에 try catch로 잡으려 하지 말고 이런 상황이 아예 없도록 만드는 게 좋다.
    • async/await처럼 어쩔 수 없이 try catch를 써야하는 경우도 있다.
    • throw를 하는 경우, 반드시 try catch문으로 throw한 에러를 잡아주어야 한다.

     

    setInterval(() => {
        try{
        console.log('시작');
          throw new Error('서버를 고장내주마');
      }catch(err){
           console.error(err);
      }
    }, 1000);

    Screen Shot 2020-01-03 at 9 13 25 PM

    노드 자체에서 잡아주는 에러

    const fs = require('fs');
    
    setInterval(() => {
      fs.unlink('./abcdefg.js', (err) => {
        if(err){
          console.log('시작');
          console.log(err); // 에러 로그를 기록해두고 나중에 원인을 찾아 수정하면 된다.
          console.log('끝');
        }
      });
    },1000);

    Screen Shot 2020-01-03 at 9 29 37 PM

    fs.unlink()로 없는 파일을 지우고 있다. 에러가 발생하지만 노드 내장 모듈의 에러는 실행 중인 프로세스를 멈추지 않는다.

    예측이 불가능한 에러를 처리하는 방법 - uncaughtException 이벤트 리스너

    process.on('uncaughtException', (err) => {
      console.error('예기치 못한 에러', err);
    });
    
    setInterval(() => {
      throw new Error('서버를 고장내주마!');
    }, 1000);
    
    setTimeout(() => {
      console.log('실행됩니다.');
    }, 2000);

    Screen Shot 2020-01-03 at 9 48 01 PM

    process 객체에 uncaughtException 이벤트 리스너를 달아주면, 처리하지 못한 에러가 발생했을 때 이벤트 리스너가 실행되어 프로세스를 유지시켜 준다.
    모든 에러가 uncaughtException 이벤트 리스너의 콜백 인자에 기록될 수 있다.
    근본적인 에러의 원인을 해결하지는 않으므로, 단순히 에러 내용을 기록하는 용도로 사용하고 process.exit()로 프로세스를 종료하는 것이 좋다.

    https와 http2

    HTTP(Hypertext Transfer Protocol)

    • Hypertext인 HTML을 전송하기 위한 통신규약이다.

      하이퍼텍스트(Hypertext) 는 참조를 통해 독자가 한 문서에서 다른 문서로 즉시 접근할 수 있는 텍스트이다.
      출처: 위키백과

     

    암호화되지 않은 방법으로 데이터를 전송하므로 서버와 클라이언트가 주고 받는 메시지를 도청하는 것이 매우 쉽다.
    로그인을 위해 서버로 비밀번호를 전송하거나, 중요한 기밀 문서를 열람하는 과정에서 악의적인 감청이나 데이터의 변조 등이 일어날 수 있다.

    HTTPS(Hypertext Transfer Protocol over Secure Socket Layer)

    • 보안이 강화된 HTTP
    • S → Over Secure Socket Layer의 약자

     

    웹이 인터넷 위에서 돌아가는 서비스 중의 하나인 것처럼, HTTPS도 SSL(Secure Socket Layer) 프로토콜 위에서 돌아가는 프로토콜이다.
    SSL 위에서 HTTP가 동작하면 HTTPS가 되고, FTP가 동작하면 SFTP가 된다.

    download

    ^sslprotocol

    https 모듈

    웹 서버에 SSL 암호화 추가
    GET이나 POST 요청을 할 때 오고 가는 데이터를 암호화.
    중간에 다른 사람이 요청을 가로채더라도 내용을 확인할 수 없다.
    인증서 필요

    • createServer 메서드 첫 번째 인자에 인증서에 관련된 옵션 객체를 넣는다.
    • 인증서를 구입하면 pem이나 crt, 또는 key 확장자를 가진 파일들을 제공해준다.
    • 파일들을 fs.readFileSync 메서드로 읽어서 cert, key, ca 옵션에 알맞게 넣어준다.

     

    const https = require('https');
    const fs = require('fs');
    
    // 인증서를 발급받았다고 친다.
    https.createServer({
        cert: fs.readFileSync('도메인 인증서 경로'),
          key: fs.readFileSync('도메인 비밀키 경로'),
        ca: [
            fs.readFileSync('상위 인증서 경로'),
            fs.readFileSync('상위 인증서 경로'),
            fs.readFileSync('상위 인증서 경로'),
        ],
    },(req,res) => {
        res.end('https server');
    }).listen(443);

    http2 모듈

    SSL 암호화와 더불어 최신 HTTP 프로토콜인 http/2를 사용할 수 있게 해준다.
    http2는 https 기반으로 동작하므로 인증서가 필요하다.

    const https = require('https');
    const fs = require('fs');
    
    // 인증서를 발급받았다고 친다.
    http2.createSecureServer({
        cert: fs.readFileSync('도메인 인증서 경로'),
        key: fs.readFileSync('도메인 비밀키 경로'),
        ca: [
          fs.readFileSync('상위 인증서 경로'),
          fs.readFileSync('상위 인증서 경로'),
          fs.readFileSync('상위 인증서 경로'),
        ],
    },(req,res) => {
        res.end('https server');
    }).listen(443);

    익스프레스랑 호환 문제가 있다.
    spdy를 대신 사용.

    SPDY(스피디)는 웹 콘텐츠를 전송할 목적으로 구글이 개발한 비표준 개방형 네트워크 프로토콜이다. 웹 페이지 부하 레이턴시를 줄이고 웹 보안을 개선하는 목표 면에서 HTTP와 비슷하다.
    출처: 위키백과

    cluster로 멀티 프로세싱 하기

    cluster 모듈

    싱글 스레드인 노드가 CPU 코어를 모두 사용할 수 있게 해준다.

    • 코어가 8개인 서버가 있을 때, 노드는 보통 코어를 하나만 활용.

     

    포트를 공유하는 노드 프로세스를 여러 개 둘 수 있다.

    • 요청이 많이 들어왔을 때 병렬로 실행된 서버의 개수만큼 요청이 분산되게 할 수 있다. 서버에 무리가 덜 감.

     

    코어 하나당 노드 프로세스 하나가 돌아가게 할 수 있다.
    세션을 공유하지 못하는 등의 단점이 있다. Redis 등의 서버를 도입하여 해결할 수 있다.

     

    cluster에는 master 프로세스(관리자)와 worker 프로세스(일꾼)가 있다.
    cluster.fork() → 워커 프로세스 생성.

    Screen Shot 2020-01-02 at 7 50 35 PMScreen Shot 2020-01-02 at 7 51 44 PM

    새로고침(새로운 요청 보내기)을 할 때마다 현재 워커가 죽고, 새로운 워커가 생성된다.

    Screen Shot 2020-01-02 at 7 51 11 PM

    실무에서는 pm2 등의 모듈로 cluster 기능 사용.

    Javascript

    const & let

    if(true){
      const x = 3;
    }
    console.log(x); // 3
    /*-----------------------*/
    //const와 let은 블록 안에서만 선언할 수 있다.
    if(true){
      const y = 3;
    }
    console.log(y); //y is not defined 에러 발생
    //const는 값 재할당(=) 불가능
    //let은 값 재할당(=) 가능
    
    const o = {a:1, b:2, c:3};
    o = [1,2,3] //Assignment to constant variable 에러 발생
    o.a = 3;
    o.b = 5;
    //o -> {a:3,b:5,c:3}
    //const에 객체가 할당된 경우, 객체 내부 속성은 바꿀 수 있다.
    const h = [1,2,3,4,5]
    h[0]=true
    h[1]=false
    //h -> [true,false,3,4,5] 배열도 객체다!
    h = 123 //Assignment to constant variable 에러 발생

    const는 메모리 주소에 대한 상수이다.

    백틱(`)

    const laugh = 'ㅎㅎㅎ'
    const g = `작은따옴표(')입니다. ${laugh}`

    객체 리터럴

    const obj = {
      a: 1,
      b: 2,
    };
    //ES6 이전
    var sayNode = function(){
      console.log('Node');
    };
    var es = 'ES';
    var oldObject = {
      sayJS: function(){
        console.log('JS');
      },
      sayNode: sayNode,
    };
    oldObject[es+6] = 'Fantastic'; // 동적 속성
    
    //ES6
    const newObject = {
      sayJS(){
        console.log('JS');
      },
      sayNode,
      [es+6]: 'Fantastic',
    };
    
    // sayJS = function(){} -> sayJS(){}
    // 키랑 값(변수)이 같은 경우 {data:data, hello:hello} -> {data, hello}

    화살표 함수

    //함수 선언문
    function add1(x,y){
      return x+y;
    }
    //함수 표현식
    var add2 = function(x,y){
      return x+y;
    };
    //화살표 함수
    const add3 = (x,y) => {
      return x+y;
    };
    //화살표 함수2
    const add4 = (x,y) => x+y; 

    function과 화살표 함수는 내부 this 동작이 차이난다.

    var relationship1 = {
      name: 'zero',
      friends: ['nero','hero','xero'],
      logFriends: function(){
        var that = this; // relationship1을 가리키는 this를 that에 저장
        this.friends.forEach(function(friend){
          console.log(that.name, friend); //this는 global객체 가리킴
        });
      },
    };
    relationship1.logFriends();
    /* zero nero 
         zero hero
         zero xero */
    
    const relationship2 = {
      name: 'zero',
      friend = ['nero', 'hero', 'xero'],
      logFriends(){
        this.friends.forEach(friend => { 
          // 화살표 함수는 함수 내부의 this를 외부의 this와 같게 만들어준다.
             // 화살표 함수의 this = 자신을 감싼 정적 범위와 동일
          console.log(this.name, friend);
        });
      },
    };

    forEach() 메서드

    arr.forEach(callback(currentvalue[, index[, array]])[, thisArg]);

    • 주어진 callback을 배열에 있는 각 요소에 대해 오름차순으로 한 번씩 실행.
    • thisArg → callback을 실행할 때 this로 사용할 값.
    • thisArg를 제공하지 않으면 undefined를 사용하며, 최종 this값은 함수의 this를 결정하는 평소 규칙을 따른다.

    비구조화 할당(Destructuring)

    const candyMachine = {
      status:{
        name: 'node',
        count: 5,
      },
      getCandy(){
        this.status.count--;
        return this.status.count;
      }
    };
    
    /* const status = candyMachine.status;
       const getCandy = candyMachine.getCandy; */
    
    const { status, getCandy } = candyMachine;
    
    getCandy(); // undefined. 비구조화 할당 시 this가 의도와 다르게 동작할 수 있다.
    candyMachine.getCandy(); // 4
    getCandy.call(candyMachine); // 3. call -> this를 바꿔주는 메서드.
    

    const a = 객체.a;
    const b = 객체.b;
    const { a, b } = 객체;
    로 바꿀 수 있다.

    const { Router } = require('express');
    // require('express')라는 객체에서 Router라는 속성 값을 변수로 꺼내온다.

    Screen Shot 2019-12-30 at 10 05 12 AM

    • const 변수 = require('파일 경로');
    // 예전
    const variable = require('./var');
    
    console.log(variable.odd);
    console.log(variable.even);
    
    // ES2015(비구조화 할당)
    const {odd, even} = require('./var');
    
    console.log(odd);
    console.log(even);

    모듈이 될 파일은 module.exports = 값; 을 마지막에 붙여준다.

    Screen Shot 2019-12-30 at 10 44 47 AM

    함수를 내보낼 수도 있다.

     

    모듈은 여러 번 재사용될 수 있다.

    배열에서 되는 비구조화 할당

    var array = ['nodejs', {}, 10, true];
    
    var node = array[0];
    var obj = array[1];
    var bool = array[array.length - 1];
    
    //ES6
    const arr = ['nodejs', {}, 10, true];
    const [node, obj, , bool] = arr;

    const a = array[0];
    const b = array[1];
    const [a,b] = array;
    로 바꿀 수 있다.

    rest 문법과 Q&A

    const arr = ['nodejs', {}, 10, true];
    const [node, obj, ...bool] = arr;
    //bool -> [10, true]
    
    //es6 이전
    function o(){
      console.log(arguments);
    }
    o(1,2,3,4,5); // Arguemnts(5) [1,2,3,4,5]
    
    //es6 - 더 이상 arguments 사용하지 않는다.
    const p = (...rest) => console.log(rest);
    p(5,6,7,8,9); // (5) [5,6,7,8,9]

    ...변수 는 rest로 여러 개의 변수를 모아서 배열로 만든다.

    객체 참조란?

    const → 참조에 대한 상수

    const x = { a:1, b:2 };
    let y = x; 
    // y는 x를 참조한다. x값들이 저장된 메모리의 위치를 찍고있다.
    // x가 바뀌면 y도 함께 바뀐다.
    x.a = 3;
    y.a; // 3.

    const x 는 껍데기({})를 찍고 있는 셈이므로 바꿀 수 없지만, 그 안에 있는 값들은 바꿀 수 있다.

    콜백과 프로미스(Promise) 비교

    콜백

    //데이터베이스에 Users라는 테이블이 있고, 거기서 'zero'라는 한 사람을 찾아온다고 가정.
    Users.findOne('zero', (err, user) => { // 네트워크를 통해서 데이터베이스에 가서 쿼리를 수행하는데까지 시간이 많이 소요되기 때문에, 논 블로킹 방식으로 먼저 요청만 보내놓고 다음 코드를 실행한다.
      if(error){
        return console.error(error);
      }
      console.log(user);
    });
    console.log('다 찾았니?'); // console.log(user); 보다 먼저 실행.
    // 콜백 쓰는 이유? -> 이 코드가 논 블로킹으로 작동하기 때문.

    console.log('다 찾았니?'); 가 먼저 실행되고, 찾았으면 (err, user) => {...} 콜백이 실행된다.
    단점 → 순서가 애매하다. 콜백의 경우, 코드를 신나게 열심히 썼는데, 실행 순서를 못찾아서 순서가 어떻게 흘러가는지 파악이 어려운 경우가 있다.

    // 예시 : 콜백 안에 콜백
    // 비동기이기 때문에 한 번 비동기로 들어가버리면, 그 다음부터는 모든 코드를 콜백 안에 작성해야한다.
    Users.findOne('zero', (err, user) =>{
        if(err){
          return console.error(err);
        }
        console.log(user);
        Users.update('zero', 'nero', (err, updatedUser) => {
          if(err){
              return console.error(err);
            }
          console.log(updatedUser);
          Users.remove('nero', (err, removedUser) => {
            if(err){
                  return console.error(err);
                }
            console.log(removedUser);
          });
        });
      });
    console.log('다 찾았니?');

    콜백지옥 발생.

    예전에는 콜백지옥을 막기 위해 콜백을 변수로 뺐다. 가독성이 떨어진다.

    const afterRemove = (err, removedUser) => {
        console.log(removedUser);
    }
    
    const afterUpdate = (err, updatedUser) => {
        console.log(updatedUser);
          Users.remove('nero', afterRemove);
    }
    
    Users.findOne('zero', (err, user) => {
        if (err) {
            return console.error(err);
        }
        console.log(user);
        Users.update('zero', 'nero', afterUpdate);
    });
    console.log('다 찾았니?');

    프로미스

    // Promise를 지원하는 메서드는 내부적으로 지원해주기 때문에 사용 가능하다. 내부적으로 Promise를 return하기 때문에 then과 catch를 붙일 수 있다.
    /*
    const Users = {
      findOne(){
        return new Promise((resolve, reject) => {
          if ('사용자를 찾았으면') {
            resolve('사용자');
          } else {
            reject('못 찾았어요');
          }
        });
      },
      update() {return new Promise(...)},
      remove(),
    }; 
    */ 
    Users.findOne('zero')
        .then((user) => {
          console.log(user);
          return Users.update('zero', 'nero');
        })
        .then((updatedUser) => {
          console.log(updatedUser);
          return Users.remove('nero');
        })
        .then((removedUser) => {
          console.log(removedUSer);
        })
        .catch((err) => {
          console.error(err);
        });
    console.log('다 찾았니?');

    image-20191229221330271

    Promise 생성자

    const plus = new Promise((resolve, reject) => {
        const a = 1;
        const b = 2;
        if (a + b > 2) { // a+b = 3
          resolve(a+b);
        }else{
          reject(a+b);
        }
    });
    /* resolve -> 성공했을 때 
       reject -> 실패했을 때
       위 코드는 성공했으므로 resolve가 실행된다.
    */
    plus
     .then((success) => { // 성공(resolve)한 경우 실행
        console.log(success);
     })
     .catch((fail) => { // 실패(reject)한 경우 실행
        console.error(fail);
     })

    Promise를 return하면 resolve나 reject되어 then 또는 catch가 실행된다.
    then에 리턴 값이 있으면 다음 then으로 넘어간다.

    Promise.resolve(성공 메시지) / Promise.reject(실패 메시지)

    const promise1 = new Promise((resolve, reject) => {
        resolve('성공');
    });
    const promise2 = new Promise((resolve, reject) => {
        reject('성공');
    });
    // 무조건 성공하거나 실패하는 Promise는 아래처럼 축약할 수 있다.
    const successPromise = Promise.resolve('성공')
        .then();
    const failurePromise = Promise.reject('실패');
        .catch();

    Promise.all로 여러 프로미스를 동시에 실행할 수 있다. 하나라도 실패하면 catch가 실행된다.

    Promise.all([Users.findOne(), Users.remove(), Users.update()])
        .then((results) => {})
        .catch((error) => {});

    Promise는 결과값을 가지고 있지만 .then이나 .catch를 붙이기 전까지 반환하지 않는 것.

    async/await

    Users.findOne('zero')
        .then((user) => {
          console.log(user);
          return Users.update('zero', 'nero');
        })
        .then((updatedUser) => {
          console.log(updatedUser);
          return Users.remove('nero');
        })
        .then((removedUser) => {
          console.log(removedUSer);
        })
        .catch((err) => {
          console.error(err);
        });
    console.log('다 찾았니?');
    
    async func() => {
      try{
          const user = await Users.findOne('zero');
          console.log('다 찾았니?');
          console.log(user);
          const updatedUser = await Users.update('zero', 'nero');
          console.log(updatedUser);
          const removedUser = await Users.remove('nero');
          console.log(removedUser);
      } catch (err) {
        console.error(err);
      }   
    }
    func();

    async () => {
    const 변수 = await 값
    }

    • async / await 도 Promise 기반
    • 동기식으로 보이기 때문에 코드 순서와 실행 순서가 같다.
    • 에러 처리를 위해 await를 try catch문으로 감싼다.
    • try {} catch (error) {}

Designed by Tistory.