프로그래밍/Node.js

Node.js 파일 시스템 정복: 파일, 디렉토리, 스트림, 버퍼까지!

shimdh 2025. 2. 19. 00:13
728x90

1. 파일 읽기 및 쓰기: 데이터 저장과 관리의 기본

Node.js에서 파일 시스템 작업은 데이터를 저장하고 관리하는 데 필수적입니다. Node.js는 비동기 I/O 모델을 사용하여 효율적으로 파일을 읽고 쓸 수 있습니다. 이는 서버 성능을 높이고 대규모 애플리케이션에서도 원활한 데이터 처리를 가능하게 합니다.

1.1. 기본 개념: 파일 읽기, 파일 쓰기

파일 시스템 작업은 크게 두 가지로 나뉩니다.

  • 파일 읽기(Reading): 기존 파일에서 데이터를 가져오는 과정입니다.
  • 파일 쓰기(Writing): 새로운 데이터를 파일에 기록하거나 기존 데이터를 수정하는 과정입니다.

Node.js에서는 fs 모듈을 사용하여 이러한 작업을 수행합니다. fs 모듈은 비동기적으로 작동하며, 콜백 함수나 프로미스를 통해 결과를 처리할 수 있습니다.

1.2. 파일 읽기: fs.readFile()

fs.readFile() 메서드를 사용하여 특정 경로에 있는 파일의 내용을 비동기적으로 읽을 수 있습니다.

1.2.1. 예제: 텍스트 파일 읽기

const fs = require('fs');

fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('파일을 읽는 중 오류가 발생했습니다:', err);
    return;
  }
  console.log('파일 내용:', data);
});

위 코드는 example.txt 파일의 내용을 UTF-8 인코딩으로 읽습니다. 오류가 발생하면 콘솔에 에러 메시지를 출력하고, 성공적으로 읽었다면 파일 내용을 출력합니다.

1.2.2. 예제: JSON 파일 읽기

const fs = require('fs');

fs.readFile('data.json', 'utf8', (err, data) => {
  if (err) {
    console.error('JSON 파일 읽기 오류:', err);
    return;
  }
  try {
    const jsonData = JSON.parse(data);
    console.log('JSON 데이터:', jsonData);
  } catch (parseError) {
    console.error('JSON 파싱 오류:', parseError);
  }
});

이 예제는 data.json 파일을 읽고 JSON 형식으로 파싱합니다. JSON.parse()를 사용하여 JSON 문자열을 JavaScript 객체로 변환합니다.

1.2.3. 예제: 파일 존재 여부 확인 후 읽기

const fs = require('fs');

const filePath = 'maybe_exists.txt';

fs.access(filePath, fs.constants.F_OK, (err) => {
  if (err) {
    console.error(`${filePath} 파일이 존재하지 않습니다.`);
    return;
  }

  fs.readFile(filePath, 'utf8', (err, data) => {
    if (err) {
      console.error('파일 읽기 오류:', err);
      return;
    }
    console.log('파일 내용:', data);
  });
});

fs.access()를 사용하여 파일이 존재하는지 먼저 확인합니다. fs.constants.F_OK는 파일 존재 여부를 확인하는 플래그입니다. 파일이 존재하면 fs.readFile()로 내용을 읽습니다.

1.3. 파일 쓰기: fs.writeFile()

fs.writeFile() 메서드를 사용하여 지정된 경로에 데이터를 쓰거나 기존 데이터를 덮어쓸 수 있습니다.

1.3.1. 예제: 텍스트 파일 생성 및 쓰기

const fs = require('fs');

const content = '안녕하세요! 이것은 Node.js에서 작성한 텍스트입니다.';

fs.writeFile('output.txt', content, (err) => {
  if (err) {
    console.error('파일을 쓰는 중 오류가 발생했습니다:', err);
    return;
  }
  console.log('output.txt에 데이터가 성공적으로 작성되었습니다.');
});

위 코드는 output.txt 파일을 생성하고 주어진 내용을 기록합니다. 성공적으로 작성되면 확인 메시지를 출력합니다.

1.3.2. 예제: JSON 데이터 파일에 쓰기

const fs = require('fs');

const jsonData = {
  name: '홍길동',
  age: 30,
  city: '서울'
};

