파일 시스템 접근

Haechan Kim·2022년 1월 9일

Node.js

목록 보기
4/15
  • fs 모듈
    fs모듈은 파일 시스템에 접근하는 모듈.
    파일 생성, 삭제, 읽고 쓰는 기능.
    웹 브라우저에서 js사용시에는 일부 제거하고는 파일 접근이 금지되어있어 낯설다.

  • 파일 읽기

const fs = require('fs');

fs.readFile('./readme.txt', (err, data) => {
    if (err) {
        throw err;
    }
    console.log(data); // <Buffer 72 65 61 64 20 6d 65 21 21 21>
    console.log(data.toString()); // readme.txt의 내용 출력
})

fs모듈을 불러온 뒤 읽을 파일의 경로를 지정
파일을 읽은 후에 실행될 콜백 함수도 readFile 메서드의 인수로 같이 넣는다.
이 콜백 함수의 파라미터로 에러, 데이터를 받음
readFile의 결과물은 버퍼 형식을 제공됨.
버퍼는 메모리의 데이터. toString해야 문자열로 변환

fs는 콜백 형식의 모듈이기 때문에 사용이 불편
fs모듈을 프로미스 형식으로 바꾸는 방법 사용

// fs모듈에서 promise속성 불러오면 프로미스 기반의 fs 모듈 사용할 수 있음
const fs = require('fs').promises;

fs.readFile('./readme.txt')
.then((data)=>{ //성공 시 then
    console.log(data);
    console.log(data.toString());
})

.catch((err) => {
    console.error(err);
});

앞으로 프로미스 기반의 fs모듈 사용

  • 파일 쓰기
const fs = require('fs').promises;
// writeFile 메서드에 생성될 파일 경로와 내용 입력
fs.writeFile('./writeme.txt', '글이 입력됩니다')
.then(()=>{ // 실행 성공 후 파일 읽기
    return fs.readFile('./writeFile.txt');
})
.then((data)=>{ // 읽기 성공 후 데이터 출력
    console.log(data.toString());
})
.catch((err)=>{ // 실패 시 에러 출력
    console.error(err);
});
  • 동기 메서드와 비동기 (async) 메서드
    setTimeout같은 타이머와 process.nextTick 외에도 노드는 대부분의 메서드를 비동기 방식으로 처리.
    but 몇 메서드는 동기 방식. 특히 fs 모듈
    어떤 메서드가 동기 또는 비동기 방식인지와 언제 어떤것을 사용할까?

  • 파일 하나를 여러번 읽는 경우

// 비동기
const fs = require('fs');

console.log('시작');
// 3번 반복해서 읽음
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('끝');

실행할때마다 결과 달라짐
시작 / 끝 / 2/3/1, 1/3/2, ...

비동기 메서드들은 백그라운드에 해당 파일을 읽으라고만 요철하고 다음 작업으로 넘어감
따라서 파일 읽기 요청 3번 보내고 '끝' 찍음
나중에 읽기 완료되면 백그라운드가 다시 메인 스레드에 알림
메인 스레드는 그때 등록된 콜백 함수 실행

이 방법은 수백개의 I/O 요청이 들어와도 메인 스레드는 백그라운드에 요청 처리 위임
나중에 백그라운드가 각각의 요청 처리 완료 알리면 그때 콜백 함수 처리

여기서 백그라운드는 요청 3개 거의 동시에 실행

  • 동기와 비동기, 블로킹과 논 블로킹
    서로 다른 의미
    • 동기, 비동기: 백그라운드 작업 완료 확인 여부
    • 블로킹, 논 블로킹: 함수가 바로 return 되는지 여부

노드에서는 동기-블로킹, 비동기-논 블로킹 방식이 대부분.
동기-논 블로킹등은 없다고 봐도 무방.

  • 동기-블로킹: 백그라운드 작업 완료 여부 계속 확인
    호출한 함수가 바로 리턴되지 않고 백그라운드 작업이 끝나야 리턴됨

  • 비동기-논 블로킹: 호출한 함수가 바로 리턴되고 다음 작업으로 넘어감
    백그라운드 작업 완료 여부는 신경쓰지 않고 나중에 백그라운드가 알림 주면 그때 처리

readFileSync 사용하면 순서대로 출력 가능

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 메서드 사용시 이전 작업 완료되어야만 다음 작업 진행 가능
즉 백그라운드 작업하는 동안 메인 스레드는 아무것도 못하고 대기
백그라운드는 fs작업 동시에 처리할 수 있는데 Sync 사용하면 동시에 처리 못함 => 비동기 사용이 효율적

