Node.js 교과서 : 3. 노드 기능 (2)

파일 시스템 접근

  • fs 모듈 : 파일 시스템에 접근하는 모듈
const fs = require('fs');

// read a file
fs.readFile('./Script/Chapter3/node3_5/readme.txt', (err, data) => {
    if (err) {
        throw err;
    }
    console.log(data); // 버퍼(Buffer)
    console.log(data.toString()); // 버퍼 문자열 반환
});
  • 콜백 형식의 모듈이기 때문에 실무에서 사용하기 위해서는 프로미스 형식으로 작성
  • readFile(파일 경로, 인코딩, 파일 읽기 모드) : 파일을 읽어옴
const fs = require('fs').promises;

// read a file, promises
fs.readFile('./Script/Chapter3/node3_5/readme.txt')
    .then((data) => {
        console.log(data);
        console.log(data.toString());
    })
    .catch((err) => {
        console.error(err);
    });
  • writeFile(파일 경로, 인코딩) : 파일을 씀
const fs = require('fs').promises;

// write a file
fs.writeFile('./Script/Chapter3/node3_5/writeme.txt', "글을 작성했습니다.")
    .then(() => { // 작성된 파일 읽기
        return fs.readFile('./Script/Chapter3/node3_5/writeme.txt');
    })
    .then((data) => {
        console.log(data.toString());
    })
    .catch((err) => {
        console.error(err);
    });

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

  • fs 모듈이 동기 방식으로 처리 하는 메서드를 많이 가지고 있음
const fs = require('fs');

console.log('시작');

// 비동기 - 논블로킹 방식 : 순서가 보장되지 않음

fs.readFile('./Script/Chapter3/node3_5/readme.txt', (err, data) => {
    if (err) {
        throw err;
    }
    console.log('1번', data.toString());
});
fs.readFile('./Script/Chapter3/node3_5/readme.txt', (err, data) => {
    if (err) {
        throw err;
    }
    console.log('2번', data.toString());
});
fs.readFile('./Script/Chapter3/node3_5/readme.txt', (err, data) => {
    if (err) {
        throw err;
    }
    console.log('3번', data.toString());
});
console.log('끝');
시작
끝
1번 안녕하세요.
3번 안녕하세요.
2번 안녕하세요.
const fs = require('fs');

// 비동기 - 논블로킹 방식, then 안에 작성해 순서 설정

console.log('시작');

fs.readFile('./Script/Chapter3/node3_5/readme.txt', (err, data) => {
    if (err) {
        throw err;
    }
    console.log('1번', data.toString());
    fs.readFile('./Script/Chapter3/node3_5/readme.txt', (err, data) => {
        if (err) {
            throw err;
        }
        console.log('2번', data.toString());
        fs.readFile('./Script/Chapter3/node3_5/readme.txt', (err, data) => {
            if (err) {
                throw err;
            }
            console.log('3번', data.toString());
            console.log('끝');
        });
    });
});
const fs = require('fs');

// 콜백 지옥에 대한 Promise async/await 해결
console.log('시작');

fs.readFile('./Script/Chapter3/node3_5/readme.txt')
    .then((data) => {
        console.log('1번', data.toString());
        return fs.readFile('./Script/Chapter3/node3_5/readme.txt');
    })
    .then((data) => {
        console.log('2번', data.toString());
        return fs.readFile('./Script/Chapter3/node3_5/readme.txt');
    })
    .then((data) => {
        console.log('3번', data.toString());
        console.log('끝');
    }).catch((err) => {
        console.error(err);
    });
  • readFileSync 메서드를 사용할 시 많은 요청이 들어올 때 성능에 문제가 생길 수 있음
  • 동기 메서드들은 이름 뒤에 Sync가 붙어있음(writeFileSync, readFileSync 등)
const fs = require('fs');

// 동기-블로킹 방식, 순서대로 출력
console.log('시작');
let data = fs.readFileSync('./Script/Chapter3/node3_5/readme.txt');
console.log('1번', data.toString());
data = fs.readFileSync('./Script/Chapter3/node3_5/readme.txt');
console.log('2번', data.toString());
data = fs.readFileSync('./Script/Chapter3/node3_5/readme.txt');
console.log('3번', data.toString());
console.log('끝');
버퍼(Buffer)와 스트림(Stream)

  • 노드에서 파일을 읽을 때 메모리에 파일 크기만큼 Buffer에 저장하고 데이터 전송
    • 메모리와 시간을 효율적으로 사용할 수 있음
  • from(문자열) : 문자열을 버퍼로 변경
  • toString(인코딩 방식) : 버퍼를 다시 문자열로 변경, base64나 hex를 인수로 넣으면 해당 인코딩으로 변경 가능
  • copy(버퍼 이름) : 버퍼의 내용을 버퍼 이름에 복사
  • concat(배열) : 배열 안에 든 버퍼들을 하나로 합침
  • alloc(바이트) : 빈 버퍼 생성, 바이트를 인수로 넣으면 해당 크기의 버퍼 생성
  • allocUnsafe(바이트) : 빈 공간으로 초기화 과정 없이 버퍼 생성, 해당 공간이 사용 중인 경우 사용 중인 값이 출력