fs.writeFile('output.json', JSON.stringify(jsonData, null, 2), (err) => {
  if (err) {
    console.error('JSON 파일 쓰기 오류:', err);
    return;
  }
  console.log('output.json에 JSON 데이터가 성공적으로 작성되었습니다.');
});

JSON.stringify()를 사용하여 JavaScript 객체를 JSON 문자열로 변환하여 파일에 씁니다. null, 2 인자는 가독성을 위해 JSON 문자열을 들여쓰기합니다.

1.3.3. 예제: 파일에 내용 추가하기

const fs = require('fs');

const additionalContent = '\n추가된 내용입니다.';

fs.appendFile('output.txt', additionalContent, (err) => {
  if (err) {
    console.error('파일에 내용 추가 중 오류 발생:', err);
    return;
  }
  console.log('output.txt에 내용이 추가되었습니다.');
});

fs.appendFile()을 사용하여 기존 파일에 내용을 추가합니다. 파일이 없으면 새로 생성합니다.

1.4. 동기 vs 비동기: fs 모듈의 두 가지 방식

위에서 설명한 fs.readFile()fs.writeFile()은 비동기적으로 작동합니다. fs 모듈은 readFileSync, writeFileSync와 같은 동기 함수도 제공합니다.

1.4.1. 비동기 함수 (예: fs.readFile)

  • 파일 읽기/쓰기 작업이 완료될 때까지 기다리지 않고 다음 코드를 실행합니다.
  • 작업이 완료되면 콜백 함수가 호출됩니다.
  • 논 블로킹(Non-blocking) 방식이기 때문에 서버의 성능을 향상시킬 수 있습니다.

1.4.2. 동기 함수 (예: fs.readFileSync)

  • 파일 읽기/쓰기 작업이 완료될 때까지 기다립니다.
  • 작업이 완료된 후에 다음 코드가 실행됩니다.
  • 블로킹(Blocking) 방식이기 때문에 대용량 파일 처리 시 성능 저하를 유발할 수 있습니다.

1.4.3. 예제: 동기 함수와 비동기 함수 비교

const fs = require('fs');

// 동기 방식
console.log('동기 방식 시작');
const data = fs.readFileSync('example.txt', 'utf8'); // 파일 읽기가 완료될 때까지 대기
console.log('동기 방식 파일 내용:', data);
console.log('동기 방식 끝');

// 비동기 방식
console.log('비동기 방식 시작');
fs.readFile('example.txt', 'utf8', (err, data) => { // 파일 읽기를 요청하고 즉시 다음 코드로 넘어감
  if (err) {
    console.error('비동기 방식 파일 읽기 오류:', err);
    return;
  }
  console.log('비동기 방식 파일 내용:', data);
});
console.log('비동기 방식 끝'); // 파일 읽기 완료 여부와 상관없이 먼저 실행됨

실행 결과:

동기 방식 시작
동기 방식 파일 내용: ... (example.txt의 내용) ...
동기 방식 끝
비동기 방식 시작
비동기 방식 끝
비동기 방식 파일 내용: ... (example.txt의 내용) ...

설명:

  • 동기 방식에서는 readFileSync가 파일 읽기를 완료할 때까지 기다리기 때문에 "동기 방식 끝"이 파일 내용보다 먼저 출력됩니다.
  • 비동기 방식에서는 readFile이 파일 읽기를 요청하고 즉시 다음 코드로 넘어가기 때문에 "비동기 방식 끝"이 먼저 출력되고, 파일 읽기가 완료된 후에 콜백 함수가 실행되어 파일 내용이 출력됩니다.

1.4.4. 예제: 동기 함수를 사용한 파일 쓰기

const fs = require('fs');

const content = '동기 방식으로 파일에 쓰기';

try {
  fs.writeFileSync('sync_output.txt', content);
  console.log('sync_output.txt에 동기 방식으로 데이터가 작성되었습니다.');
} catch (err) {
  console.error('동기 방식 파일 쓰기 오류:', err);
}

fs.writeFileSync()를 사용하여 동기적으로 파일에 데이터를 씁니다. try...catch 블록을 사용하여 오류를 처리합니다.

