본문 바로가기
node.js

[node.js] fs.write()로 알아보는 node.js

by 박연호의 개발 블로그 2021. 9. 17.

javascript는 Netscape에서 만든 프로그래밍 언어이며 브라우저에서 버튼을 눌렀을 때 색상이 변경되거나, alert창이 뜨는 등 주로 browser에서 화면을 조작하기 위해 사용되었습니다. 하지만 2009년 node.js가 등장하면서 javascript는 단지 browser에서 화면을 조작 하는데만 그치지 않고 파일을 읽거나, network I/O, process 등 브라우저 밖에서 동작할 수 있게 되었습니다.

node.js 공식문서에서는 node.js를 다음과 같이 소개하고 있습니다.

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

여기서 runtime은 javascript가 실행되는 환경을 의미합니다. 사실 runtime, 즉 실행되는 환경이라고 함은, 이후에 배우겠지만 node.js는 V8, libuv로 구성되어 있으며 우리가 작성한 코드는 V8에 의해 실행되며, node api(libuv와 binding되어 있음)을 사용하는 것, 행위, 환경을 의미합니다.

우리가 작성한 javascript 코드를 node.js라는 비밀상자에 던져주면 안에서 어떤식으로 처리되는지는 정확히 모르지만 어찌됐건 실행이 됩니다. 즉 동작한다는 것이죠. 우리의 코드가 파일을 읽는 다면 비밀상자에서 뭐 어떤 API 로직을 타고 우리가 원하는 결과물을 보내줍니다.

node.js는 V8과 libuv로 이루어져 있으며 javascript, c++를 사용하여 구현되어 있습니다. c++은 하드웨어에 접근할 수 있는 low-level 코드이며, javascript는 node.js 모듈을 구현하는 코드이고 V8엔진에 의해 실행됩니다.


우리가 작성한 fs코드는 어디에 있을까 ?

node.js는 다양한 라이브러리를 지원합니다. 여기에서 필요한 모듈을 찾아 문서를 보고 잘사용하면 됩니다. 딱히 내부 코드가 어떻게 돌아가는지 확인할 필요는 없지만, 그래도 우리가 작성한 코드가 nodejs에 어떻게 존재 하는지 눈으로 직접 본다면 nodejs와 좀 더 친해질 것(?) 같습니다.

var fs = require('fs')

fs.writeFile('fiel.txt', data, 'utf8', function(error) {
  console.log('end')
})