const buffer = Buffer.from('저를 버퍼로 바꾸십시오.');
console.log('from() : ', buffer); // 유니코드 형태로 출력
console.log('length : ', buffer.length); // 버퍼 길이
console.log('toString() : ', buffer.toString());

const array = [Buffer.from('띄엄 '), Buffer.from('띄엄 '), Buffer.from('띄어쓰기')];
const buffer2 = Buffer.concat(array);
console.log('concat() : ', buffer2.toString());

const buffer3 = Buffer.alloc(5);
console.log('alloc() : ', buffer3);
from() :  <Buffer ec a0 80 eb a5 bc 20 eb b2 84 ed 8d bc eb a1 9c 20 eb b0 94 ea be b8 ec 8b ad ec 8b 9c ec 98 a4 2e>
length :  33
toString() :  저를 버퍼로 바꾸십시오.
concat() :  띄엄 띄엄 띄어쓰기
alloc() :  <Buffer 00 00 00 00 00>
  • Stream : 버퍼의 크기를 작게 만든 후 여러 번 나눠 보내는 것
    • readFile 방식 사용 시 용량이 커질 경우 메모리 사용에 비효율적이기 때문에 스트림 방식을 사용
  • createReadStream(읽을 파일 경로, 옵션) : 읽기 스트림 생성
    • highWaterMark : 버퍼의 크기(바이트 단위)를 정할 수 있는 옵션(기본값 : 64KB)
  • readStream : 스트림 정보를 읽으며, 이벤트 리스너를 붙여서 사용(data, end, error)
const fs = require('fs');

const readStream = fs.createReadStream('./Script/Chapter3/node3_5/readme2.txt', {
    highWaterMark: 16
}); // 뒤로 .on을 이어 작성할 수 있음
const data = [];

// 파일 읽기가 시작되면 data 이벤트 리스너
readStream.on('data', (chunk) => {
    data.push(chunk);
    console.log('data : ', chunk, chunk.length);
});

// 파일을 다 읽으면 end 이벤트 리스너
readStream.on('end', () => {
    console.log('end : ', Buffer.concat(data).toString()); // 버퍼들을 모두 합쳐 문자열로 출력
});

readStream.on('error', (err) => {
    console.log('error : ', err);
});
data :  <Buffer ec 95 88 eb 85 95 20 eb 82 98 eb 8a 94 20 eb b0> 16
data :  <Buffer 95 eb 8f 99 ed 9b 88 ec 9d b4 eb 9d bc ea b3 a0> 16
data :  <Buffer 20 ed 95 9c eb 8b a4 2e 20 eb a7 8c eb 82 98 ec> 16
data :  <Buffer 84 9c 20 eb b0 98 ea b0 91 eb 8b a4 2e> 13
end :  안녕 나는 박동훈이라고 한다. 만나서 반갑다.
  • createWriteStream(출력 파일명) : 쓰기 스트림 생성
  • writeStream : 스트림 정보를 쓰며, 이벤트 리스너를 붙여서 사용(finish)
const fs = require('fs');

const writeStream = fs.createWriteStream('./Script/Chapter3/node3_5/writeme2.txt');
writeStream.on('finish', () => { // 파일 작성 시 finish 이벤트 리스너
    console.log('파일 쓰기 완료'); 
});

writeStream.write('나는 글을 쓴다.');
writeStream.write('나는 뇌절 쓴다.');
writeStream.end();
  • pipe : 스트림끼리 연결, 파일을 전달할 수 있으며 여러 번 연결 가능
const fs = require('fs');
const zlib = require('zlib'); // 파일을 압축하는 라이브러리

const readStream = fs.createReadStream('readme.txt');
const zlibStream = zlib.createGzip(); // 파일을 압축된 형태로 사용 가능(.zip)
const writeStream = fs.createWriteStream('writeme3.zip');
const piping = readStream.pipe(zlibStream).pipe(writeStream); // readStream 내용의 writeStream 연결
piping.on('finish', () => {
    console.log('done!');
});
기타 fs 메서드
  • fs.access(경로, 옵션, 콜백) : 폴더나 파일에 접근할 수 있는지 확인
    • F_OK : 파일 존재 여부
    • R_OK : 읽기 권한 여부
    • W_OK : 쓰기 권한 여부
    • ENOENT : 파일/폴더 없을 때 에러 코드
  • fs.mkdir(경로, 콜백) : 폴더를 만드는 메서드
  • fs.open(경로, 옵션, 콜백) : 파일의 아이디(fd 변수)를 가져오는 메서드
    • w : 쓰기 옵션
    • r : 읽기 옵션
    • a : 기존 파일 추가 옵션
  • fs.rename(기존 경로, 새 경로, 콜백) : 파일 이름 바꾸는 메서드
  • fs.readdir(경로, 콜백) : 폴더 안의 내용물 확인
  • fs.unlink(경로, 콜백) : 파일 삭제
  • fs.rmdir(경로, 콜백) : 폴더 삭제
  • fs.watch(경로) : 파일/폴더 변경 사항 감지