1.4.5. 예제: 비동기 함수를 사용한 여러 파일 순차적으로 읽기

const fs = require('fs');

const filesToRead = ['file1.txt', 'file2.txt', 'file3.txt'];

function readFileSequentially(files) {
  if (files.length === 0) {
    console.log('모든 파일 읽기 완료');
    return;
  }

  const currentFile = files.shift(); // 배열의 첫 번째 요소를 제거하고 반환

  fs.readFile(currentFile, 'utf8', (err, data) => {
    if (err) {
      console.error(`${currentFile} 읽기 오류:`, err);
    } else {
      console.log(`${currentFile} 내용:`, data);
    }
    readFileSequentially(files); // 재귀 호출로 다음 파일 읽기
  });
}

readFileSequentially(filesToRead);

이 예제는 비동기 함수를 사용하여 여러 파일을 순차적으로 읽는 방법을 보여줍니다. 재귀 함수 readFileSequentially를 사용하여 파일 목록을 순회하며 fs.readFile()로 각 파일을 읽습니다.

1.4.6. 결론: 동기 vs 비동기

  • 일반적으로 비동기 방식이 성능상 유리하지만, 코드의 흐름을 이해하기 어려울 수 있습니다.
  • 동기 방식은 코드의 흐름이 직관적이지만, 대용량 파일 처리 시 성능 저하를 유발할 수 있습니다.
  • 상황에 맞게 적절한 방식을 선택하는 것이 중요합니다.

2. 디렉토리 다루기: 파일 시스템 구조 관리

Node.js의 fs 모듈을 사용하여 디렉토리를 생성, 삭제, 읽기 및 나열할 수 있습니다. 이를 통해 파일 시스템 구조를 효과적으로 관리할 수 있습니다.

2.1. 디렉토리 생성: fs.mkdir()

fs.mkdir() 메서드를 사용하여 새로운 디렉토리를 생성합니다.

2.1.1. 예제: 새로운 디렉토리 생성

const fs = require('fs');

const dirPath = './newDirectory';

fs.mkdir(dirPath, { recursive: true }, (err) => {
  if (err) {
    console.error('디렉토리 생성 실패:', err);
  } else {
    console.log('디렉토리가 성공적으로 생성되었습니다!');
  }
});

recursive: true 옵션을 사용하면 중간 경로의 디렉토리가 없는 경우에도 한 번에 생성할 수 있습니다. 예를 들어, ./a/b/c 경로를 생성할 때 ab 디렉토리가 없어도 recursive: true 옵션을 사용하면 a, b, c 디렉토리가 모두 생성됩니다.

2.1.2. 예제: 중첩된 디렉토리 생성

const fs = require('fs');

const nestedDirPath = './level1/level2/level3';

fs.mkdir(nestedDirPath, { recursive: true }, (err) => {
  if (err) {
    console.error('중첩 디렉토리 생성 실패:', err);
  } else {
    console.log(`${nestedDirPath} 디렉토리가 성공적으로 생성되었습니다.`);
  }
});

recursive: true 옵션을 사용하여 한 번에 중첩된 디렉토리를 생성합니다.

2.1.3. 예제: 권한 설정하여 디렉토리 생성

const fs = require('fs');

const dirPath = './my_folder';
const permissions = 0o775; // read, write, and execute permissions for owner and group, read and execute for others

fs.mkdir(dirPath, { mode: permissions }, (err) => {
  if (err) {
    console.error('디렉토리 생성 실패:', err);
  } else {
    console.log(`${dirPath} 디렉토리가 성공적으로 생성되었습니다. (권한: ${permissions.toString(8)})`);
  }
});

mode 옵션을 사용하여 디렉토리 생성 시 권한을 설정할 수 있습니다. 0o775는 소유자와 그룹에게는 읽기, 쓰기, 실행 권한을, 그 외 사용자에게는 읽기, 실행 권한을 부여합니다.

2.2. 디렉토리 삭제: fs.rmdir()

fs.rmdir() 메서드를 사용하여 빈 디렉토리를 삭제할 수 있습니다.

2.2.1. 예제: 빈 디렉토리 삭제

const fs = require('fs');

const dirPath = './newDirectory';

