Juni_Dev_log

(node.js) [Part.4] 노드의 기본 기능 알아보기 - 파일 다루기 본문

Theorem (정리)/node.js

(node.js) [Part.4] 노드의 기본 기능 알아보기 - 파일 다루기

Juni_K 2020. 12. 6. 12:25

노드의 파일 시스템은 파일을 다루는 기능과 디렉터리를 다루는 기능으로 구성되어 있으며, 동기식 IO와 비동기식 IO 기능을 함께 제공한다.

동기식 IO는 파일 작업이 끝날 때까지 대기하며, 비동기식IO는 파일 작업을 요청만 하고, 그 다음 작업을 바로 수행한다.

이후 파일 작업이 끝나면 그 상태는 이벤트로 받아서 처리한다. 동기식IO비동기식IO를 구별하기 위해서, 동기식 IO 메소드는 Sync 라는 단어를 붙인다.

동기식 방식을 사용할 때는 파일 처리가 끝날 때까지 대기하므로 처리 속도에 영향을 줄 수 있다.

파일을 읽어 들이거나 파일에 쓰기

파일을 읽어 들이는 기능을 확인하기 위해서 ch04_test5.js 파일을 만들고 다음 코드를 입력한다.

1
2
3
4
5
6
7
var fs = require('fs');
 
// 파일을 동기식 IO로 읽어들인다.
var data = fs.readFileSync('./package.json''utf8');
 
// 읽어 들인 데이터를 출력한다.
console.log(data);
cs

 

파일 시스템에 접근하기 위해서 fs 모듈을 사용한다. fs 모듈을 사용하려면 우선 require() 메소드를 호출하면서 fs 를 파라미터로 전달한다. 여기에서는 readFileSync() 메소드를 사용했는데, 메소드 이름에 Sync 라는 단어가 붙어있으므로 동기식으로 전달한다.

즉 이 코드가 실행되면 파일을 다 읽을 때까지 대기한다. 따라서 그 아래에 있는 console.log() 메소드 호출 부분은 파일을 다 읽을 때까지 실행되지 않는다.

이 파일을 실행하면 프로젝트 폴더 안에 들어있는 package.json 파일의 내용을 콘솔창에 추가한다.

 

이번에는 package.json 파일을 읽어 들여 콘솔 창에 출력하는 기능은 같지만 비동기식 IO 로 실행하는 코드를 작성해본다.

1
2
3
4
5
6
7
8
9
var fs = require('fs');
 
// 파일을 비동기식IO로 불러들인다.
fs.readFile('./package.json''utf8'function(err,data){
    // 읽어들인 데이터를 출력한다.
    console.log(data);
});
 
console.log('프로젝트 폴더 안의 package.json 파일을 읽도록 요청했습니다.');
cs

파일을 비동기식으로 읽어 들이는 방식이 노드에서 자주 사용하는 코드 패터인다.

readFile() 메소드를 실행하면서 세 번째 파라미터로 전달된 함수는 파일을 읽어 들이는 작업이 끝났을 때 호출된다.

이때 두 개의 파라미터 err 와 data 를 전달받아서 오류가 발생했는지 아니면 제대로 실행되었는지를 알 수 있다.

 

오류가 발생하면 err 에 오류 데이터가 들어가고 그렇지 않으면 err 변수의 값이 null이 된다.

그러므로 첫 번째 파라미터인 err 가 null 인지를 체크하는 코드를 사용한 후 문제가 없으면 파일 읽기에 성공한 것으로 처리한다.

 

이 파일을 실행하면 readFile() 메소드를 먼저 호출했지만 그 다음에 작성한 코드가 먼저 출력되는 것을 볼 수 있다.