동기 메서드들은 이름 뒤에 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('끝')
        });
    });
});

이전 readFile의 콜백에 다음 readFile 넣으면 됨
but 콜백 지옥
콜백 지옥은 propmise난 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());
    return fs.readFile('./readme2.txt');
})
.catch((err)=>{
    console.error(err)
})
  • 버퍼와 스트림
    readFile과 readFileSync 에서 받아온 데이터를 toString()으로 변환하는 이유는?
    결론부터 말하면 데이터가 버퍼이기 때문

파일 읽고 쓰는 방식에는 크게 버퍼, 스트림 방식이 있다
노드는 파일 읽을 때 메모리에 파일 크기만큼 공간 마련해둠
파일 데이터를 메모리에 저장한 뒤 사용자가 조작할 수 있도록 함
이때 메모리에 저장된 데이터가 버퍼

  • Buffer
    버퍼를 직접 다룰 수 있는 클래스
    Buffer 객체의 여러가지 메서드
// from() : 문자열을 버퍼로 바꿈
const arr = [Buffer.from('hello '), Buffer.from('world!')]
console.log(arr);
const buffer = Buffer.concat(arr); // 배열 안에 든 버퍼들을 하나로 합침
console.log(buffer.toString()); // 버퍼를 문자열로
console.log(buffer.length); // 버퍼의 크기
const buffer2 = Buffer.alloc(5); // 빈 버퍼를 생성
console.log(buffer2);
  • readFile 방식 버퍼의 단점
    100MB인 파일을 읽으려면 메모리에도 100MB의 버퍼를 만들어야 함
    이 작업 동시에 열개만 해도 1GB의 메모리 사용 됨
    메모리 문제 발생 가능

또한 모든 내용 버퍼에 다 쓴 후에야 다음작업으로 넘어가므로 매번 전체 용량을 버퍼로 처리해야만 다음단계로 넘어갈 수 있다

  • 스트림
    버퍼 크기를 작게 만든 후 여러번에 나눠 보내는 방식
    1MB버퍼를 만들고 100번에 걸쳐 나눠 보내기
    이렇게 하면 1MB메모리로 100MB 파일 전송할 수 있다

  • createReadStream
    파일 읽는 스트림 메서드

// createReadStream.js
const fs = require('fs');
// 읽기 스트림 만든다. (파일경로, 옵션 객체) : 버퍼의 크기(단위) 정하는 옵션
// 16B씩 읽음. 5번에 걸쳐서 전송 됨
const readStream = fs.createReadStream('./readme3.txt', {highWaterMark: 16});
const data = [];

// readStream은 이벤트 리스너 붙여서 사용
readStream.on('data', (chunk) => { // data 이벤트. 파일 읽기 시작되면 발생
    data.push(chunk); // 들어오는 chunk들을 하나씩 push
    console.log('data: ', chunk, chunk.length);
});

readStream.on('end', () => { // 다 읽으면 end 이벤트 발생
    // chunk들 concat으로 합쳐 문자열로 만듬
    console.log('end: ', Buffer.concat(data).toString());
});

readStream.on('error', (err) => {
    console.log('error: ', err);
})
  • createWriteStream
    파일 쓰는 스트림 메서드
const fs = require('fs');
// 파라미터로 (파일 명, 옵션)
const writeStream = fs.createWriteStream('./writeme2.txt');
writeStream.on('finish', () => { // finish 이밴트 리스너.
    console.log('파일 쓰기 완료!!'); // 파일 쓰기 종료 시 콜백함수 호출
});

writeStream.write('이 글을 씁니다.\n');
writeStream.write('한번 더 씁니다.\n');
writeStream.end(); // 데이터 다 썼다면 end 메서드로 종료 알림
// 이 때 finish 이벤트 발생
  • pipe
    createReadStream으로 파일 읽고 그 스트림 전달받아 createWriteStream으로 파일 쓸 수 있음
    파일 복사와 비슷
    스트림 끼리 연결하는 것 '파이핑한다'고 표현
const fs = require('fs');

const readStream = fs.createReadStream('readme4.txt')
const writeStream = fs.createWriteStream('writeme3.txt')
// 미리 읽기와 쓰기 스트림 만들어 둔 후
// 두개의 스트림 사이를 pope 메서드로 연결하면
// 저절로 데이터가 writeStream으로 넘어감
readStream.pipe(writeStream); 