fs.rmdir(dirPath, (err) => {
  if (err) {
    console.error('디렉토리 삭제 실패:', err);
  } else {
    console.log('디렉토리가 성공적으로 삭제되었습니다!');
  }
});

주의: fs.rmdir()은 빈 디렉토리만 삭제할 수 있습니다. 디렉토리 안에 파일이나 다른 디렉토리가 있으면 삭제할 수 없습니다.

2.2.2. 예제: 존재 여부 확인 후 디렉토리 삭제

const fs = require('fs');

const dirPath = './maybe_empty_dir';

fs.access(dirPath, fs.constants.F_OK, (err) => {
  if (err) {
    console.error(`${dirPath} 디렉토리가 존재하지 않습니다.`);
    return;
  }

  fs.rmdir(dirPath, (err) => {
    if (err) {
      console.error(`${dirPath} 디렉토리 삭제 실패:`, err);
    } else {
      console.log(`${dirPath} 디렉토리가 성공적으로 삭제되었습니다.`);
    }
  });
});

fs.access()를 사용하여 디렉토리 존재 여부를 먼저 확인하고, 존재하면 fs.rmdir()로 삭제합니다.

2.2.3. 예제: 비어 있지 않은 디렉토리 강제 삭제 (주의: 위험)

const fs = require('fs');

const dirPath = './not_empty_dir';

fs.rm(dirPath, { recursive: true, force: true }, (err) => {
  if (err) {
    console.error(`${dirPath} 디렉토리 강제 삭제 실패:`, err);
  } else {
    console.log(`${dirPath} 디렉토리가 강제로 삭제되었습니다.`);
  }
});

fs.rm() 메서드에 recursive: trueforce: true 옵션을 사용하면 비어 있지 않은 디렉토리도 강제로 삭제할 수 있습니다. 주의: 이 방법은 매우 위험하므로 신중하게 사용해야 합니다.

2.3. 디렉토리 내용 읽기: fs.readdir()

fs.readdir() 메서드를 사용하여 특정 디렉토리 내의 파일 및 하위 디렉토리 목록을 읽을 수 있습니다.

2.3.1. 예제: 디렉토리 내용 읽기

const fs = require('fs');

const dirPath = './someDirectory';

fs.readdir(dirPath, (err, files) => {
  if (err) {
    console.error('디렉토리 내용 읽기 실패:', err);
  } else {
    console.log('디렉토리 내용:', files);
  }
});

files 배열에는 dirPath 디렉토리 내의 모든 파일 및 하위 디렉토리의 이름이 문자열로 저장됩니다.

2.3.2. 예제: 디렉토리 내용과 파일 타입 정보 함께 읽기

const fs = require('fs');

const dirPath = './someDirectory';

fs.readdir(dirPath, { withFileTypes: true }, (err, dirents) => {
  if (err) {
    console.error('디렉토리 내용 읽기 실패:', err);
  } else {
    dirents.forEach((dirent) => {
      const type = dirent.isDirectory() ? 'Directory' : 'File';
      console.log(`${dirent.name} - ${type}`);
    });
  }
});

withFileTypes: true 옵션을 사용하면 파일 이름뿐만 아니라 파일 타입 정보도 함께 얻을 수 있습니다. dirent 객체의 isDirectory() 메서드를 사용하여 파일인지 디렉토리인지 확인할 수 있습니다.

2.3.3. 예제: 특정 패턴에 맞는 파일만 읽기

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

const dirPath = './someDirectory';
const pattern = /\.txt$/; // .txt로 끝나는 파일

fs.readdir(dirPath, (err, files) => {
  if (err) {
    console.error('디렉토리 내용 읽기 실패:', err);
  } else {
    const filteredFiles = files.filter((file) => pattern.test(file));
    console.log('패턴에 맞는 파일:', filteredFiles);
  }
});

path.extname()과 정규 표현식을 사용하여 특정 패턴에 맞는 파일만 필터링하여 읽을 수 있습니다. 위 예제는 .txt 확장자를 가진 파일만 읽습니다.

2.4. 비동기 처리를 위한 async/await 활용

Node.js는 비동기 프로그래밍 모델을 지원하므로, Promiseasync/await를 활용하여 코드를 간결하게 작성할 수 있습니다.