프로젝트 폴더 안의 package.json 파일을 읽도록 요청했습니다.
{
"name": "node",
"version": "1.0.0",
"description": "",
"main": "calc.js",
"dependencies": {
"nconf": "^0.10.0"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

 

파일을 읽어들이는 것뿐만 아니라 파일을 쓰는 기능까지 fs 모듈에 정의한다.

파일을 일고 쓸 때 사용하는 대표적인 네 가지 메소드이다.

메소드 이름 설명
readFile(filename, [encoding], [callback]) 비동기식 IO로 파일을 읽어 들입니다.
readFileSync(filename, [encoding]) 동기식 IO로 파일을 읽어 들입니다.
writeFile(filename, data, encoding='utf8', [callback]) 비동기식 IO로 파일을 씁니다.
writeFileSync(filename, data, encoding='utf8') 동기식 IO로 파일을 씁니다.

대부분은 비동기 방식으로 읽고 쓰기 떄문에, 비동기 방식의 메소드를 주로 사용한다.

 

파일을 읽고 쓰는 메소드의 구분

이번에는 파일에 데이터를 쓰는 기능을 만들기 위해서, ch04_test7.js 파일을 만들고 다음과 같이 입력한다.

1
2
3
4
5
6
7
8
9
10
var fs = require('fs');
 
// 파일에 데이터를 씁니다.
fs.writeFile('./output.txt''Hello World!'function(err){
    if(err){
        console.log('Error : '+ err);
    }
    
    console.log('output.txt 파일에 데이터 쓰기 완료.');
});
cs

파일에 데이터를 쓸 때 사용하는 writeFile() 메소드는 첫 번째 파라미터로서 파일 이름을 전달받는다.

두 번째 파라미터는 파일에 쓸 내용이고, 세 번째는 작업이 끝나면 호출될 콜백함수이다.

작업 중 오류가 발생하면, 콜백함수로 오류 객체가 전달된다. 오류 객체가 null 값으로 전달되면 데이터 쓰기가 완료된 것이다. 

파일을 실행하면 프로젝트 폴더 안에 output.txt 파일이 만들어지고 그 안에 "Hello World!"라는 글자가 쓰여진 것을 볼 수 있다.

output.txt 파일에 데이터 쓰기 완료.

파일이 생성된 것을 볼 수 있다.

파일을 직접 열고 닫으면서 읽거나 쓰기

지금까지 살펴본 방식을 사용하면 어렵지 않게 파일을 읽거나 쓸 수 있다. 그런데, 실제로 파일을 읽거나 쓸 때는 한꺼번에 모든 데이터를 읽거나 쓰지않고 조금씩 읽거나 쓰는 방식을 사용하는 경우도 많다.

또한 다른 곳에서 받아 온 데이터를 파일에 쓰는 경우도 있기 때문에 파일을 다루는 다양한 방식이 따로 정의되어있다.

 

파일을 직접 열고 닫으면서 읽거나 쓰고 싶다면 다음 메소드들을 사용할 수 있다.

메소드 이름 설명
open(path, flags, [model], [callback]) 파일을 연다
read(fd, buffer, offset, length, position, [callback]) 지정한 부분의 파일 내용을 읽어 들인다.
write(fd, buffer, offset, length, position, [callback]) 파일의 지정한 부분에 데이터를 씁니다.
close(fd, [callback]) 파일을 닫아 줍니다.

파일의 내용을 일부분만 읽어 들이려먼 먼저 open() 메소드를 파일을 열어야 한다.

파일을 열고 데이터 쓰기 기능을 만들려면 새로운 자바스크립트 파일 ch04_test8.js를 만들고 다음 코드를 입력한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var fs = require('fs');
 
// 파일에 데이터를 쓴다.
fs.open('./output.txt''w'function(err,fd){
    if(err) throw err;
    
    var buf = new Buffer('안녕!\n');
    fs.write(fd, buf, 0, buf.lengthnullfunction(err, written, buffer){
        if(err) throw err;
            console.log(err, written, buffer);
        fs.close(fd, function(){
            console.log('파일 열고 데이터 쓰고 파일 닫기 완료.');
        });
    });
});
cs

이 코드를 보면 함수를 호출할 때마다 파라미터로 함수를 전달하고 있다.

따라서, 함수 안에 함수가 들어가는 형태가 되므로 함수 호출의 순서가 중요하다.

함수를 호출하는 순서는 'open -> write ->close' 이다.

파일을 열어 데이터를 쓴 후 파일을 다는 과정

먼저 open() 메소드를 호출하면 파일을 열 수도 있다. 그리고 파일을 열면 write() 메소드를 사용해서 데이터를 쓸 수 있다. 데이터는 필요한 만큼 Buffer 객체 안에 쓴다. 

파일에 데이터를 쓰고 나면 close() 메소드를 호출하여 파일을 닫는다. 이렇게 각 메소드를 호출할 때마다 콜백함수를 파라미터로 전달하므로 각각의 기능이 실행흘 끝냈을 때 그 다음 메소드를 실행한다.

 

파일을 열기 위해 open() 메소드를 호출할 때 동시에 세 개의 파라미터가 전달되었다. 첫 번째 파일의 이름, 파일을 읽거나 쓰기 위한 플래그이다. 대표적인 플래그로는 'r', 'w', 'w+', 'a+' 가 있다.

플래그 설명
'r' 읽기에 사용하는 플래그이다.
파일이 없으면 예외가 발생한다.
'w' 쓰기에 사용하는 플래그이다.
파일이 없으면 만들어지고 파일이 있으면 이전 내용을 모두 삭제한다.
'w+' 읽기와 쓰기에 모두 사용하는 플래그이다.
파일이 없으면 만들어지고 파일이 있으면 이전 내용을 모두 삭제한다.
'a+' 읽기와 추가에 모두 사용하는 플래그이다.
파일이 없으면 만들어지고 있으면 이전 내용에 새로운 내용을 추가한다.

코드에서는 'w' 플래그를 사용하였으므로, 쓰기 작업만을 위해 파일을 연다.

콜백 함수 안에서는 객체를 하나 만들고 '안녕!'이라는 글자를 넣은 후 write() 메소드를 호출하여 파일에 내용을 씁니다.

파일이 열리면 fd 객체를 전달받을 수 있으므로 이 fd 객체로 파일을 구별한다.

파일에 데이터를 쓸 때 어느 위치에 쓸 것인지 정할 수 있다. 코드를 모두 입력한 후 실행하면 파일이 만들어지는데, 다음과 같이 그 파일에 쓴 내용이 콘솔 창에 표시된다.

null 8
파일 열고 데이터 쓰고 파일 닫기 완료.

앞에서 만들어진 파일을 읽어 들이는 코드를 만들어 보자. 파일을 읽어 들일 때도 파일을 먼저 열고 난 후 데이터를 읽어 들인다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var fs = require('fs');
 
// 파일에서 데이터를 읽어 들인다.
fs.open('./output.txt''r',function(err,fd){
    if(err) throw err;
    
    var buf = new Buffer(10);
    console.log('버퍼 타입 : %s',Buffer.isBuffer(buf));
    
    fs.read(fd, buf, 0, buf.lengthnullfunction(err,bytesRead, buffer){
        if(err) throw err;
        
        var inStr = buffer.toString('utf8'0, bytesRead);
        console.log('파일에서 읽은 데이터 : %s',inStr);
        
        console.log(err, bytesRead, buffer);
        
        fs.close(fd,function(){
            console.log('output.txt 파일을 열고 읽기 완료.');
        });
    });
});
cs

Buffer 객체는 바이너리 데이터를 읽고 쓰는 데 사용한다.

새로운 버퍼 객체를 만들기 위해서는 new 연산자를 사용하며, 그 안에 들어갈 바이트(byte) 데이터의 크기만 지정하면 된다.

write() 메소드를 사용해 문자열을 버퍼에 쓰거나 처음부터 문자열을 사용해 버퍼 객체를 만들 수 있다.

파일을 실행하면 파일에서 읽은 데이터가 콘솔 창에 출력된다.

버퍼 타입 : true
파일에서 읽은 데이터 : 안녕!
null 8
output.txt 파일을 열고 읽기 완료.

버퍼 객체 사용하는 방법 알아보기

Buffer 객체를 사용하는 방법에 대해서 알아보자. 다음은 Buffer 객체의 기능을 확인하기 위해서 만든 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 버퍼 객체를 크기만 지정하여 만든 후, 문자열을 쓴다.
var output = '안녕1!';
var buffer1 = new Buffer(10);
var len = buffer1.write(output,'utf8');
console.log('첫 번째 버퍼의 문자열 : %s', buffer1.toString());
 
// 버퍼 객체를 문자열을 이용해 만든다.
var buffer2 = new Buffer('안녕2!''utf8');
console.log('두 번째 버퍼의 문자열 : %s', buffer2.toString());
 
// 타입을 확인한다.
console.log('버퍼 객체의 타입 : %s', Buffer.isBuffer(buffer1));
 
// 버퍼 객체에 들어 있는 문자열 데이터를 문자열 변수로 만든다.
var byteLen = Buffer.byteLength(output);
var str1 = buffer1.toString('utf8'0, byteLen);
var str2 = buffer2.toString('utf8');
 
// 첫 번째 버퍼 객체의 문자열을 두 번째 버퍼 객체로 복사한다.
buffer1.copy(buffer2, 00, len);
console.log('두 번째 버퍼에 복사한 후의 문자열 : %s',buffer2.toString('utf8'));
 
// 두 개의 버퍼를 붙여 준다.
var buffer3 = Buffer.concat([buffer1,buffer2]);
console.log('두 개의 버퍼를 붙인 후의 문자열 : %s',buffer3.toString('utf8'));
cs
첫 번째 버퍼의 문자열 : 안녕1!
두 번째 버퍼의 문자열 : 안녕2!
버퍼 객체의 타입 : true
두 번째 버퍼에 복사한 후의 문자열 : 안녕1!
두 개의 버퍼를 붙인 후의 문자열 : 안녕1!안녕1!

두 개의 버퍼를 서로 다른 방식으로 만든다.

하나는 빈 버퍼를 먼저 만들고 그 안에 문자열을 넣었으며, 다른 하나는 버퍼를 만들면서 문자열을 파라미터로 전달하였다.

이 두개의 버퍼에 대해 toString() 메소드를 호출하여 결과 문자열을 확인해보면 문자열이 똑같이 들어있는 것을 알 수 있다.

다만 크기를 먼저 지정하면 나머지 공간이 그대로 버퍼에 남아있게 되었다. 변수에 들어있는 것이 버퍼 객체인지 아닌지 확인할 때isBuffer() 메소드를 사용한다.

하나의 버퍼 객체를 다른 버퍼 객체로 복사할 때copy() 메소드를 사용하여, 두 개의 버퍼를 하나로 붙여서 새로운 버퍼 객체를 만들 때는 concat() 메소드를 사용한다.

 

버퍼에 대해서 이해했다면, 파일을 읽었을 때 콜백 함수로 전달된 버퍼를 이용해 문자열을 만드는 부분도 이해가 될 것이다.

스트림 단위로 파일 읽고 쓰기

파일을 읽거나 쓸 때는 데이터 단위가 아닌 스트림 단위로 처리할 수 있다.

스트림은 데이터가 전달되는 통로와 같은 개념이다. 파일에서 읽을 때는 createReadStream(), 파일에 쓸 때는 createWriteStream() 메소드로 스트림 객체를 만든 후 데이터를 읽고 쓰게 된다.

 

메소드 이름 설명
createReadStream() 파일을 읽기 위한 스트림 객체를 만든다.
createWriteStream() 파일을 쓰기 위한 스트림 객체를 만든다.

옵션으로는 flags, encoding, autoClose 속성이 들어있는 자바스크립트 객체를 전달할 수 있다.

output.txt 파일의 내용을 읽어 들인후, output2.txt 파일로 쓰는 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var fs = require('fs');
 
var infile = fs.createReadStream('./output.txt', {flags:'r'});
var outfile = fs.createWriteStream('./output2.txt', {flags: 'w'});
 
infile.on('data'function(data){
    console.log('읽어 들인 데이터',data);
    outfile.write(data);
});
 
infile.on('end',function(){
    console.log('파일 읽기 종료');
    outfile.end(function(){
        console.log('파일 쓰기 종료');
    });
});
 
cs
읽어 들인 데이터
파일 읽기 종료
파일 쓰기 종료

파일을 실행하면 output2.txt 가 만들어지고, 그 안에 output.txt 파일의 내용이 똑같이 들어가 있는 것을 확인할 수 있다.

 

앞서 살펴본 두 개의 스트림을 붙여주면 더 간단하게 만들 수 있다.

pipe() 메소드는 두 개의 스트림을 붙어주는 역할을 한다. ReadStream 타입의 객체와 WriteStream 타입의 객체를 붙여주면 스트림 간에 데이터를 알아서 전달한다. 위 코드를 pipe() 메소드를 활용해보도록 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var fs = require('fs');
 
var inname = './output.txt';
var outname = './output2.txt';
 
fs.exists(outname, function(exists){
    if(exists){
        fs.unlink(outname, function(err){
            if(err) throw err;
            console.log('기존 파일 ['+ outname +'] 삭제함.');
        });
    }
    var infile = fs.createReadStream(inname, {flags:'r'});
    var outfile = fs.createWriteStream(outname, {flags:'w'});
    infile.pipe(outfile);
    console.log('파일 복사 ['+ inname +'] -> ['+ outname +']');
});
 
cs
파일 복사 [./output.txt] -> [./output2.txt]
기존 파일 [./output2.txt] 삭제함.

기존에 만들어 놓은 output2.txt 가 있으면 파일이 중복될 수 있다. 그래서 같은 이름을 가진 파일을 다시 만들기 전에 먼저 이 파일을 삭제하도록 unlink() 메소드를 사용했다.

수행해보면 pipe() 메소드로 두 개의 스트림 객체를 연결하기만 했는데도 파일 내용이 복사된 것을 알 수 있다.

http 모듈로 요청받은 파일 내용을 읽고 응답하기

스트림을 서로 연결하는 방법은 웹 서버를 만들고 사용자의 요청을 처리할 때 유용하다.

이제는 http 모듈을 사용해 사용자로부터 요청을 받았을 때 파일의 내용을 읽어 응답으로 보내는 코드이다.

1
2
3
4
5
6
7
8
var fs = require('fs');
var http = require('http');
var server = http.createServer(function(req,res){
    //파일을 읽고 응답 스트림과 pipe()로 연결한다.
    var instream = fs.createReadStream('./output.txt');
    instream.pipe(res);
});
server.listen(7001'127.0.0.1');
cs

웹 서버에서 클라이언트로부터 요청을 받으면 먼저 output.txt 파일에서 스트림을 만든 후 클라이언트로 데이터를 보낼 수 있는 스트림과 연결해 준다.

http 모듈을 이용해 웹 서버를 만드는 방법은 다음에 알아볼 것이다.

지금은 파일에서 만든 스트림 객체와 웹 서버의 스트림 객체를 pipe() 메소드로 연결할 수 있다는 것만 이해하면 된다.

두 객체의 연결이 가능한 이유는 파일에서 데이터를 읽어오기 위해 만든 것도 스트림 객체이고, 데이터를 쓰기 위해 웹 서버에서 클라이언트 쪽에 만든 것도 스트림 객체이기 때문이다.

 

따라서, 읽기 스트림과 쓰기 스트림은 pipe() 메소드를 사용해서 연결할 수 있다.

pipe()를 이용해 읽기 스트림과 쓰기 스트림 연결하기

fs 모듈로 새 디렉터리 만들고 삭제하기

fs 모듈은 그 밖에도 파일과 디렉터리를 다루는 여러 가지 메소드를 포함하고 있다.

새로운 디렉터리를 만들었다가 삭제하는 코드이다.

1
2
3
4
5
6
7
8
9
10
11
var fs =require('fs');
fs.mkdir('./docs',0066function(err){
    if(err) throw err;
    console.log('새로운 doc 폴더를 만들었습니다.');
    
    fs.rmdir('./docs'function(err){
        if(err) throw err;
        console.log('docs 폴더를 삭제했습니다.')
    });
});
 
cs

파일을 실행하면 새로운 폴더가 만들어졌다가 삭제된다.

만약 삭제하는 부분의 코드를 // 기호를 이용해서 설명글로 막는다면 새로운 폴더는 만들어지지만 삭제되지는 않는다.

파일 탐색기를 열어 새로 만든 파일을 확인할 수 있다.

새로운 doc 폴더를 만들었습니다.
docs 폴더를 삭제했습니다.

파일을 처리하는 방법에 대해서 어느 정도 배웠다.

그런데 파일의 내용을 읽거나 쓰는 데 사용하는 자바스크립트 코드의 양이 많지 않음에도 불구하고 노드의 비동기 프로그래밍 방식이 콜백함수를 사용하기 때문에 코드 구조가 조금 복잡해 보일 수 있다.

또 기능을 추가함에 따라 코드의 양도 많아진다.

이 때문에 자바스크립트를 사용해 본 웹 개발자들도 노드의 코드 패턴이 익숙하지 않다고 느낀다.

 

일단 여기에서는 파일을 다루는 방식과 코드 입력 방식에 대해서 잘 알아두도록 한다.

 

Comments