본문 바로가기

ComputerScience/NodeJs

node - 3.6 파일 시스템 접근하기

728x90
 

더북(TheBook): Node.js 교과서 개정 2판

 

thebook.io

* 위 내용을 정리하였음

3.6 파일 시스템 접근하기

- 파일 생성, 삭제, 읽기가 가능하다.

const fs = require('fs');

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

- readFile의 결과물은 버퍼 형식으로 제공되기 때문에 toString()으로 변환해줘야 사람이 읽을 수 있다.

const fs = require('fs').promises;

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

- 위의 콜백 형식의 예제를 프로미스 형식으로 바꾸었다.

- 이번에는 파일을 생성해보자.

const fs = require('fs');

fs.writeFile('./writeme.txt', '글이 입력됩니다', (err) => {
  if (err) {
    throw err;
  }
  fs.readFile('./writeme.txt', (err, data) => {
    if (err) {
      throw err;
    }
    console.log(data.toString());
  });
});

- 프로미스로 바꿔보자.

const fs = require('fs').promises;

fs.writeFile('./writeme.txt', '글이 입력됩니다')
  .then(() => {
    return fs.readFile('./writeme.txt');
  })
  .then((data) => {
    console.log(data.toString());
  })
  .catch((err) => {
    console.error(err);
  });

 

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

- setTimeout같은 메서드와 더불어 노드에는 대부분 메서드를 비동기 방식으로 처리한다. 하지만 몇몇 메서드는 동기 방식으로도 사용할 수 있다.

const fs = require('fs');

console.log('시작');
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('끝');

- 시작, 끝이 먼저 출력되면 나머지는 랜덤으로 수행된다.

- 비동기 메서드들은 백그라운드에 해당 파일을 읽으라고 요청만하고 다음 작업으로 넘어간다.

- 나중에 메인 스레드에서 읽기가 완료되면 콜백 함수를 실행한다.

- 수백개의 I/O 요청이 들어와도 메인스레드는 처리를 백그라운드에게 위임하기 때문에 상당히 좋다.

- 위의 코드 예를 동기 방식으로 만들어보자.

const fs = require('fs');

console.log('시작');
let data = fs.readFileSync('./readme2.txt');
console.log('1번', data.toString());
data = fs.readFileSync('./readme2.txt');
console.log('2번', data.toString());
data = fs.readFileSync('./readme2.txt');
console.log('3번', data.toString());
console.log('끝');

- sync함수를 사용하여 동기로 작업을 처리할 수 있다.

- 이렇게 코드를 만들면 백그라운드가 작업하는 동안 메인스레드는 아무것도 못하고 대기해야 한다.

- 만약 비동기 메서드를 사용하되 순서를 유지하고 싶다면? 콜백을 활용한다. 아래 예시를 보자.

const fs = require('fs');

console.log('시작');
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('끝');
    });
  });
});

- 이번에는 콜백 지옥을 promise혹은 async/await으로 해결해 보자.

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

 

3.6.2 버퍼와 스트림 이해하기

- 파일을 읽거나 쓸때는 버퍼를 사용하는 방식, 스트림을 사용하는 방식이 있다.

- 버퍼링은 영상을 재생할 수 있을 때까지 데이터를 모으는 동작이고 스트리밍은 방송인의 컴퓨터에서 시청자의 컴퓨터로 영상 데이터를 조금씩 전송하는 동작이다.

- 물론 스트리밍중에 버퍼링을 사용할수도 있다.

- 노드는 파일을 읽을때 버퍼를 사용한다. 즉 메모리에 파일 크기만큼 공간을 마련해두고 다써지면 데이터를 다룰 수 있다.

- 버퍼를 직접 다룰 수 있는 버퍼 클래스를 알아보자.

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

- 버퍼의 단점은 미리 공간을 잡야 한다는 것이다. 만약 서버처럼 몇명이 이용할지 모르는 환경이라면 메모리 문제가 발생할 수 있다.