2.4.1. 예제: async/await를 사용한 디렉토리 생성, 읽기, 삭제

const fs = require('fs').promises; // fs 모듈의 프로미스 기반 함수들을 사용

async function manageDirectories() {
  try {
    const dirPath = './asyncDir';

    // 새 디렉토리 생성
    await fs.mkdir(dirPath);
    console.log(`${dirPath} 디렉토리가 생성되었습니다.`);

    // 디렉토리 내용 읽기
    const files = await fs.readdir(dirPath);
    console.log(`${dirPath} 내의 파일들:`, files);

    // 빈 디렉토리 삭제
    await fs.rmdir(dirPath);
    console.log(`${dirPath} 디렉토리가 성공적으로 삭제되었습니다.`);
  } catch (err) {
    console.error('오류 발생:', err);
  }
}

manageDirectories();

async/await를 사용하면 비동기 코드를 동기 코드처럼 작성할 수 있어 가독성이 향상되고 오류 처리가 간편해집니다.

2.4.2. 예제: async/await를 사용한 중첩 디렉토리 생성 및 파일 쓰기

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

async function createNestedDirectoryAndWriteFile() {
  try {
    const nestedDirPath = './level1/level2/level3';
    const filePath = path.join(nestedDirPath, 'example.txt');
    const content = 'Hello from nested directory!';

    // 중첩 디렉토리 생성
    await fs.mkdir(nestedDirPath, { recursive: true });
    console.log(`${nestedDirPath} 디렉토리가 생성되었습니다.`);

    // 파일 쓰기
    await fs.writeFile(filePath, content);
    console.log(`${filePath} 파일에 내용이 작성되었습니다.`);
  } catch (err) {
    console.error('오류 발생:', err);
  }
}

createNestedDirectoryAndWriteFile();

async/await를 사용하여 중첩된 디렉토리를 생성하고 그 안에 파일을 쓰는 예제입니다.

2.4.3. 예제: async/await 사용하여 디렉토리 내용 재귀적으로 읽기

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

async function readDirectoryRecursively(dirPath) {
  try {
    const entries = await fs.readdir(dirPath, { withFileTypes: true });

    for (const entry of entries) {
      const fullPath = path.join(dirPath, entry.name);
      if (entry.isDirectory()) {
        console.log(`[Directory] ${fullPath}`);
        await readDirectoryRecursively(fullPath); // 재귀 호출
      } else {
        console.log(`[File] ${fullPath}`);
      }
    }
  } catch (err) {
    console.error(`Error reading directory ${dirPath}:`, err);
  }
}

readDirectoryRecursively('./someDirectory');

async/await와 재귀 호출을 사용하여 디렉토리 내용을 재귀적으로 탐색하며 파일과 디렉토리를 구분하여 출력하는 예제입니다.

3. 스트림과 버퍼: 대용량 데이터 처리의 핵심

Node.js에서 대용량 파일을 효율적으로 처리하려면 스트림과 버퍼를 이해하는 것이 중요합니다. 스트림은 데이터를 작은 조각(chunk)으로 나누어 처리하고, 버퍼는 이러한 조각들을 임시로 저장하는 공간입니다.

3.1. 스트림(Stream)

스트림은 데이터를 연속적인 흐름으로 처리하는 방식입니다. 데이터를 작은 조각으로 나누어 처리하기 때문에 메모리 사용을 최소화하고 대용량 파일을 효율적으로 처리할 수 있습니다.

3.1.1. 스트림의 종류

Node.js는 네 가지 주요 스트림 유형을 제공합니다.

  • Readable Stream: 데이터를 읽기 위한 스트림입니다. 파일 읽기, 네트워크 요청 등이 있습니다.
  • Writable Stream: 데이터를 쓰기 위한 스트림입니다. 파일 쓰기, 네트워크 응답 등이 있습니다.
  • Duplex Stream: 읽기와 쓰기가 모두 가능한 스트림입니다. TCP 소켓 등이 있습니다.
  • Transform Stream: 데이터를 읽고 쓰는 과정에서 변형을 가하는 스트림입니다. 데이터 압축, 암호화 등이 있습니다.

3.1.2. 예제: Readable Stream 사용

