본문 바로가기
전공 수업/웹 서버 프로그래밍(Node.js)

[9주 차] - Node의 fs 모듈 (Stream, 디렉토리와 파일 생성 및 수정, 삭제, fs.copyFile() 메소드), Buffer 모듈 활용, 이벤트 생성과 호출

by TwoJun 2023. 4. 29.

    과목명 : 웹 서버 프로그래밍(Web Server-side programming with Node.js)

수업일자 : 2023년 04월 27일 (목)

 

Node.js & Express.js

 

 

 

 

 

1. Module : fs - 동기식 메소드(Synchronous method), 비동기식 메소드(Asynchronous method)

1-1. Node는 대부분의 내장된 모듈 메소드를 비동기 방식으로 처리

(1) 비동기(Asynchronous) 방식의 경우 코드 라인의 순서와 코드의 실행 순서가 일치하지 않는 것을 의미하기도 합니다.

 

(2) 일부 메소드들은 동기식(Synchronous) 방식으로도 사용할 수 있습니다.

 

(3) fs 모듈의 메소드를 사용하기 때문에 require('fs') 함수를 호출합니다.

 

(4) 아래의 코드 예제로 fs 모듈의 비동기식 메소드 fs.readFile()을 한 번 확인해 보겠습니다.

// async.js (9 weeks)
const fs = require('fs');

console.log('start'); 
fs.readFile('./readme2.txt', (err, data) => {
  if (err) {
    throw err;
  }
  console.log('1번', data.toString());
});
fs.readFile('./readme2.txt', (err, data) => {
  if (err) {
    throw err;
  }
  console.log('2번', data.toString());
});
fs.readFile('./readme2.txt', (err, data) => {
  if (err) {
    throw err;
  }
  console.log('3번', data.toString());
});
console.log("finish");

 

 

<실행 결과>

(1) 실행 결과 확인 시, 코드의 실행 흐름이 순차적이지 않은 것을 확인할 수 있습니다. (비동기 방식)

Command : node async.js

 

 

 

1-2. fs 모듈의 동기식 메소드 사용해 보기

(1) fs.readFileSync() 메소드

// sync.js (9 weeks)
const fs = require('fs');

console.log('start');

let inputData = fs.readFileSync("./readme2.txt");
console.log('1', inputData.toString());

inputData = fs.readFileSync("./readme2.txt");
console.log('2', inputData.toString());

inputData = fs.readFileSync("./readme2.txt");
console.log('3', inputData.toString());

console.log('finish');

 

<실행 결과>

- 동기적으로 실행된 코드 흐름을 확인할 수 있습니다.

Command : node sync.js

 

 

 

 

1-3. fs.readFile() 비동기 메소드의 순서를 유지하는 방법 : 코드에서 Callback 형식을 사용한다.

(1) Callback 형식을 유지함에 따라 동기적인 실행 흐름을 보장받을 수 있게 되었지만 코드의 들여쓰기 현상으로 가독성이 떨어지는 것을 알 수 있습니다. (Callback Hell Phenomenon)

 

(2) 아래 코드와 함께 실행 결과를 확인해 보겠습니다.

// callback hell phenomenon
// asyncOrder.js
const fs = require('fs');
console.log('start');

fs.readFile('./readme2.txt', (err, data) => {
  if (err) {
    throw err;
  }
  console.log('1번', data.toString());

  fs.readFile('./readme2.txt', (err, data) => {
    if (err) {
      throw err;
    }
    console.log('2번', data.toString());

    fs.readFile('./readme2.txt', (err, data) => {
      if (err) {
        throw err;
      }
      console.log('3번', data.toString());
    });
  });
});

console.log("finish");

 

<실행 결과>

- 비동기 메소드 fs.readFile()을 사용하였으나 코드 실행 흐름이 동기적으로 변경된 것을 확인할 수 있습니다.

Command : node asyncOrder.js

 

 

 

 

 

 

2. 파일을 읽고 쓸 수 있는 방식 : 버퍼와 스트림(Buffer and Stream)

왼쪽부터 오른쪽 / 버퍼(Buffer)와 스트림(Stream)

2-1. 버퍼(Buffer)

(1) 파일을 읽을 때 파일 크기만큼의 메모리 공간에 데이터가 저장됩니다.

 

(2) 일정한 크기가 되면 이후에 한 번에 처리합니다.

 

(3) Buffering : 버퍼에 데이터가 모두 찰 때까지 데이터를 모으는 과정

 

