ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Node.js 스터디 2주차
    BackEnd/Node.js 2022. 1. 25. 06:31

    이번 주차에는 Node.js 교과서의 3장 노드 기능 알아보기를 끝내기로 했다. 3.6절 파일 시스템 접근하기 ~ 3.8절 예외 처리하기까지이다. 


     3.6 파일 시스템 접근하기

    fs 모듈 : 노드에서 파일 시스템에 접근하기 위해서 사용하는 모듈로 파일 생성, 삭제 및 읽기 쓰기, 폴더 생성 삭제도 가능하다. '파일 시스템'이란 컴퓨터에서 파일 및 자료를 쉽게 발견하고 접근할 수 있도록 보관 및 조직하는 체계를 일컫는 말이다. 

     

    - 가장 간단하게 파일을 읽는 방법이다. 

    fs 모듈을 불러온 뒤 파일 경로 지정해주는 가장 간단한 방법이다. 이때 경로는 노드 명령어 실행하는 콘솔 기준 경로로 써줘야한다. 위 코드의 콜백함수를 프로미스 형식으로 바꿔서 아래처럼 사용할 수 있다. (실행 결과는 동일) 

     

    const fs = require('fs').promises;
    
    fs.readFile('./readme.txt')
        .then((data) => {
            console.log(data);
            console.log(data.toString());
        })
        .catch((err) => {
            console.log(err);
        })

     


    - 파일 만들기

    writeFile메서드의 인수에 파일 경로와 내용을 입력하면 writeme.txt파일이 만들어지면서 내용이 작성된다. 

     


     

    - 동기 메서드와 비동기 메서드

    대체로 노드는 비동기 방식의 처리가 많지만, fs모듈의 경우 동기 방식으로 사용 가능한 메서드들이 많이 있다. 

    실행할 때마다 1, 2, 3의 순서가 달라진다. 

    비동기 메서드들의 경우 파일을 읽으라고만 요청하고 다음 작업으로 넘어간다. 항상 콘솔 출력에는 시작-끝이 먼저 찍히고, 1번 2번 3번은 순서가 계속 바뀐다. '시작'을 먼저 출력하고, 1, 2, 3순서로 요청만 보내고 다음 작업인 '끝'을 출력한다. 읽기가 완료된 경우에 백그라운드가 메인 스레드에 알려서 1, 2, 3을 완료된 순서대로 콜백함수를 실행한다. 

     

    이 방법의 장점은 다량의 I/O작업이 들어온다고 해도 백그라운드에 요청을 위임하고 완료된 후에 그때그때 콜백함수를 처리하면 되기 때문에 요청을 더 받을 수 있는 장점이 있다. (비동기 - 논 블로킹 방식)

     

    만약에 순서대로 실행하고 싶다면 readFileSync라는 메서드를 사용한다. 

    보통 ~sync로 끝나는 메서드들이 동기 메서드들이다. 하지만 이 방법에는 요청이 수백개 이상 들어올 경우 매우 느려진다는 단점이 있다. 백그라운드의 작업을 기다리기 때문이다. 메인 스레드는 백그라운드의 작업이 끝날 때까지 다른 요청을 처리할 수 없기 때문에 비효율적이다. 

     

    비동기 메서드를 사용해서 순서대로 처리하고 싶다면 아래와 같은 방법도 있다. 

    promise방식으로 좀 더 코드를 간결하게 만들면 아래와 같다. 

    const fs = require('fs').promises;
    console.log('시작');
    fs.readFile('./readme2.txt')
        .then((data) => {
            console.log('1번', data.toString());
            return fs.readFile('./readme2.txt');
        })
        .then((data) => {
            console.log('2번', data.toString());
            return fs.readFile('./readme2.txt');
        })
        .then((data) => {
            console.log('3번', data.toString());
            console.log('끝');
        })
        .catch((err) => {
            console.error(err);
        });

     


     

    넘어가기 전에 동기, 비동기, 블로킹, 논 블로킹 정리

    동기 vs 비동기 : 백그라운드 작업 완료 확인을 하는가 안하는가

    블로킹 vs 논블로킹 : 함수가 바로 return되는가? 

     

    동기 - 블로킹 방식 : 백그라운드 작업 완료 여부를 확인함, 호출 함수를 바로 return하지 않고 백그라운드 작업 끝나야만 return시킨다. 

     

    비동기 - 논블로킹 방식 : 백그라운드 작업 완료 여부 확인하지 않고 호출 함수를 바로 return시키고 다음 작업으로 넘어간다. 백그라운드 작업이 끝나서 알림이 오면 그 때 처리한다. 

     


     

    Buffer & Stream 

    - Buffer : 파일을 읽을 때, 파일 크기만큼의 공간을 마련해두고 파일 데이터를 메모리에 저장한 뒤 사용자가 조작할 수 있다. 이 때 메모리에 저장된 데이터가 버퍼이다. 

     

    버퍼 방식으로 처리할 경우 파일 용량 크기만큼의 버퍼를 메모리에 만들어야 하는 단점이 있다. 그리고 버퍼에 모든 내용을 다 쓰고 나서야 다음으로 넘어가기 때문에 파일 조작을 연달아 할 경우 매번 파일 전체의 용량을 버퍼로 처리해야 한다. 

     

    -> 그래서 이를 보완하기 위해 버퍼의 크기를 작게 만든 다음 여러번 나눠 보내는 방식이 등장했다. 적은 메모리 용량으로 큰 파일을 전송할 수 있다. 이 방식을 편리하게 만든 것이 'Stream'이다. 

    콘솔창을 보면 여러 번 나눠져서 읽는 것을 확인할 수 있다. 

     

    - 파일 작성하기

    기존의 파일을 읽고 그 스트림을 전달받아서 createWriteStream으로 쓸 수 도 있다. (stream끼리 연결, 파이핑 한다고 표현한다. ) 

     

    -> 읽기 스트림과 쓰기 스트림을 연결하여 결과적으로 두 파일의 내용이 동일해진 것을 확인할 수 있다. pipe는 스트림 사이를 여러번 연결할 수 있다. 

    const zlib = require('zlib'); 
    const fs = require('fs');
    
    const readStream = fs.createReadStream('./readme4.txt');
    const zlibStream = zlib.createGzip();// zlib 모듈의 createGzip()메서드로 스트림 파이핑. 
    const writeStream = fs.createWriteStream('./readme4.txt.gz'); //.gz확장자, gzip방식으로 압축되었음. 
    readStream.pipe(zlibStream).pipe(writeStream);

    -> zlib이라는 모듈을 이용해서 압축과 스트림 연결을 위 코드처럼 함께 할 수도 있다. 

     


     

    - 대용량 파일 다루기 

    for문을 돌리면서 용량 큰 파일을 작성하는 방법이다. 실행시간이 꽤 걸린다.. (노트북 팬도 요란하게 돌아갔다...ㅋㅋㅋ)

    const fs = require('fs');
    const file = fs.createWriteStream('./big.txt');
    
    for(let i = 0; i <= 10000000; i++) {
        file.write('안녕하세요. 엄청나게 큰 파일을 만들어 볼 것입니다. 각오 단단히 하세요! \n');
    }
    file.end();

    오른쪽 캡처 이미지 보면 알겠지만... 엄청 큰 파일이다. 

    readFile메서드로 big2.txt파일로 복사한다. 

     

    메모리 용량이 18mb -> 1G가 넘어간다. 파일 복사를 위해서 메모리 위에 big.txt를 다 올려두었기 때문이다. 

    Stream을 사용해서 big3.txt에도 복사해보았다. 

     

    용량이 Buffer때보다 줄어든 것을 확인할 수 있다. 

     


     

    - 그 외 fs 메서드들 (파일 및 폴더 생성 & 삭제)

    const fs = require('fs').promises;
    const constants = require('fs').constants;
    
    fs.access('./folder', constants.F_OK | constants.W_OK | constants.R_OK) 
    // 폴더나 파일에 접근 가능한지 체크, F_OK : 존재 여부, W_OK : 쓰기 권한, R_OK : 읽기 권한
        .then(() => {
            return Promise.reject('이미 폴더 있음');
        })
        .catch((err) => {
            if (err.code === 'ENOENT') { // 권한이 없을 경우의 에러코드 
                console.log('폴더 없음');
                return fs.mkdir('./folder'); // mkdir : 폴더 생성. 이미 있으면 에러 나니까 처음에 access로 존재 여부 확인하자! 
            }
            return Promise.reject(err);
        })
        .then(() => {
            console.log('폴더 만들기 성공');
            return fs.open('./folder/file.js', 'w'); // 파일의 아이디, 2번째 인수는 권한
            // 'a' : 기존 파일에 추가, 'w' : 쓰기, 없을 때 새로 생성!, 'r' : 읽기
        })
        .then((fd) => {
            console.log('빈 파일 만들기 성공', fd);
            return fs.rename('./folder/file.js', './folder/newfile.js'); // 파일 이름 변경 
        })
        .then(() => {
            console.log('이름 바꾸기 성공');
        })
        .catch((err) => {
            console.error(err);
        })

    처음 실행할 때는 폴더 생성 - 파일 생성 - 이름 바꾸기 순으로 간다. 

    2번째 실행하면 이미 존재하는 폴더라고 콘솔에 찍히는 것을 확인할 수 있다. 

    &amp;amp;nbsp;


    위에서 만든 폴더와 파일을 삭제해보자. 

    const fs = require('fs').promises;
    
    fs.readdir('./folder') // 폴더 안 내용물 확인, 배열 형태로 내부 파일, 폴더 명 나온다. 
        .then((dir) => {
            console.log('폴더 내용 확인', dir);
            return fs.unlink('./folder/newFile.js'); // 파일 지우기
        })
        .then(() => {
            console.log('파일 삭제 성공');
            return fs.rmdir('./folder'); // 폴더 삭제. 내부가 빈 폴더만 삭제 가능하다
        })
        .then(() => {
            console.log('폴더 삭제 성공');
        })
        .catch((err) => {
            console.error(err);
        });

    위 콘솔 상태에서 한번더 fsDelete를 실행하면 ENOENT : 존재하지 않는 파일 에러가 발생한다. 


    Stream의 pipe없이 복사하는 방법도 있다. (노드 8.5버전 이후)

    const fs = require('fs').promises;
    fs.copyFile('readme4.txt', 'writeme4.txt')
        .then(() => {
            console.log('복사 완료');
        })
        .catch((error) => {
            console.error(error);
        });

     

    파일의 수정사항을 감시하는 방법도 있다. 

    - change : 내용물이 바뀐 경우. node를 실행시키고, 파일 내용물을 계속 바꾸면 콘솔처럼 찍힌다. 

    - rename : 파일 명을 바꾸거나 삭제한 경우이다. rename이후에는 watch가 수행되지 않음. 

     


     

    'Node.js 의 스레드' 

    event loop를 실행하는 스레드는 하나이기 때문에 기본적으로 싱글스레드 논블로킹이라고 이야기한다. 하지만 fs, crypto, zlib 등의 cpu연관 작업들(파일 입출력 등)은 미리만들어둔 스레드에서 사용한다. 

     

    const crypto = require('crypto');
    const pass = 'pass';
    const salt = 'salt';
    const start = Date.now();
    
    crypto.pbkdf2(pass, salt, 1000000, 128, 'sha512', () => {
        console.log('1 : ', Date.now() - start);
    })
    
    crypto.pbkdf2(pass, salt, 1000000, 128, 'sha512', () => {
        console.log('2 : ', Date.now() - start);
    })
    
    crypto.pbkdf2(pass, salt, 1000000, 128, 'sha512', () => {
        console.log('3 : ', Date.now() - start);
    })
    
    crypto.pbkdf2(pass, salt, 1000000, 128, 'sha512', () => {
        console.log('4 : ', Date.now() - start);
    })
    
    crypto.pbkdf2(pass, salt, 1000000, 128, 'sha512', () => {
        console.log('5 : ', Date.now() - start);
    })
    
    crypto.pbkdf2(pass, salt, 1000000, 128, 'sha512', () => {
        console.log('6 : ', Date.now() - start);
    })
    
    crypto.pbkdf2(pass, salt, 1000000, 128, 'sha512', () => {
        console.log('7 : ', Date.now() - start);
    })
    
    crypto.pbkdf2(pass, salt, 1000000, 128, 'sha512', () => {
        console.log('8 : ', Date.now() - start);
    })

     

    이 출력결과는 1234가 한 그룹으로 먼저 실행되고 (완료 되는 순서는 매번 다르다) 그 후에 5~8이 실행된다. 노드의 기본 스레드풀의 스레드 개수는 4개이기 때문이다. 

    process.env.UV_THREADPOOL_SIZE=8;

     

    기본적으로 4개지만, 위 코드처럼 스레드 개수를 조절할 수도 있다. 

     


     

    3.7 이벤트 이해하기

    - 직접 사용자가 이벤트를 만드는 예시이다. 

    const EventEmitter = require('events');
    const myEvent = new EventEmitter();
    
    myEvent.addListener('event1', () => { //on과 동일! 
        console.log('이벤트 1 ');
    });
    myEvent.on('event2', () => { // on(이벤트명, 콜백 함수) : 이벤트 이름과 발생시 콜백 연결. (= 이벤트 리스닝)
        console.log('이벤트 2 ');
    });
    myEvent.on('event2', () => { // 여러개 다는 것도 가능
        console.log('이벤트 2 추가 ');
    })
    myEvent.once('event3', () => { //한 번만 실행하게 한다. 
        console.log('이벤트 3');
    })
    
    myEvent.emit('event1'); // emit : 이벤트를 호출한다. 
    myEvent.emit('event2');
    
    myEvent.emit('event3');
    myEvent.emit('event3');
    
    myEvent.on('event4', () => {
        console.log('이벤트 4');
    });
    myEvent.removeAllListeners('event4'); //모든 이벤트 리스너 제거, 따라서 아래 emit('event4')는 실행 X
    myEvent.emit('event4');
    
    const listener = () => {
        console.log('이벤트 5');
    };
    myEvent.on('event5', listener);
    myEvent.removeListener('event5', listener); // 이벤트에 연결된 리스너 하나 제거, 매개변수에 리스너 이름 넣어야함. 
    myEvent.emit('event5');
    
    console.log(myEvent.listenerCount('event2')); //몇 개의 리스너가 연결되어 있는가.

    실행결과를 보면 이벤트 4와 이벤트 5는 emit되기 전 리스너가 remove되어서 실행이 안 된것을 확인할 수 있다. 

     


     

    3.8절 예외 처리하기

    노드의 메인스레드는 1개이므로 에러가 기록되더라도 작업은 계속 되게끔 에러처리를 해줘야 한다. 

    throw Error로 에러를 직접 발생시킨 코드이다. try - catch문으로 감싸져 있어서 종료되지 않는다. 

     

    아래 코드는 존재하지 않는 파일을 지우려할 때 발생하는 에러를 노드 내에서 잡아주는 코드이다. 

    에러 로그만 위 콘솔처럼 기록해두고 프로세스 자체는 멈추지 않는 것을 확인할 수 있다. 

    위 코드를 프로미스 형태로 고치면 아래와 같다. 

    const fs = require('fs').promises;
    
    setInterval(() => {
        fs.unlink('./abcdefg.js')
    }, 1000);

     

    그 외 예측 불가능한 에러를 잡는 uncaughtException도 있다. 

    * uncaughtException에서 복구 작업을 위한 코드를 넣어도 되는가? 

    공식문서에 따르면 최후의 수단으로 사용하라고 명시가 되어있다. uncaughtException의 다음 동작이 제대로 작동할지 확신 불가능하기 때문이다. 에러 로그 기록용으로만 사용하는것이 권장되는 듯 하다. 

     

    < 기타 자주 발생하는 에러들 >

    - node : command not found 

    환경 변수 제대로 설정되지 않음

     

    - ReferenceError : 모듈 is not defined 

    해당 모듈 require안함

     

    - Error : Cannot find module 모듈명

    require은 했지만 npm i 명령어로 설치하지 않음. 

     

    - Error : Can't see headers after they are sent

    응답을 2번 이상 보냄. 요청에 대한 응답은 한 번만 보내야 한다. 

     

    - FATAL ERROR : CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

    코드 실행시 메모리가 부족하여 스크립트가 작동하지 않는 경우. 보통 코드가 잘못되었을 확률이 높다. 

    코드가 정상이면 직접 노드 메모리를 늘린다. 

     

    - UnhandledPromiseRejectionWarning : Unhandled promise rejection 

    프로미스 사용시 catch메서드를 붙이지 않음. 

     

    - EADDRINUSE 포트번호

    해당 포트 번호에 이미 다른 프로세스가 연결이 되어있음

     

    - EACCES || EPERM 

    작업 수행시의 권한 충분하지 않음. 

     

    - EJSONPARSE 

    JSON파일에 문법 오류가 있음. (package.json등)

     

    - ECONNREFUSED 

    요청을 보냈으나 연결이 성립하지 않음 

     

    - ETARGET

    package.json의 패키지 버전이 존재하지 않음. 

     

    -ETIMEOUT 

    요청을 보냈으나 응답이 일정시간 내에 오지 않음(서버 상태 점검 요망)

     

    -ENOENT : no such file or directory 

    폴더나 파일이 존재하지 않음. 이름 확인해볼 것. 

    'BackEnd > Node.js' 카테고리의 다른 글

    Node.js 스터디 4주차  (0) 2022.02.07
    Node.js 스터디 3주차  (0) 2022.02.05
    Node.js 스터디 1주차  (0) 2022.01.18
    Node.js에서 로그인 기능 구현하기(+github 위키)  (0) 2021.08.28
    AWS db와 node.js 연결하기  (0) 2021.08.14
Designed by Tistory.