5.4) HandyFileSystem
Runner는 어셈블리 코드를 번역한 파일을 내놓는다. 따라서 파일 시스템에 접근해야 한다. 그런데 서문에서 브라우저에 내장된 JavaScript를 이용해서는 파일 시스템에 접근할 수 없다는 사실을 이미 밝혔다. 따라서 우리는 파일 시스템에 접근할 수 있도록 Chrome 브라우저 대신 node.js 프로그램을 이용하여 개발을 진행해야 한다. brackets는 편한 개발도구이므로 그대로 사용하지만, 사용 방법은 약간 변한다. 이에 대해서는 블로그에 brackets에서 nw.js를 사용하는 방법을 설명하는 포스트2를 작성했으니 참고하기 바란다.
이 절에서는 파일 시스템에 접근하기 위한 래퍼 객체인 HandyFileSystem 싱글톤 객체를 작성한다. 이는 자주 사용하는 객체이므로 handy.js 파일에 구현할 것이다. 그런데 이를 만드는 방법이 이전과 약간 다르니 주의해서 코드를 봐야 한다.
먼저 HandyFileSystem 객체를 초기화하는 initHandyFileSystem 함수를 부분을 나눠서 보자.
handy.js (initHandyFileSystem) |
/** HandyFileSystem 싱글톤 객체를 생성하고 초기화합니다. */ function initHandyFileSystem() {
// HandyFileSystem 싱글톤 객체를 정의합니다. // 빈 객체를 먼저 생성함에 주의합니다. var hfs = {};
... // HandyFileSystem 객체의 속성을 구현합니다.
// 전역 객체를 의미하는 window 객체에 HandyFileSystem 속성을 추가하고 // 생성한 HandyFileSystem 싱글톤 객체를 대입합니다. window['HandyFileSystem'] = hfs; } |
HandyFileSystem이라는 객체를 정의한다면서 var HandyFileSystem = { ... };과 같은 식이나 생성자 함수를 정의하지 않고, initHandyFileSystem이라는 함수를 만든 다음 함수 내에 지역 변수로 빈 객체를 생성했다. 그리고 함수의 마지막 부분에 window[‘HandyFileSystem'] = hfs;라는 이상한 문장을 수행한다. 이것이 어떻게 싱글톤 객체의 정의가 될 수 있을까?
이 이야기를 진행하기 전에 아직 설명하지 않은 window라는 녀석에 대해 먼저 설명해야겠다. JS에는 문서가 기본적으로 지원하는 객체가 있다. textarea 요소에 접근하기 위해 document.getElementById라는 메서드를 호출했던 것을 기억하는가? 이때 getElementById는 document라는, JS가 기본으로 제공하는 객체가 가진 메서드다. 이런 기본 객체는 window, document 말고도 여러 가지가 있지만 이에 대해서 이 문서에서 자세히 다루지는 않겠다.중요한 건 window와 document다.
window는 JS 코드 내에서 생성되는 모든 객체에 대한 기본 객체다. Object는 모든 객체에 대한 기본 형식이라고 하면, window는 생성한 객체에 대한 기본 영역이 된다. 여전히 말을 이해하지 못하겠다면 다음의 예제를 보라.
window.htm <html.head.script> |
// main 함수의 바깥에서 num과 func를 정의합니다. // 이때 num과 func가 정의된 영역을 '전역'이라고 하면, var num = 10; function func() { log('testfunction'); }
function main() { // 전역에 정의된 멤버는 모두 window 객체의 속성이 됩니다. log(window.num); log(window.func);
// 특별히 소속이 없는 모든 속성과 메서드는 window 객체의 속성입니다. window.alert('hello, world!');
// window 객체의 멤버처럼 alert 메서드를 호출할 수 있습니다. window['alert']('this is also possible'); } |
실행 결과 (순서대로 로그, 경고1, 경고2) |
10 function func() { log('testfunction'); } |
hello, world! |
this is also possible |
이와 같이 전역에 정의된 임의의 변수, 함수 또는 객체는 모두 window 객체의 속성이 된다. 사실 전역에 변수나 함수, 객체를 정의하는 행위는 다음을 수행하는 것과 같다.
window.num = 10;
window['func'] = function() { /* */ };
window['person'] = { name:'handy', age:20 };
따라서 이러한 특징을 이용하여 HandyFileSystem 속성을 window 객체에 추가하였다. 이에 따라 우리는 임의의 위치에서 HandyFileSystem에 접근하는 것이 가능해졌다. 잠시 후에 이에 대한 예제를 보일 것이다.
이제 HandyFileSystem 객체의 구현을 보자.
handy.js (initHandyFileSystem) |
function initHandyFileSystem() { ... var hfs = {};
// 파일 시스템 객체를 획득합니다. var fso = require('fs');
// 현재 작업중인 파일의 디렉터리를 획득합니다. var gui = require('nw.gui'); var dir = gui.App.argv[0];
...
// 정의한 속성을 HandyFileSystem 싱글톤 객체의 멤버로 정의합니다. hfs.fso = fso; hfs.dir = dir; ... } |
이 부분에서는 fso, gui, dir이라는 세 변수를 생성하였다. 이들에 대해 간단히 설명하겠다.
fso는 파일 시스템에 접근하기 위해 필요한 모듈이다. gui는 현재 경로를 얻기 위해 gui.App.argv 속성에 접근해야 해서 가져온 모듈이다. 그 외의 용도로는 사용하지 않는데, 이에 대해 자세한 내용을 알고 싶다면 필자의 블로그3를 참조하라. 이를 이용해 얻은 경로는 dir 변수에 저장한다.
그리고 마지막에 hfs.fso = fso;와 같이 값을 대입하는 식이 나오는데, 이는 기존 객체에 멤버를 추가하는 것이다. 6장에서 이에 대해 설명했으니 기억이 나지 않는다면 다시 찾아보길 바란다.
마지막으로 HandyFileSystem의 메서드를 설명하겠다. 먼저 load 함수를 보자.
handy.js (initHandyFileSystem) |
function initHandyFileSystem() { ...
/** // 파일에 기록된 텍스트를 모두 불러와 문자열로 반환합니다. // 성공하면 획득한 문자열, 실패하면 null을 반환합니다. @param {string} filename @return {string} */ function load(filename) { try { var filepath = this.dir + '\\' + filename; return this.fso.readFileSync(filepath); } catch (ex) { return null; } }
...
// 정의한 속성을 HandyFileSystem 싱글톤 객체의 멤버로 정의합니다. ... hfs.load = load; ... } |
JavaScript에서는 함수 내부에 함수를 정의하는 것이 가능하므로 load 함수를 정의했다. 중요한 특징인데, JavaScript에서 함수 내부에서, 즉 지역에서 함수를 정의한다고 전역에서도 바로 이 함수를 호출할 수는 없다. 지역에서 정의된 함수는 지역적인 성질을 가진다. 이는 다음의 예제에서 확인할 수 있다.
localfunc.htm |
// 전역 함수 gfunc를 정의합니다. function gfunc() { // 지역적인 함수 lfunc를 정의합니다. function lfunc() { alert('hello, world!'); }
// lfunc의 내용이 잘 출력됩니다. log(lfunc); // lfunc가 전역에 정의되지 않았으므로 undefined가 출력됩니다. log(window.lfunc); }
function main() { gfunc(); } |
실행 결과 |
function lfunc() { alert('hello, world!'); } undefined |
load 함수 내부는 fso 객체의 메서드 readFileSync를 이용하여 파일에 있는 모든 텍스트를 가져온 다음 문자열로 반환하는 단순한 코드로 이루어져있다. 실패하면 null을 반환한다. 마지막으로 hfs의 load 속성을 우리가 정의한 지역 함수 load로 맞춘다. 이게 끝이다.
나머지는 방식이 완전히 똑같으므로 코드만 보이겠다.
handy.js (initHandyFileSystem) |
function initHandyFileSystem() { ...
/** // 파일에 텍스트를 기록합니다. // 기록에 성공하면 true, 실패하면 false를 반환합니다. @param {string} filename @return {Boolean} */ function save(filename, data) { try { var filepath = this.dir + '\\' + filename; this.fso.writeFileSync(filepath, data); return true; } catch (ex) { return false; } } /** // 파일이 디스크에 존재하는지 확인합니다. @param {string} filepath @return {Boolean} */ function exists(filepath) { return this.fso.exists(filepath); }
...
// 정의한 속성을 HandyFileSystem 싱글톤 객체의 멤버로 정의합니다. ... hfs.save = save; hfs.exists = exists; ... } |
결국 initHandyFileSystem은 다음과 같은 식으로 구성되어있다.
handy.js (initHandyFileSystem) |
function initHandyFileSystem() { // 1. 빈 객체 생성 var hfs = {};
// 2. 필드와 메서드 정의 var fso = ..., dir = ...; function load(...) { ... } function save(...) { ... } function exists(...) { ... }
// 3. 빈 객체에 속성 추가 hfs.fso = fso; hfs.dir = dir; hfs.load = load; hfs.save = save; hfs.exists = exists;
// 4. 전역에 작성한 hfs 객체 등록 window['HandyFileSystem'] = hfs; } |
앞으로 만들 모든 싱글톤 객체는 거의 이런 식으로 구성할 것이므로, 이 형태에 익숙해지기 바란다. 이로써HandyFileSystem에 대한 설명이 끝났다. 다음은 이를 활용하는 코드다.
main.html <html.head.script> |
// HandyFileSystem demonstration function main() {
// HandyFileSystem 객체를 가리키는 변수입니다. var hfs = HandyFileSystem;
// 파일에 문장을 기록하는 예제입니다. var filename = 'handymakesman.txt'; var text = prompt('Enter text to write', 'hello, world!'); if (hfs.save(filename, text) == false) { log('failed'); } else { log('complete'); }
// 파일에 기록된 텍스트를 불러오는 예제입니다. var text = hfs.load(filename); if (text) { log('succeed: ' + text); } else { log('failed to load'); } } function init() { var logStream = document.getElementById('HandyLogStream'); logStream.style.width = '100%'; logStream.style.height = '100%'; initHandyFileSystem(); } |
실행 결과 |
complete hello, world! |
이와 같이 HandyFileSystem을 정의할 수 있었다.