const fs = require('fs');

// Readable stream 생성
const readableStream = fs.createReadStream('largefile.txt', { encoding: 'utf8' });

readableStream.on('data', (chunk) => {
  console.log(`Received chunk: ${chunk.substring(0, 50)}...`); // 첫 50글자만 출력
});

readableStream.on('end', () => {
  console.log('No more data to read.');
});

readableStream.on('error', (err) => {
  console.error('Error reading stream:', err);
});

위 코드는 largefile.txt 파일의 내용을 읽어오는 Readable Stream을 생성합니다. data 이벤트는 데이터 조각(chunk)이 도착할 때마다 발생하고, end 이벤트는 모든 데이터를 읽었을 때 발생합니다. error 이벤트는 스트림 처리 중 오류가 발생했을 때 발생합니다.

3.1.3. 예제: Readable Stream으로 한 줄씩 읽기

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

const readableStream = fs.createReadStream('largefile.txt', { encoding: 'utf8' });

const rl = readline.createInterface({
  input: readableStream,
  crlfDelay: Infinity // 모든 CR LF ('\r\n') 인스턴스를 단일 '\n'으로 처리
});

rl.on('line', (line) => {
  console.log(`Line: ${line}`);
});

rl.on('close', () => {
  console.log('Finished reading line by line.');
});

readline 모듈을 사용하여 Readable Stream에서 데이터를 한 줄씩 읽는 예제입니다.

3.1.4. 예제: Writable Stream 사용

const fs = require('fs');

const writeableStream = fs.createWriteStream('output_stream.txt');

writeableStream.write('첫 번째 줄\n');
writeableStream.write('두 번째 줄\n');
writeableStream.end('마지막 줄');

writeableStream.on('finish', () => {
    console.log("모든 데이터가 output_stream.txt에 쓰여졌습니다.")
})

fs.createWriteStream을 사용하여 output_stream.txt 파일에 데이터를 쓰는 예제입니다. write() 메서드로 데이터를 쓰고, end() 메서드로 스트림을 종료합니다. finish 이벤트는 모든 데이터가 쓰여졌을 때 발생합니다.

3.2. 버퍼(Buffer)

버퍼는 메모리의 고정된 크기 블록으로, 데이터를 임시로 저장하는 데 사용됩니다. 스트림에서 처리되는 데이터 조각(chunk)은 일반적으로 버퍼에 저장됩니다.

3.2.1. 예제: 버퍼 생성 및 사용

// 길이가 10인 빈 버퍼 생성
const buffer = Buffer.alloc(10);
console.log('빈 버퍼:', buffer);

// 문자열을 버퍼에 쓰기
buffer.write('Hello');
console.log('문자열 쓴 후:', buffer);

// 버퍼 내용을 문자열로 변환
console.log('문자열로 변환:', buffer.toString());

// 버퍼 일부만 문자열로 변환
console.log('일부만 변환:', buffer.toString('utf8', 0, 5));

실행 결과:

빈 버퍼: <Buffer 00 00 00 00 00 00 00 00 00 00>
문자열 쓴 후: <Buffer 48 65 6c 6c 6f 00 00 00 00 00>
문자열로 변환: Hello
일부만 변환: Hello

설명:

  • Buffer.alloc(10)은 10바이트 크기의 빈 버퍼를 생성합니다.
  • buffer.write('Hello')는 버퍼에 "Hello" 문자열을 씁니다.
  • buffer.toString()은 버퍼의 내용을 문자열로 변환합니다.
  • buffer.toString('utf8', 0, 5)는 버퍼의 0번째부터 5번째 바이트까지를 UTF-8 인코딩으로 문자열로 변환합니다.

3.2.2. 예제: 버퍼 병합

const buffer1 = Buffer.from('Hello');
const buffer2 = Buffer.from(' ');
const buffer3 = Buffer.from('World!');

const combinedBuffer = Buffer.concat([buffer1, buffer2, buffer3]);

console.log('병합된 버퍼:', combinedBuffer.toString());

Buffer.concat()을 사용하여 여러 버퍼를 하나로 병합하는 예제입니다.

3.2.3. 예제: 버퍼 슬라이싱

const buffer = Buffer.from('Hello World!');