- 또한 다음 동작으로 넘어가기 위해서는 버퍼를 다쓴 후에 가능하고 파일 읽기, 압축, 파일 쓰기를 연달아 할때는 매번 전체 용량을 버퍼로 처리해야 한다. 

- 그래서 버퍼의 크기를 작게 만들어 여러번 나눠 보내는 방식이 생겼고 이를 스트림이라고 한다. 버퍼크기 1MB로 백번에 걸쳐 나눠 보내서 100MB를 보내는 것이다.

const fs = require('fs');

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

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

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

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

- 스트림을 먼저 만든다 highwatrmark가 버퍼의 크기이다 16B

- readstream은 이벤트 리스너를 붙여서 사용한다. (on)

- 파일 읽기가 시작되면 data 이벤트가 발생한다.

- 파일을 다 읽으면 end 이벤트가 발생한다. data배열에 chunk들을 담고 마지막에 각 요소들을 다 합쳐서 스트링으로 반환한다.

const fs = require('fs');

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

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

- end메서드로 종료를 알리면 on리스너 finish이벤트를 감지한다.

- 이번에는 createReadStream으로 파일을 읽고 그 스트림을 전달받아 createWriteStream으로 파일에 써보자. 이렇게 연결하는 것을 파이핑이라고 한다.

const fs = require('fs');

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

- 이렇게 pipe를 해주면 알아서 데이터가 넘어간다.

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

- zlib은 파일을 압축하는 모듈이다.

- 위처럼 연속으로 pipe를 통해 스트림을 연결하면 읽어와서 압축하여 파일로 써진다. 

- 결과는 readme4.txt.gz파일이 된다.

- 정리하면 버퍼는 공간을 잡기 때문에 메모리 사용량이 커질수 있다. 스트림을 통해 동영상 같은 큰 파일을 효과적으로 전송할 수 있다.

 

3.6.3 기타 fs 메서드 알아보기

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

- fs.access : 폴더나 파일에 접근할 수 있는지 체크한다. F_OK는 파일 존재 여부, R_OK는 읽기권한 여부, W_OK는 쓰기 권한 여부를 체크한다. 파일/폴더가 없을 때의 에러 코드는 ENOENT이다.

- fs.mkdir : 폴더 생성, 이미 있으면 에러 발생

- fs.open : 파일 아이디를 가져온다. 파일이 없으면 파일을 만들어서 가져온다. w(쓰기), r(읽기), a(추가) 옵션을 주고 파일을 열면 읽기 쓰기를 할 수 있다.

- fs.rename(기존 경로, 새 경로, 콜백) : 이름 바꾸기 잘라내기 가능 

- fs.readdir(경로, 콜백) : 폴더 안의 내용물을 확인, 배열로 파일 이름 반환

- fs.unlink(경로, 콜백) : 파일을 지운다. 파일이 없다면 에러기 때문에 파일을 꼭 확인한다.

- fs.rmdir(경로, 콜백) : 폴더를 지운다. 풀더 안에 파일들이 있다면 에러 발생.

const fs = require('fs');

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

- 파일 복사 예제 이다.

const fs = require('fs');

fs.watch('./target.txt', (eventType, filename) => {
  console.log(eventType, filename);
});

- 내용물을 수정하면 change이벤트가 발생하고 파일명을 변경하거나 삭제하면 rename이벤트가 발생한다.

 

3.6.4 스레드풀 알아보기

- 비동기 메서드들은 백그라운드에서 실행되고, 실행된 후에는 다시 메인 스레드의 콜백 함수나 프로미스의 then 부분이 실행된다.

- 이때 fs를 여러번 실행해도 백그라운드에서 동시에 처리되는데 스레드풀 덕분이다.

- crypto.pbkdf2는 스레드풀을 쓰는 메서드이다. 아래 예시를 통해 스레드풀을 이해해 보자.

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

- 기본으로 설정된 스레드풀의 개수가 4개이기 때문에 1,2,3,4가 동시에 처리되고 그다음 5,6,7,8이 동시에 처리되었다.

- 실제로 네 작업중 누가 먼저 완료되는지는 알수 없다.

728x90
반응형