fs 모듈을 사용하여 파일을 읽는 코드입니다. 이 코드를 시작으로 우리는 nodejs 실제 코드로 들어가겠습니다. ('fs') 부분에 마우스를 올리고 command + click을 하면 fs.d.ts파일로 이동하게 됩니다.

 * All file system operations have synchronous, callback, and promise-based
 * forms, and are accessible using both CommonJS syntax and ES6 Modules (ESM).
 * @see [source](https://github.com/nodejs/node/blob/v16.7.0/lib/fs.js)
 */
declare module 'fs' {
    import * as stream from 'node:stream';

그러면 아래와 같은 코드를 볼 수 있는데요, 이곳에 우리가 사용하는 fs API가 정의되어 있습니다.

node.js github의 lib폴더는 node.js에서 제공하는 모듈이 javascript로 구현되어 있으며, src 폴더는 node.js의 모듈의 실제 구현과 libuv,V8 코드가 존재합니다.

lib/fs.js파일의 line 2844의 module.exports = fs = { ...} 코드는 fs 에서 사용할 수 있는 모든 api를 export하고 있으며 덕분에 우리는 fs 메서드를 사용할 수 있습니다.

line 2090을 보면 우리가 파일을 만드는데 사용한 writeFile API가 있습니다.

function writeFile(path, data, options, callback) {
   callback = maybeCallback(callback || options)
   options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' })
   const flag = options.flag || 'w'

  if (!isArrayBufferView(data)) {
     validateStringAfterArrayBufferView(data, 'data')
     data = Buffer.from(String(data), options.encoding || 'utf8')
  }

  if (isFd(path)) {
     const isUserFd = true
     const signal = options.signal
     writeAll(path, isUserFd, data, 0, data.byteLength, signal, callback)
     return
  }

  if (checkAborted(options.signal, callback)) return

  fs.open(path, flag, options.mode, (openErr, fd) => {
     if (openErr) {
        callback(openErr)
     }else {
        const isUserFd = false
        const signal = options.signal
        writeAll(fd, isUserFd, data, 0, data.byteLength, signal, callback)
    }
  })
}

writeFile은 내부적으로 writeAll API를 사용하고 으며 writeAll은 다시 내부적으로 fs.write을 사용하고 있습니다. write API는 line 795에 구현되어 있습니다.

function write(fd, buffer, offset, length, position, callback) {
  function wrapper(err, written) {
    // Retain a reference to buffer so that it can't be GC'ed too soon.
       callback(err, written || 0, buffer);
  }

  fd = getValidatedFd(fd);

  if (isArrayBufferView(buffer)) {
       callback = maybeCallback(callback || position || length || offset);
    if (offset == null || typeof offset === 'function') {
          offset = 0;
    }else {
      validateInteger(offset, 'offset', 0);
    }

    if (typeof length !== 'number')
           length = buffer.byteLength - offset;
    if (typeof position !== 'number')
          position = null;
          validateOffsetLengthWrite(offset, length, buffer.byteLength);

      const req = new FSReqCallback();
      req.oncomplete = wrapper;
      return binding.writeBuffer(fd, buffer, offset, length, position, req);
  }

중요한 점은 함수의 return값이 binding.writeBuffer(...)이라는 것입니다. 여기서 binding은 (line 70)에 정의되어 있습니다.

const { fs: constants } = internalBinding('constants')
...
const pathModule = require('path')
const { isArrayBufferView } = require('internal/util/types')

// We need to get the statValues from the binding at the callsite since
// it's re-initialized after deserialization.

const binding = internalBinding('fs')

사실 우리가 작성하는 모든 코드는 javascript이자만 실제로 fs 작업을 수행하는 부분은 c++코드 입니다. 이런 c++ 코드는 src/*에 정의되어 있고 javascript API와 매핑되며 이렇게 javascript 코드와 c++코드를 엮어주는 부분이 위의 코드에서 binding입니다.

src/node_file.cc에 javascript에서 fs의 모든 API가 여기 파일에 정의되어 있습니다.

우리가 javascript파일에서 binding.writeBuffer을 call할 수 있었던 이유는 line 2492에서 다음과 같이 처리해주었기 때문입니다.

...
env->SetMethod(target, "writeBuffer", WriteBuffer);
...

WriteBuffer의 구현 코드는 line 1998에 정의되어 있습니다.

static void WriteBuffers(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);

  const int argc = args.Length();
  CHECK_GE(argc, 3);

  CHECK(args[0]->IsInt32());
  const int fd = args[0].As<Int32>()->Value();

  CHECK(args[1]->IsArray());
  Local<Array> chunks = args[1].As<Array>();

  int64_t pos = GetOffset(args[2]);

  MaybeStackBuffer<uv_buf_t> iovs(chunks->Length());

  for (uint32_t i = 0; i < iovs.length(); i++) {
      Local<Value> chunk = chunks->Get(env->context(), i).ToLocalChecked();
      CHECK(Buffer::HasInstance(chunk));
      iovs[i] = uv_buf_init(Buffer::Data(chunk), Buffer::Length(chunk));
  }

  FSReqBase* req_wrap_async = GetReqWrap(args, 3);

  if (req_wrap_async != nullptr) {  // writeBuffers(fd, chunks, pos, req)
        AsyncCall(env, req_wrap_async, args, "write", UTF8, AfterInteger,
        uv_fs_write, fd, *iovs, iovs.length(), pos);
  } else {  // writeBuffers(fd, chunks, pos, undefined, ctx)
        CHECK_EQ(argc, 5);
        FSReqWrapSync req_wrap_sync;
        FS_SYNC_TRACE_BEGIN(write);
        int bytesWritten = SyncCall(env, args[4], &req_wrap_sync, "write",
        uv_fs_write, fd, *iovs, iovs.length(), pos);
        FS_SYNC_TRACE_END(write, "bytesWritten", bytesWritten);
       args.GetReturnValue().Set(bytesWritten);
  }
}

그리고 실제 파일을 작성하는 libuv call은 line 1912에 있습니다.

지금까지의 과정을 다시 생각해보면, 우리가 javascript로 작성한작성한 fs.writeFile은 node/lib/fs.js에 있습니다(writeFile뿐만 아니라 fs 모듈에서 제공하는 모든 API가 이곳에 정의되어 있습니다.). writeFile API를 시작으로 writeAll -> write -> binding.writeBuffer을 호출하게 됩니다. 여기까지는 모두 javascript 부분이였고, 이후 실제 파일을 생성하는 부분은 src/node_file.cc에서 담담하게 됩니다. 마지막으로 writerBuffer 함수에서 libuv call을 함으로써 실제 파일 생성로직이 수행됩니다.

'node.js' 카테고리의 다른 글

[nodejs] nodejs와 nginx timeout  (0) 2023.03.21
[node.js] single thread and libuv  (0) 2021.09.24