스레드풀
  • 작업 처리에 사용되는 스레드 개수를 정하고 백그라운드 작업에 해당 개수만큼 나누어 동시 처리
  • fs, crypto, zlib, dns.lookup 등

이벤트

  • 이벤트 만들기
  • on(이벤트명, 콜백) : 이벤트 리스너, 해당 이름의 이벤트 발생 시 콜백 연결
  • addListener(이벤트명, 콜백) : 이벤트 리스너 추가, on과 기능 동일
  • emit(이벤트명) : 이벤트 호출 메서드
  • once(이벤트명, 콜백) : 한 번만 실행되는 이벤트, 여러 번 호출해도 한 번만 실행됨
  • removeAllListeners(이벤트명) : 이벤트에 연결된 모든 이벤트 리스너 제거
  • removeListener(이벤트명, 리스너) : 이벤트에 연결된 리스너 하나씩 제거
  • off(이벤트명, 콜백) : removeListener 기능과 동일
  • listenerCount(이벤트명) : 현재 연결된 리스너 개수 확인
const EventEmitter = require('events');

const myEvent = new EventEmitter();
myEvent.addListener('event1', () => {
  console.log('이벤트 1');
});
myEvent.on('event2', () => {
  console.log('이벤트 2');
});
myEvent.on('event2', () => {
  console.log('이벤트 2 추가');
});
myEvent.once('event3', () => {
  console.log('이벤트 3');
}); // 한 번만 실행됨

myEvent.emit('event1'); // 이벤트 호출
myEvent.emit('event2'); // 이벤트 호출

myEvent.emit('event3');
myEvent.emit('event3'); // 실행 안 됨

myEvent.on('event4', () => {
  console.log('이벤트 4');
});
myEvent.removeAllListeners('event4');
myEvent.emit('event4'); // 실행 안 됨

const listener = () => {
  console.log('이벤트 5');
};
myEvent.on('event5', listener);
myEvent.removeListener('event5', listener);
myEvent.emit('event5'); // 실행 안 됨

console.log(myEvent.listenerCount('event2'));
  • 재사용성이 있는 이벤트 클래스 만들기
// 이벤트 클래스
const EventEmitter = require('events');

class Logger extends EventEmitter { // EventEmitter 클래스를 상속 받은 Logger 클래스 생성
    log(callback) {
        this.emit('log', 'started');
        callback();
        this.emit('log', 'ended');
    }
}

module.exports.Logger = Logger;
// Event 클래스 사용(main.js)

const logger = require('./logger.js');
const emitter = new logger.Logger();

emitter.on('log', (event) => {
    console.log(event);
});

emitter.log(() => {
    console.log('doing something!');
})
started
doing something!
ended

예외 처리

  • node: command not found : 노드를 설치했지만 환경 변수가 제대로 설정되지 않은 문제
  • ReferenceError: 모듈 is not a defined : 모듈이 제대로 require 되지 않음
  • Error: Cannot find module 모듈명 : 해당 모듈을 require 했지만 설치하지 않음
  • Error: Can't set headers after they are sent : 요청에 대한 응답을 보낼 때 응답을 두 번 이상 보낼 때 발생하는 오류, 요청에 대한 응답은 한 번만 보내야함
  • FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - Javascript heap out of memory : 메모리가 부족해 스크립트가 정상적으로 작동되지 않음, 코드가 잘못되거나 메모리가 부족한 경우
  • UnhandledPromiseRejectionWarning: Unhandled promise rejection : 프로미스 사용 시 catch 메서드를 붙이지 않았을 때 발생
  • EADDRINUSE 포트 번호 : 해당 포트 번호에 이미 다른 프로세스가 연결되어 있음
  • EACCES 또는 EPERM : 노드가 작업을 수행하는 데 권한이 충분하지 않음
  • EJSONPARSE : package.json 등 JSON 파일에 문법 오류가 존재
  • ECONNREFUSED : 요청을 보냈으나 연결이 성립되지 않을 경우 발생
  • ETARGET : package.json에 기록한 패키지 버전이 존재하지 않을 때 발생
  • ETIMEOUT : 요청을 보냈으나 응답이 일정 시간 내 오지 않을 경우 발생
  • ENOENT: no such file or directory : 지정한 폴더나 파일이 존재하지 않는 경우 발생

참고 문헌 및 강의

조현영, 『Node.js 교과서』, 개정 2판, 길벗출판사, 2020
엘리, 『노드로 배우는 백엔드 A-Z, 드림코딩 아카데미』

이 글이 도움이 되었나요?

0 minutes ago
작성된 댓글이 없습니다. 첫 댓글을 달아보세요!
    댓글을 작성하려면 로그인이 필요합니다.