(4) 버퍼는 데이터를 임시로 저장하는 공간으로 주로 I/O 작업(입출력 작업)에서 발생하는 속도 차이를 완화하기 위해서 사용됩니다. 예를 들어, 빠른 프로세서와 느린 하드 디스크 사이에서 데이터를 전송할 때 이들 간의 속도 차이를 감소시키기 위해 버퍼를 사용합니다. 버퍼는 일반적으로 순차적으로 데이터를 읽고 쓰는 데 사용되며 FIFO(First In First Out) 구조를 가지고 있습니다.

 

(5) 입력 속도에 비해 출력 속도가 느린 경우 데이터를 임시로 저장할 수 있는 공간으로, 예를 들어 유튜브 영상 같은 대용량의 데이터를 서버로부터 가져오려면 상당히 많은 시간이 소요됩니다. 이에 따라 데이터를 버퍼에 담고 버퍼가 가득 차게 되면 데이터를 처리하게 되는데 이때 버퍼에 가득 차게 된 데이터의 내용아래에서 설명할 스트림(Stream)입니다.

 

 

 

 

2-2. 스트림(Stream)

(1) 일정한 크기로 나눠서 여러 번에 걸쳐서 처리합니다.

 

(2) 버퍼 또는 Chunk의 크기를 작게 만들어서 주기적으로 데이터를 전달합니다.

 

(3) Streaming : 일정한 크기의 데이터를 지속적으로 전달하는 작업

 

(4) 스트림은 쉽게 말해서 데이터를 운반되고 있는 흐름을 의미합니다. 단방향 통신의 특성을 갖기 때문에 하나의 스트림으로 입력과 출력을 동시에 처리할 수 없고 먼저 보낸 데이터를 먼저 받기 때문에 연속적인 데이터의 흐름으로 볼 수 있어서 큐(Queue)의 FIFO(First In First Output) 구조를 가지고 있습니다.

 

 

 

 

2-3. Buffer 객체의 메소드

(1) from('문자열')

- 문자열을 버퍼로 변환할 수 있습니다.

 

 

(2) length()

- 버퍼의 크기를 Byte 단위로 알려주는 메소드입니다.

 

 

(3) toString(Buffer)

- 버퍼를 다시 문자열로 변환할 수 있습니다.

 

- 이때 base64나 hex를 인자(Argument)로 넣게 되면 해당 인코딩으로도 변환할 수 있습니다.

 

 

(4) concat(Array)

- 배열 내부에 든 버퍼들을 하나로 합칩니다.

 

 

(5) alloc(Byte)

- 빈 버퍼(Empty buffer)를 생성합니다.

 

- Byte 값을 인자로 주면 해당 크기의 버퍼를 생성할 수 있습니다.

 

 

 

 

2-4. Node에서는 Buffer 객체를 사용

(1) Buffer 객체를 사용하고 Buffer 객체 메소드로 연관된 작업들을 처리할 수 있습니다.

 

(2) LF(Line Feed) : 다음 행의 맨 앞으로 이동합니다.

Hexadecimal and Decimal of String

// buffer.js - 9 weeks

const buffer = Buffer.from('BufferStringStream');
console.log('from(): ', buffer);
console.log('Length : ', buffer.length);
console.log('toString() :', buffer.toString());

const array = [Buffer.from("Buffer "), Buffer.from("String "), Buffer.from("String ")];

const buffer2 = Buffer.concat(array);

console.log('concat() : ', buffer2.toString());

// Buffer.alloc(n) : n byte의 empty buffer generate.
const buffer3 = Buffer.alloc(5);
console.log('alloc() : ', buffer3);

 

<실행 결과>

Command : node buffer.js

 

 

 

 

2-5. 파일을 읽는 스트림(Stream) 사용해 보기 : fs.createReadStream() 메소드

(1) fs.createReadStream(경로, 버퍼의 크기)

- 해당 메소드의 인자로 파일 경로와 옵션 객체를 전달합니다.

 

- highWaterMark 옵션의 경우 버퍼의 크기(Byte 단위, 기본 값 : 64KB)입니다.

 

- readStream.on() 메소드

→ 첫 번째 메소드(Chunk 전달), 두 번째 메소드(Chunk 전달 완료), 세 번째 메소드(오류에 대한 예외, EventListener와 같이 사용한다.)

 

 

(2) readme3.txt 

Hello! My name is NodeJS!!!!

 

 

(3) createReadStream.js

// createReadStream.js (9weeks)

const fs = require('fs');

const readStream = fs.createReadStream('./readme3.txt', { highWaterMark: 16 });
const emptyData = [];