노드 8.5 버전까지는 이 방법으로 파일 복사했다.

readFile: 전체 파일을 모두 버퍼에 저장
createReadStream: 부분으로 나눠 읽음

두 메서드의 메모리 사용량을 비교해보자

  • readFile을 사용한 경우
const fs = require('fs');

console.log('before: ', process.memoryUsage().rss);

const data1 = fs.readFileSync('./big.txt'); // 230MB 파일
fs.writeFileSync('./big2.txt', data1);
// 230MB 용량의 파일을 복사하기 위해 메모리에 파일를 모두 올려둔 후
// writeFileSync를 수행했기 때문
console.log('buffer: ', process.memoryUsage().rss);


처음에 19MB였던 메모리 용량이 순식간에 230MB가 되었다.
230MB 용량의 파일을 복사하기 위해 메모리에 파일을 모두 올려둔 후 writeFileSync를 수행했기 때문

  • 스트림 사용한 경우
const fs = require('fs');

console.log('before: ', process.memoryUsage().rss);

const readStream = fs.createReadStream('./big.txt');
const writeStream = fs.createWriteStream('./big3.txt');

readStream.pipe(writeStream);
readStream.on('end', () => {
    console.log('stream: ', process.memoryUsage().rss);
})


메모리가 27MB밖에 차지하지 않는다
큰 파일을 조각내어 작은 버퍼 단위로 옮겼기 때문

이렇게 스트림을 사용하면 효과적으로 데이터 전송 가능
동영상 같은 큰 파일 전송 시 스트림 사용

  • 기타 fs 메서드
const fs = require('fs').promises;
const constants = require('fs').constants;

// access(경로, 옵션, 콜백)
fs.access('./folder', constants.F_OK | constants.W_OK | constants.R_OK)
.then(() => {//        파일 존재 여부   읽기 권한 여부    쓰기 권한 여부
    return Promise.reject('이미 폴더 있음');
})
.catch((err) => {
    if (err.code === "ENOENT") { // 파일/폴더가 없는 경우의 에러 코드
        console.log('폴더 없음');
        return fs.mkdir('./folder') // 폴더 만드는 메서드
        // 이미 폴더 있으면 에러 발생하므로 먼저 access 호출 해 확인하는 것 중요
    }
    return Promise.reject(err);
})
.then(() => {
    console.log('폴더 만들기 성공');
    // open(경로, 옵션, 콜백)
    // 파일의 아이디(fd변수) 가져오는 메서드
    // 가져온 아이디로 fs.read, fs.write로 읽거나 쓸 수 있음
    return fs.open('./folder/file.js', 'w');
  })
.then((fd) => {
console.log('빈 파일 만들기 성공', fd);
fs.rename('./folder/file.js', './folder/newfile.js'); // 파일 이름 바꾸기
})
.then(() => {
console.log('이름 바꾸기 성공');
})
.catch((err) => {
console.error(err);
});
  • 폴더 확인 및 삭제 메서드
const fs = requirea('fs').promises;

// readdir(경로, 콜백). 폴더 안 내용물 확인
fs.readdir('./folder')
.then((dir) => {
    console.log('폴더 내용 확인', dir);
    // unlink(경로, 콜백). 파일 삭제
    return fs.unlink('./folder/newFile.js')
})
.then(() => {
    console.log('파일 삭제 성공');
    // rmdir(경로, 콜백). 폴더 삭제
    // 폴더 안에 파일 있다면 에러발생 하므로
    // 먼저 내부 파일 모두 지우고 호출해야 함
    return fs.rmdir('./folder');
})
.then(() => {
    console.log('폴더 삭제 성공')
})
.catch((err) => {
    console.error(err);
})
  • 노드 8.5 이후 복사
    createReadStream과 createWriteStream을 pipe 대신 copyFile
const fs = require('fs').promises;

fs.copyFile('readme4.txt', 'write4.txt')
.then(() => {
    console.log('복사 완료');
})
.catch((error) => {
    console.error(err);
});
  • 스레드풀
    지금까지 fs모듈의 비동기 메서드들을 사용해봤다.
    비동기 메서드들은 백그라운드에서 실행되고 여러번 실행해도 백그라운드에서 동시에 처리되는데 바로 스레드풀이 있기 때문

스레드풀의 개수가 4개이기 때문에 4개씩 묶여 실행됨

0개의 댓글