const slicedBuffer = buffer.slice(0, 5); // 0번째부터 5번째 바이트 전까지 슬라이싱

console.log('원본 버퍼:', buffer.toString());
console.log('슬라이스된 버퍼:', slicedBuffer.toString());

buffer.slice()를 사용하여 버퍼의 일부분을 추출하는 예제입니다. 원본 버퍼와 슬라이스된 버퍼는 메모리 공간을 공유합니다. 따라서 슬라이스된 버퍼를 수정하면 원본 버퍼도 함께 수정됩니다.

3.3. 스트림과 버퍼를 이용한 파일 처리

스트림과 버퍼를 함께 사용하면 대용량 파일을 효율적으로 처리할 수 있습니다. 예를 들어, fs.createReadStreamfs.createWriteStream을 사용하여 파일을 복사하면, 전체 파일을 한 번에 메모리에 로드하지 않고 작은 조각으로 나누어 처리할 수 있습니다.

3.3.1. 예제: 스트림과 버퍼를 사용한 파일 복사

const fs = require('fs');

const sourceFile = fs.createReadStream('largefile.txt'); // 큰 텍스트 파일
const destFile = fs.createWriteStream('copy_largefile.txt'); // 복사할 파일 이름

sourceFile.pipe(destFile); // sourceFile에서 읽은 데이터를 destFile에 씀

destFile.on('finish', () => {
  console.log('파일 복사가 완료되었습니다.');
});

destFile.on('error', (err) => {
  console.error('파일 복사 중 오류 발생:', err);
});

pipe() 메서드는 sourceFile에서 읽은 데이터 조각(chunk)을 destFile에 자동으로 전달합니다. finish 이벤트는 모든 데이터가 destFile에 쓰여졌을 때 발생하고, error 이벤트는 복사 중 오류가 발생했을 때 발생합니다.

3.3.2. 예제: 진행률 표시와 함께 파일 복사

const fs = require('fs');
const progress = require('progress-stream'); // 진행률 표시를 위한 모듈

const sourceFile = fs.createReadStream('largefile.txt');
const destFile = fs.createWriteStream('copy_largefile.txt');

const stat = fs.statSync('largefile.txt'); // 파일 크기 정보 가져오기
const str = progress({
  length: stat.size,
  time: 100 /* ms */
});

str.on('progress', (progress) => {
  console.log(`진행률: ${progress.percentage.toFixed(2)}%`);
});

sourceFile.pipe(str).pipe(destFile);

destFile.on('finish', () => {
  console.log('파일 복사가 완료되었습니다.');
});

progress-stream 모듈을 사용하여 파일 복사 진행률을 표시하는 예제입니다. fs.statSync()를 사용하여 파일 크기 정보를 가져오고, progress() 함수를 사용하여 진행률 스트림을 생성합니다.

3.3.3. 예제: Transform Stream을 이용한 파일 압축

const fs = require('fs');
const zlib = require('zlib'); // 압축 모듈

const sourceFile = fs.createReadStream('largefile.txt');
const destFile = fs.createWriteStream('largefile.txt.gz'); // 압축된 파일

const gzip = zlib.createGzip(); // Transform Stream 생성 (압축)

sourceFile.pipe(gzip).pipe(destFile);

destFile.on('finish', () => {
  console.log('파일 압축이 완료되었습니다.');
});

zlib 모듈을 사용하여 파일을 압축하는 예제입니다. zlib.createGzip()은 데이터를 압축하는 Transform Stream을 생성합니다.

4. 결론

Node.js의 파일 시스템 모듈은 파일 읽기 및 쓰기, 디렉토리 조작, 그리고 스트림과 버퍼를 활용한 대용량 데이터 처리까지 다양한 기능을 제공합니다. 이 가이드에서 살펴본 내용들을 잘 활용한다면 Node.js를 이용한 서버 사이드 애플리케이션 개발 시 파일 시스템 작업을 효율적이고 안정적으로 수행할 수 있을 것입니다. 더 나아가, Node.js의 비동기 I/O 모델과 스트림, 버퍼의 개념을 이해하면 서버 성능을 최적화하고 대규모 애플리케이션을 구축하는 데 큰 도움이 될 것입니다.

728x90