readStream.on('data', (chunk) => {
  emptyData.push(chunk);
  console.log('data :', chunk, chunk.length);
})

readStream.on('end', () => {
  console.log('end : ', Buffer.concat(emptyData).toString());
})

readStream.on('error', (err) => {
  console.log('error :', err);
})

 

<실행 결과>

Command : createReadStream.js

 

 

 

 

2-6. 파일을 쓰는 스트림(Stream) 사용해 보기 : fs.createWriteStream() 메소드

(1) fs.createWriteStream(파일 경로)

- createWriteStream() 메소드에 인자로 파일 경로를 전달합니다.

 

- write() 메소드로 chunk를 직접 입력하고 end() 메소드로 스트림을 종료시킵니다.

 

- 스트림이 종료된 경우 finish 이벤트가 발생하게 됩니다.

const fs = require('fs');

const writeStream = fs.createWriteStream('./writeme2.txt');
writeStream.on('finish', () => {
  console.log('파일 쓰기 완료');
});

writeStream.write('이 글을 씁니다.\n');
writeStream.write('한 번 더 씁니다.');
writeStream.end();

 

<실행 결과>

Command : node createWriteStream.js

 

- 연관 디렉토리에 writeme2.txt 파일이 생성되었습니다.

writeme2.txt 파일이 생성된 것을 확인할 수 있다.

 

- writeme2.txt 파일 실행 시, write() 메소드의 인자로 넘긴 문자열이 정상적으로 확인됩니다.

writeme2.txt 파일 실행 시, write() 메소드의 인자로 넘긴 문자열을 확인할 수 있다.

 

 

 

 

2-7. 텍스트 파일(Text file)

(1) 텍스트 파일의 경우 아스키 코드를 이용하여 저장합니다.

 

(2) 텍스트 파일은 연속적인 라인들로 구성되어 있습니다.

 

(3) '\r' (Carriage Return) : 커서가 문장의 맨 앞으로 이동하게 됩니다.

 

 

 

 

2-8. 파일 스트림(Stream) 사이에 pipe 기능 이용하기 - 파일 복사(File copy)

(1) pipe 기능을 통해 여러 개의 스트림을 이어줄 수 있습니다.

 

(2) pipe 기능을 통해 스트림으로 파일을 복사하는 예제를 아래의 코드로 확인해 보겠습니다.

 

 

(3) readme4.txt

writeme3.txt 해당 파일로 이동하겠습니다.

 

 

(4) pipe.js

const fs = require('fs');

const readStream = fs.createReadStream('readme4.txt');
const writeStream = fs.createWriteStream('writeme3.txt');
readStream.pipe(writeStream);

 

<실행 결과>

Command : node pipe.js

 

- writeme3.txt 파일로 정상적으로 복사된 것을 확인할 수 있습니다.

writeme3.txt 파일로 정상적으로 복사된 것을 확인할 수 있다.

 

- 파일이 복사는 되었지만 컨텐츠만 복사되었으며, 파일 생성 및 수정 시기와 같은 파일 속성은 변하지 않은 것을 알 수 있습니다.

컨텐츠만 복사되었으며, 파일 생성 및 수정 시기와 같은 파일 속성은 변하지 않은 것을 알 수 있다.

 

 

 

 

2-9. 파일 스트림(Stream) 사이에 pipe 기능 이용하기 - 파일 압축(File compression)

(1) 파일을 압축한 후 복사하는 예제입니다. 압축에는 zlib 내장 모듈이 사용됩니다.

const zlib = require('zlib');
const fs = require('fs');

const readStream = fs.createReadStream('./readme4.txt');
const zlibStream = zlib.createGzip();
const writeStream = fs.createWriteStream('./readme4.txt.gz');
readStream.pipe(zlibStream).pipe(writeStream);

 

<실행 결과>

Command : node gzip.js

 

- 압축된 파일인 readme4.txt.gz 파일이 연관 디렉토리에 생성된 것을 확인할 수 있습니다.

압축된 파일인 readme4.txt.gz 파일이 연관 디렉토리에 생성된 것을 확인할 수 있다.

 

 

 

 

 

 

3. Module : fs - 디렉토리(Directory), 파일(File)의 생성 및 수정, 삭제 

- Node의 fs 모듈을 이용하면 디렉토리, 파일과 연관된 작업을 수행할 수 있습니다.

 

- 디렉토리와 파일 작업과 관련된 연관 메소드들을 알아보도록 하겠습니다.

 

 

 

