Node.js

Node.js 내장 모듈 - fs

김핵센 2023. 5. 16. 23:32

오늘은 Node.js 내장 모듈인 fs에 대해 알아볼 것이다.

fs란 파일 시스템에 접근하는 모듈이다.

  • 파일/폴더 생성, 삭제, 읽기, 쓰기 가능
  • 웹 브라우저에서는 제한적이었으나 노드는 권한을 가지고 있음

파일 읽기 예제

readFile.js

const fs = require('fs');

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

콘솔 결과

data를 그대로 출력할 경우 버퍼 형식으로 데이터가 표현된다.

버퍼는 일련의 바이너리 데이터를 나타내는 데 사용되는 객체이다.
각각의 값은 16진수로 표현되며, 데이터의 바이트를 나타낸다.

사람이 읽을 수 있게 하려면 data.toString()을 하면 된다.

현재 경로에 있는 readme.txt 파일을 읽어온다.

 

콜백 함수의 늪에 빠질 수 있기 때문에 util.promisify를 통해 프로미스로 감싸줘서 사용하면 된다.

하지만, fs에서는 보다 더 효율적인 방법을 제공한다.

 

readFilePromise.js

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

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

바로 require('fs')에 .promises만 붙이면 then/catch를 쓸 수 있다. 

파일 생성 예제

writeFile.js

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

해당 파일을 실행할 경우 현재 경로에 writeme.txt 파일을 생성하고 읽어준다.

writeFile.js 또한 .promises를 붙여서 사용할 수 있다.

 

writeFilePromise.js

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

 

프로미스나 콜백은 비동기 방식인데 순서대로 실행되지 않는 것을 아래 예제를 통해 확인할 수 있다.

 

async.js

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

async.js 코드를 보면 위에서부터 아래로 1번, 2번, 3번이 출력될 것 같다고 예상해볼 수 있다.

하지만 비동기 함수이기 때문에 아래와 같이 랜덤으로 출력된다.

콜백들은 백그라운드로 보낸다. 백그라운드로 넘어간 것들은 동시에 실행되기 때문에 완료되는 순서를 예측할 수가 없다.

순서를 보장해주지 않기 때문에 실제 서비스를 개발할 때 문제가 생길 수 있다.

순서를 보장하기 위해서 여러 방법을 사용할 수 있다.

 

1. 동기 메소드 사용

fs 모듈은 동기 메소드도 지원해준다. 동기이기 때문에 처리 속도는 비동기보다 비효율적이다.

동기 메소드를 사용하는 경우는 딱 한번 실행하거나 서버 초기화 작업을 할 경우에 사용한다.

 

sync.js

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

콘솔 결과

2. 콜백 안의 콜백(= 콜백 헬)

asyncOrder.js

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('끝');
    });
  });
});

이 방식은 소위 말하는 콜백 헬에 빠질 수 있기 때문에 추천하지 않는다.

3. 프로미스

asyncOrderPromise.js

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

2번에서 콜백 헬 문제를 해결한 방식이다. promises로 감싸줘서 then/catch를 사용한다.

이 방식도 then을 연달아 사용하면 가독성이 떨어지기 때문에 더 깔끔하게 만들고 싶으면 4번 방식을 사용하면 된다.

4. async/await

asyncAwait.js

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

async function main() {
  console.log('시작');
  let data = await fs.readFile('./readme.txt');
  console.log('1번', data.toString());
  data = await fs.readFile('./readme.txt');
  console.log('2번', data.toString());
  data = await fs.readFile('./readme.txt');
  console.log('3번', data.toString());
  console.log('끝');
}
main();

async/await을 사용하면 1번 동기 메소드를 사용한 것처럼 깔끔하게 코드를 작성할 수 있다.