3-1. fs.access(경로, 옵션, 콜백 함수)

(1) 디렉토리나 파일에 접근할 수 있는지 체크할 수 있는 메소드입니다.

 

(2) 두 번째 인자로 상수를 넣을 수 있는데 다음과 같은 종류의 상수들이 존재합니다.

- F_OK : 파일 존재 여부

- R_OK : 읽기 권한 여부

- W_OK : 쓰기 권한 여부

 

(3) 최종적으로 파일, 디렉토리에 대한 권한이 없다면 오류가 발생하며 오류 코드는 ENOENT입니다.

 

 

 

3-2. fs.mkdir(경로, 콜백 함수)

(1) 디렉토리를 생성하는 메소드입니다.

 

(2) 이미 디렉토리가 존재한다면 오류를 발생시키므로 우선 fs.access() 메소드를 통해 디렉토리의 존재 여부를 확인합니다.

 

 

 

3-3. fs.open(경로, 옵션, 콜백 함수)

(1) 파일의 아이디(fd 변수)를 가져올 수 있는 메소드입니다.

 

(2) 파일이 없다면 파일을 생성한 뒤 해당 아이디를 가져옵니다. 가져온 아이디를 통해 fs.read() 메소드나 fs.write() 메소드로 읽거나 쓸 수 있습니다.

 

(3) 두 번째 인자로 어떤 동작을 수행해 줄지 정의할 수 있습니다.

- 쓰기 : w   / 읽기 : r   / 기존 파일에 추가 :  a

 

 

 

3-4. fs.rename(기존 경로, 새로운 경로, 콜백 함수)

(1) 파일의 이름을 변경하는 메소드입니다.

 

(2) 기존 파일의 위치와 새로운 파일의 위치를 인자로 넣어주면 되며 반드시 동일한 디렉토리를 지정할 필요는 없으므로 잘라내기 같은 기능을 수행할 수도 있습니다.

 

 

 

3-5. fs.readdir(경로, 콜백 함수)

(1) 디렉토리 내부에 존재하는 데이터를 확인할 수 있는 메소드입니다.

 

(2) 결과 값으로 배열 내부에 파일명과 디렉토리명이 출력됩니다.

 

 

 

3-6. fs.unlink(경로, 콜백 함수)

(1) 파일을 삭제할 수 있는 메소드입니다.

 

(2) 삭제할 파일이 없다면 오류가 발생하므로 파일이 존재하는지 반드시 확인해야 합니다.

 

 

 

3-7. fs.rmdir(경로, 콜백 함수)

(1) 디렉토리를 삭제할 수 있는 메소드입니다.

 

(2) 디렉토리 내부에 파일이 존재한다면 오류가 발생하므로 디렉토리 내부의 파일을 모두 삭제하고 해당 메소드를 호출해야 합니다.

 

 

 

3-8. 파일 및 디렉토리 생성 예제

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) => {
          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('이미 폴더 있음');
  }
});

 

<실행 결과>

- 첫 번째 커맨드에서는 작업이 수행되었지만 두 번째 커맨드부터는 이미 디렉토리가 존재함에 따라 수행되지 않았습니다.

Command : fsCreate.js

 

 

 

3-9. 디렉토리의 내부 데이터를 확인한 후 디렉토리를 삭제하는 예제

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('폴더 삭제 성공');
    });
  });
});

 

<실행 결과>

- 첫 번째 수행 결과를 보면, 작업 디렉토리에 folder 디렉토리가 존재했으므로 디렉토리 내부 파일 삭제, 디렉토리까지 모두 삭제된 것을 확인할 수 있습니다.

 

- 그러나 두 번째 수행 결과에서는 삭제되어 존재하지 않는 디렉토리의 삭제를 시도했기 때문에 ENOENT 오류가 발생한 것을 확인할 수 있습니다.

첫 번째 수행 결과 : 삭제 성공 / 두 번째 수행 결과 : ENOENT 오류 반환

 

 

 

 

 

 

4. fs.copyFile() 메소드를 통한 파일 복사

(1) 파일을 복사할 수 있는 메소드 중 하나로 fs 모듈에서 지원하는 메소드입니다.

 

 

4-1. 스트림(Stream)을 활용한 파일 복사, fs.copyFile() 메소드를 활용한 파일 복사의 차이점

(1) 스트림(Stream)

- 스트림을 통한 파일 복사가 완료된 경우, 완료된 결과값에 대한 수정이 가능한 상태이며 파일 속성 등은 복사되지 않고, 단순히 스트림을 통한 파일의 컨텐츠만 복사합니다.

 

(2) fs.copyFile()

그러나 해당 메소드를 통해 복사한 경우 컨텐츠 내용부터 파일 생성, 수정 시기와 같은 전반적인 모든 속성을 모두 복사하기 때문에 컨텐츠 등과 같은 결과값을 수정할 수 없습니다.

 

(3) 해당 내용을 확인해 볼 수 있는 예제 코드, 실행 결과를 확인해 보겠습니다.

const fs = require('fs');

fs.copyFile('readme4.txt', 'writeme4.txt', (error) => {
  if (error) {
    return console.error(error);
  }
  console.log('복사 완료');
});

 

<실행 결과>

- 커맨드가 정상적으로 수행된 것을 확인할 수 있고 하단에 완료된 시각은 오후 10시 38분입니다.

Command : node copyFile.js / 커맨드 수행 완료 시간 10시 38분

 

- 현재 readme4.txt의 데이터가 writeme4.txt 파일로 모두 복사된 것을 확인할 수 있습니다.

정상적으로 데이터가 복사된 것을 확인할 수 있다.

 

- 여기서 주의할 점은 위에서 언급된 것처럼, fs.copyFile() 메소드를 사용했기 때문에 readme3.txt 파일의 데이터뿐만이 아닌 파일의 생성, 수정 시기와 같은 주요 속성들도 함께 writeme4.txt 파일로 복사된 것을 확인할 수 있었고, 이를 통해 커맨드를 통해 복사가 완료된 시간과 실제 생성된 파일의 최근 생성 / 수정 일자가 일치하지 않는 것을 알 수 있습니다.

이전 파일과 복사된 두 파일을 비교해 보면 파일의 최근 수정 시기까지 동일한 것을 확인할 수 있다.

 

 

 

 

 

 

5. Module : event - 이벤트 생성과 호출

- event 모듈을 사용해서 커스텀 이벤트를 생성할 수 있습니다.

 

- 또한 Node에는 event 모듈 EventEmitter 클래스가 내장되어 있고 이를 사용해 이벤트와 이벤트 핸들러(Event handler)를 연동시킬 수 있습니다

 

- 이벤트 핸들러(Event handler) : 특정 요소에서 발생하는 이벤트를 처리하기 위해 이벤트 핸들러라는 함수를 이용해서 연결시켜야 합니다.

const EventEmitter = require('event');

 

 

5-1. on(이벤트명, 콜백 함수)

(1) 이벤트 이름이벤트 발생 시의 콜백 함수를 서로 연결합니다. 이렇게 연결하는 동작을 Event Listening(이벤트 리스닝)이라고 부릅니다.

 

(2) 이벤트 하나에 이벤트 여러 개를 추가할 수도 있습니다.

 

 

 

5-2. addListener(이벤트명, 콜백 함수)

(1) on() 메소드와 기능이 동일합니다.

 

 

 

5-3. emit(이벤트명)

(1) 이벤트를 호출하는 메소드입니다.

 

(2) 이벤트 이름을 인자로 넣어주게 되면 미리 등록했던 이벤트 콜백 함수가 실행됩니다.

 

 

 

5-4. once(이벤트명, 콜백 함수)

(1) 이벤트를 한 번만 실행시킬 수 있는 메소드입니다.

 

 

 

5-5. removeAllListeners(이벤트명)

(1) 이벤트에 연결된 모든 Event Listener를 삭제합니다.

 

 

 

5-6. removeListener(이벤트명, 리스너)

(1) 이벤트에 연결된 Listener를 하나씩 삭제합니다.

 

 

 

5-7. off(이벤트명, 콜백 함수)

(1) Node 10 버전부터 추가된 메소드로, removeListener() 메소드와 기능이 동일합니다.

 

 

 

5-8. listenerCount(이벤트명)

(1) 현재 Listener가 몇 개 연결되어 있는지 확인할 수 있는 메소드입니다.

 

 

 

5-9. Custom 이벤트 예제

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'));

 

<실행 결과>

 

 

 

 

 

 

6. Reference

Node.js 교과서(Node.js Textbook) - 저자 : 조현영 

https://www.zerocho.com/book/1

 

ZeroCho Blog

ZeroCho의 Javascript와 Node.js 그리고 Web 이야기

www.zerocho.com

 

 

 

 

- 학부에서 수강했던 전공 수업 내용을 정리하는 포스팅입니다.

- 내용 중에서 오타 또는 잘못된 내용이 있을 시 지적해 주시기 바랍니다.

댓글