Programming/JavaScript

[EloquentJS] Ch20. Node.js

dododoo 2020. 5. 24. 15:40

Node.js

  • Node.js, a program that allows you to apply your JavaScript skills outside of the browser. With it, you can build anything from small command line tools to HTTP servers that power dynamic websites.
  • These chapters aim to teach you the main concepts that Node.js uses and to give you enough information to write useful programs for it.

Background

  • One of the more difficult problems with writing systems that communicate over the network is managing input and output—that is, the reading and writing of data to and from the network and hard drive. Moving data around takes time, and scheduling it cleverly can make a big difference in how quickly a system responds to the user or to network requests.

    네트워크를 통해 커뮤니케이션 하는 시스템을 만드는 것을 더 어렵게 하는 것 중 하나는 입력과 출력을 관리하는 것이다(네트워크/디스크로 부터/에 읽기/쓰기). 데이터를 움직이는 시간을 잘 스케쥴링하면 시스템이 사용자나 네트워크 리퀘스트에 훨씬 빠르게 응답할 수도 있다.

  • In such programs, asynchronous programming is often helpful. It allows the program to send and receive data from and to multiple devices at the same time without complicated thread management and synchronization.

    비동기 프로그래밍이 유용할 수 있다. (cf. 복잡한 스레드 관리와 동기화)

  • Node was initially conceived for the purpose of making asynchronous programming easy and convenient. JavaScript lends itself well to a system like Node. It is one of the few programming languages that does not have a built-in way to do in- and output. Thus, JavaScript could be fit onto Node’s rather eccentric approach to in- and output without ending up with two inconsistent interfaces.

    노드는 비동기 프로그래밍을 쉽고 편리하게 만들기 위해 고안되었다. 자바스크립트는 노드 같은 시스템에 아주 적합하다. (입/출력...)

The node command

  • When Node.js is installed on a system, it provides a program called node, which is used to run JavaScript files.
  • The console.log method in Node does something similar to what it does in the browser. It prints out a piece of text. But in Node, the text will go to the process’s standard output stream, rather than to a browser’s JavaScript console.
  • If you run node without giving it a file, it provides you with a prompt at which you can type JavaScript code and immediately see the result.
  • $ node
    > 1 + 1
    2
    > [-1, -2, -3].map(Math.abs)
    [1, 2, 3]
    > process.exit(0)
    $
  • The process binding, just like the console binding, is available globally in Node. It provides various ways to inspect and manipulate the current program. The exit method ends the process and can be given an exit status code, which tells the program that started node (in this case, the command line shell) whether the program completed successfully (code zero) or encountered an error (any other code).

    console처럼 전역적으로 사용 가능한 process 바인딩은 현재 프로그램을 점검하거나 조작하는 방법들을 제공한다. 그 중, exit 메소드는 프로세스를 끝내고 인자로 받은 종료 상태 코드를 node를 시작한 프로그램에게 전달한다.

  • To find the command line arguments given to your script, you can read process.argv, which is an array of strings. Note that it also includes the name of the node command and your script name, so the actual arguments start at index 2. If showargv.js contains the statement console.log(process.argv), you could run it like this:
  • $ node showargv.js one --and two
    ["node", "/tmp/showargv.js", "one", "--and", "two"]

    커맨드 라인 인자: process.argv로 문자열 배열에 접근할 수 있다.

  • All the standard JavaScript global bindings, such as Array, Math, and JSON, are also present in Node’s environment. Browser-related functionality, such as document or prompt, is not.

    노드에서도 자바스크립트의 표준 전역 바인딩은 모두 존재하지만, 브라우저와 관련된 기능들은 존재하지 않는다.

Modules

  • Beyond the bindings I mentioned, such as console and process, Node puts few additional bindings in the global scope. If you want to access built-in functionality, you have to ask the module system for it.

    노드는 이 외에도 전역 스코프에서 여러 바인딩들을 제공한다. 만약 내장 기능들을 사용하고 싶다면 모듈 시스템에 요청해야 한다.

  • The CommonJS module system is built into Node and is used to load anything from built-in modules to downloaded packages to files that are part of your own program.

    CommonJS 모듈 시스템을 사용하며, 이는 내장 모듈부터 다운로드된 패키지, 프로그램에 포함된 파일들을 불러오기 위해 사용된다.

  • When require is called, Node has to resolve the given string to an actual file that it can load. Pathnames that start with /, ./, or ../ are resolved relative to the current module’s path, where . stands for the current directory, ../ for one directory up, and / for the root of the file system. So if you ask for "./graph" from the file /tmp/robot/robot.js, Node will try to load the file /tmp/robot/graph.js.

    /: 파일 시스템의 루트

  • The .js extension may be omitted, and Node will add it if such a file exists. If the required path refers to a directory, Node will try to load the file named index.js in that directory.

    .js 확장자는 생략될 수 있는데, 이 경우에 해당 파일이 존재한다면 노드는 확장자를 알아서 붙일 것이다. 만약 경로가 디렉터리라면 노드는 해당 디렉터리 내의 index.js파일을 로드하려고 할 것이다.

  • When a string that does not look like a relative or absolute path is given to require, it is assumed to refer to either a built-in module or a module installed in a node_modules directory.

    require에 절대경로, 상대경로가 아닌 문자열이 주어진다면, 이는 내장 모듈이나 node_modules에 설치된 모듈을 참조하는 것으로 여겨진다.

  • For example, require("fs") will give you Node’s built-in file system module. And require("robot") might try to load the library found in node_modules/robot/. A common way to install such libraries is by using NPM.

  • Let’s set up a small project consisting of two files.

  • /* main.js */
    const {reverse} = require("./reverse");
    
    // Index 2 holds the first actual command line argument
    let argument = process.argv[2];
    
    console.log(reverse(argument));
  • /* reverse.js */
    exports.reverse = function(string) {
        return Array.from(string).reverse().join("");
    };
  • Remember that adding properties to exports adds them to the interface of the module. Since Node.js treats files as CommonJS modules, main.js can take the exported reverse function from reverse.js.

  • $ node main.js JavaScript
    tpircSavaJ

Installing with NPM

  • NPM is an online repository of JavaScript modules, many of which are specifically written for Node. When you install Node on your computer, you also get the npm command, which you can use to interact with this repository.
  • ... After running npm install, NPM will have created a directory called node_modules. Inside that directory will be an ini directory that contains the library. You can open it and look at the code. When we call require("ini"), this library is loaded, and we can call its parse property to parse a configuration file.

    node_modules

  • By default NPM installs packages under the current directory, rather than in a central place. If you are used to other package managers, this may seem unusual, but it has advantages—it puts each application in full control of the packages it installs and makes it easier to manage versions and clean up when removing an application.

    기본적으로 NPM은 어떤 중앙의 장소가 아니라 현재 디렉터리에 패키지들을 설치한다. 이는 각 애플리케이션을 그것이 설치한 패키지들의 관리하에 두고, 패키지의 버전 관리, 그리고 애플리케이션을 지울 때 정리하는 것을 더 쉽게 만들어 준다.

Package files

  • In the npm install example, you could see a warning about the fact that the package.json file did not exist. It is recommended to create such a file for each project, either manually or by running npm init. It contains some information about the project, such as its name and version, and lists its dependencies.

    npm install할 때, package.json이 존재하지 않는다는 경고를 볼 수 있다. 각 프로젝트 마다 수동으로든 npm init 명령어를 사용하든 package.json 파일을 만드는 것이 추천된다. 이 파일은 프로젝트의 이름, 버전, 의존성과 같은 프로젝트에 대한 정보를 포함한다.

  • The robot simulation from Chapter 7, as modularized in the exercise in Chapter 10, might have a package.json file like this:
  • {
        "author": "Marijn Haverbeke",
        "name": "eloquent-javascript-robot",
        "description": "Simulation of a package-delivery robot",
        "version": "1.0.0",
        "main": "run.js",
        "dependencies": {
            "dijkstrajs": "^1.0.1",
            "random-item": "^1.0.0"
        },
        "license": "ISC"
    }
  • When you run npm install without naming a package to install, NPM will install the dependencies listed in package.json. When you install a specific package that is not already listed as a dependency, NPM will add it to package.json.

    패키지 이름 없이 npm install을 사용하면 package.json의 의존성들이 설치될 것이다. 또한, 의존성에 포함되지 않는 어떤 패키지를 설치한다면, NPM은 이 패키지를 package.json에 추가할 것이다.

Versions

  • A package.json file lists both the program’s own version and versions for its dependencies. Versions are a way to deal with the fact that packages evolve separately, and code written to work with a package as it existed at one point may not work with a later, modified version of the package.
  • NPM demands that its packages follow a schema called semantic versioning, which encodes some information about which versions are compatible (don’t break the old interface) in the version number. A semantic version consists of three numbers, separated by periods, such as 2.3.0. Every time new functionality is added, the middle number has to be incremented. Every time compatibility is broken, so that existing code that uses the package might not work with the new version, the first number has to be incremented.

    semantic versioning

  • A caret character (^) in front of the version number for a dependency in package.json indicates that any version compatible with the given number may be installed. So, for example, "^2.3.0" would mean that any version greater than or equal to 2.3.0 and less than 3.0.0 is allowed.

    caret character (^)

  • The npm command is also used to publish new packages or new versions of packages. If you run npm publish in a directory that has a package.json file, it will publish a package with the name and version listed in the JSON file to the registry.

    npm publish

  • Another program, yarn, which can be installed from the NPM registry, fills the same role as npm using a somewhat different interface and installation strategy.

The file system module

  • One of the most commonly used built-in modules in Node is the fs module, which stands for file system. It exports functions for working with files and directories.
  • For example, the function called readFile reads a file and then calls a callback with the file’s contents.
  • let {readFile} = require("fs");
    readFile("file.txt", "utf8", (error, text) => {
        if (error) throw error;
        console.log("The file contains:", text);
    });
  • The second argument to readFile indicates the character encoding used to decode the file into a string. There are several ways in which text can be encoded to binary data, but most modern systems use UTF-8. So unless you have reasons to believe another encoding is used, pass "utf8" when reading a text file.

    readFile의 두 번째 인자는 파일을 문자열로 복호화할 때 사용되는 문자 인코딩을 나타낸다. 대부분의 모던 시스템들은 UTF-8을 사용하므로 다른 인코딩 방식이 사용되었다고 판단할 이유가 없다면 "utf8"을 사용해서 텍스트 파일을 읽어라.

  • If you do not pass an encoding, Node will assume you are interested in the binary data and will give you a Buffer object instead of a string. This is an array-like object that contains numbers representing the bytes (8-bit chunks of data) in the files.

    만약 인코딩 방식을 명시하지 않는다면 Buffer 객체를 얻을 것이다. 이 객체는 배열과 비슷한 객체로 바이트 단위를 나타내는 숫자를 포함한다.

  • const {readFile} = require("fs");
    readFile("file.txt", (error, buffer) => {
        if (error) throw error;
        console.log("The file contained", buffer.length, "bytes.",
                    "The first byte is:", buffer[0]);
    });
  • A similar function, writeFile, is used to write a file to disk.
  • const {writeFile} = require("fs");
    writeFile("graffiti.txt", "Node was here", err => {
        if (err) console.log(`Filed to write file ${err}`);
        else console.log("File written");
    });
  • Here it was not necessary to specify the encoding—writeFile will assume that when it is given a string to write, rather than a Buffer object, it should write it out as text using its default character encoding, which is UTF-8.

    writeFileBuffer 객체가 아니라 문자열이 주어진다면 기본 문자 인코딩 방식인 UTF-8로 텍스트를 적는다.

  • The fs module contains many other useful functions: readdir will return the files in a directory as an array of strings, stat will retrieve information about a file, rename will rename a file, unlink will remove one, and so on.

    readdir, stat, rename, unlink (https://nodejs.org)

  • Most of these take a callback function as the last parameter, which they call either with an error (the first argument) or with a successful result (the second). As we saw in Chapter 11, there are downsides to this style of programming—the biggest one being that error handling becomes verbose and error-prone.

    콜백 함수: 에레 처리가 장황하고 오류가 발생하기 쉽다.

  • At the time of writing their integration into Node.js is still a work in progress. There is an object promises exported from the fs package since version 10.1 that contains most of the same functions as fs but uses promises rather than callback functions.

    promises: 콜백 대신 프로미스를 사용하는 것 말고는 fs와 동일한 함수들을 대부분 제공한다.

  • const {readFile} = require("fs").promises;
    readFile("file.txt", "utf8")
        .then(text => console.log("The file contains:", text));
  • Sometimes you don’t need asynchronicity, and it just gets in the way. Many of the functions in fs also have a synchronous variant, which has the same name with Sync added to the end.

    때때로 비동기성은 오히려 방해가 될 때가 있다. fs의 대부분의 함수는 동기적인 버전을 갖는데, 이는 원래 이름에 Sync를 붙인 것이다.

  • const {readFileSync} = require("fs");
    console.log("The file contains:", 
                readFileSync("file.txt", "utf8"));
  • Do note that while such a synchronous operation is being performed, your program is stopped entirely.

The HTTP module

  • Another central module is called http. It provides functionality for running HTTP servers and making HTTP requests.

    또 다른 주요한 모듈로 http가 있다. 이 모듈은 HTTP 서버를 작동시키는 것과 HTTP 리퀘스트를 만드는 기능들을 제공한다.

  • const {createServer} = require("http");
    let server = createServer((request, response) => {
        response.writeHead(200, {"Content-Type": "text/html"});
        response.write(`
            <h1>Hello!<h1>
            <p>You asked for <code>${request.url}</code></p>`);
        response.end();
    });
    server.listen(8000);
    console.log("Listening! (port 8000)");
  • The function passed as argument to createServer is called every time a client connects to the server. The request and response bindings are objects representing the incoming and outgoing data. The first contains information about the request, such as its url property, which tells us to what URL the request was made.

    createServer가 인자로 받는 함수는 클라이언트가 서버에 연결할 때 매번 호출된다. requestresponse 바인딩은 각각 들어오는 데이터와 나가는 데이터를 나타내는 객체이다.

  • To send something back, you call methods on the response object. The first, writeHead, will write out the response headers. You give it the status code (200 for “OK” in this case) and an object that contains header values. The example sets the Content-Type header to inform the client that we’ll be sending back an HTML document.

    무언가를 응답하고 싶다면 response 객체에 메소드를 호출하면 된다. writeHead는 리스폰스 헤더를 쓰는 메소드로, 상태 코드와 헤더 값들을 가진 객체를 받는다. 예시는 클라이언트에게 HTML 문서를 보낸다는 것을 알리기 위해 Content-Type 헤더를 설정했다.

  • Next, the actual response body (the document itself) is sent with response.write. You are allowed to call this method multiple times if you want to send the response piece by piece, for example to stream data to the client as it becomes available. Finally, response.end signals the end of the response.

    response.write를 사용하여 리스폰스 바디를 보낼 수 있다. 이는 여러 번 호출이 가능하다. response.end를 호출하여 리스폰스가 끝났음을 알린다.

  • The call to server.listen causes the server to start waiting for connections on port 8000.

    server.listen

  • When you run this script, the process just sits there and waits. When a script is listening for events—in this case, network connections—node will not automatically exit when it reaches the end of the script.

    스크립트가 이벤트를 듣고(listening) 있을때, node는 스크립트의 끝에 도달했어도 자동적으로 끝나지 않는다.

  • A real web server usually does more than the one in the example—it looks at the request’s method (the method property) to see what action the client is trying to perform and looks at the request’s URL to find out which resource this action is being performed on.

    실제 웹 서버는 예제의 서버보다 더 많은 일을 한다: method를 확인하여 어떤 액션이 수행되는 것인지 보거나, URL을 확인하여 어떤 리소스에 이 액션이 수행되는지를 확인하는 등

  • To act as an HTTP client, we can use the request function in the http module.

    HTTP 클라이언트의 역할을 하려면 http 모듈의 request 함수를 사용할 수 있다.

  • const {request} = require("http");
    let requestStream = request({
        hostname: "eloquentjavascript.net",
        path: "/20_node.html",
        method: "GET",
        headers: {Accept: "text/html"}
    }, response => {
        console.log("Server responded with status code", 
                    response.statusCode);
    });
    requestStream.end();
  • The first argument to request configures the request, telling Node what server to talk to, what path to request from that server, which method to use, and so on. The second argument is the function that should be called when a response comes in. It is given an object that allows us to inspect the response, for example to find out its status code.

    request의 첫 번째 인자는 서버, 경로, 메소드 등을 노드에게 전달하며 리퀘스트를 설정한다. 두 번재 인자는 리스폰스가 들어왔을 때 호출될 함수를 명시한다. 이때, 리스폰스를 조사할 수 있는 객체가 인자로 주어진다.

  • Just like the response object we saw in the server, the object returned by request allows us to stream data into the request with the write method and finish the request with the end method. The example does not use write because GET requests should not contain data in their request body.

    서버에서 봤던 response 객체처럼, request에 의해 반환되는 객체는 write 메소드를 사용하여 데이터를 리퀘스트에 더할 수 있고, end 메소르를 사용하여 리퀘스트를 끝낼 수 있다. (GET 리퀘스트는 바디를 포함하지 않으므로 예제에서는 사용되지 않았다.)

  • There’s a similar request function in the https module that can be used to make requests to https: URLs.

    https: URL에 리퀘스트하기 위한 https 모듈의 request 함수도 이와 비슷하다.

  • Making requests with Node’s raw functionality is rather verbose. There are much more convenient wrapper packages available on NPM. For example, node-fetch provides the promise-based fetch interface that we know from the browser.

    노드의 기본 기능을 이용하여 리퀘스트를 만드는 건 꽤나 장황한 일이다. NPM에는 훨씬 더 편리한 래퍼 패키지들이 존재한다. 예를 들어, 브라우저에서 사용하는 프로미스 기반의 fetch 인터페이스를 제공하는 node-fetch 패키지가 있다.

Streams

  • We have seen two instances of writable streams in the HTTP examples—namely, the response object that the server could write to and the request object that was returned from request.

    스트림; 리스폰스 객체와 리퀘스트 객체

  • Writable streams are a widely used concept in Node. Such objects have a write method that can be passed a string or a Buffer object to write something to the stream. Their end method closes the stream and optionally takes a value to write to the stream before closing. Both of these methods can also be given a callback as an additional argument, which they will call when the writing or closing has finished.

    쓸 수 있는 스트림은 노드에서 널리 사용되는 개념이다. 그러한 객체들은 write 메소드를 통해 문자열이나 Buffer 객체를 스트림에 쓸 수 있다. end 메소드는 스트림을 닫는데 닫기 전에 쓸 값을 선택적으로 받는다. 이러한 메소드들은 콜백 함수를 추가로 받을 수 있는데, 이 콜백들은 쓰기나 닫기가 완료되었을 때 호출된다.

  • It is possible to create a writable stream that points at a file with the createWriteStream function from the fs module. Then you can use the write method on the resulting object to write the file one piece at a time, rather than in one shot as with writeFile.

    fs 모듈의 createWriteStream은 파일을 가리키는 스트림을 생성한다. (cf. writeFile)

  • Readable streams are a little more involved. Both the request binding that was passed to the HTTP server’s callback and the response binding passed to the HTTP client’s callback are readable streams.
  • Reading from a stream is done using event handlers, rather than methods.

    스트림에서 읽는 것은 메소드가 아니라 이벤트 핸들러를 사용해서 가능하다.

  • Objects that emit events in Node have a method called on that is similar to the addEventListener method in the browser. You give it an event name and then a function, and it will register that function to be called whenever the given event occurs.

    노드의 on 메소드는 브라우저의 addEventListener와 비슷하다.

  • Readable streams have "data" and "end" events. The first is fired every time data comes in, and the second is called whenever the stream is at its end. This model is most suited for streaming data that can be immediately processed, even when the whole document isn’t available yet. A file can be read as a readable stream by using the createReadStream function from fs.

    읽을 수 있는 스트림은 "data""end" 이벤트를 갖는다. "data"는 데이터가 들어올 때마다 발생하고 "end"는 스트림이 끝날 때마다 호출된다. 이 모델은 즉각적으로 처리될 수 있는 스트리밍 데이터에 가장 적합하다. 파일은 fscreateReadStream을 사용해서 스트림으로 읽힐 수 있다.

  • const {createServer} = require("http");
    createServer((request, response) => {
        response.writeHead(200, {"Content-Type": "text/plain"});
        request.on("data", chuck => {
            response.write(chuck.toString().toUpperCase());
        });
        request.on("end", () => response.end());
    }).listen(8000);
  • The chunk value passed to the data handler will be a binary Buffer. We can convert this to a string by decoding it as UTF-8 encoded characters with its toString method.

    data 핸들러에 전달되는 chuck는 이진 Buffer인데, 이는 BuffertoString 메소드를 사용해서 UTF-8 문자들로 복호화된 문자열로 바꿀 수 있다.

  • The following piece of code, when run with the uppercasing server active, will send a request to that server and write out the response it gets:
  • const {request} = require("http");
    request({
        hostname: "localhost",
        port: 8000,
        method: "POST"
    }, response => {
        response.on("data", chuck => {
            process.stdout.write(chuck.toString());
        });
    }).end("Hello server");
  • The example writes to process.stdout (the process’s standard output, which is a writable stream) instead of using console.log. We can’t use console.log because it adds an extra newline character after each piece of text that it writes, which isn’t appropriate here since the response may come in as multiple chunks.

    쓸 수 있는 스트림인 process.stdout

A file server

  • Let’s combine our newfound knowledge about HTTP servers and working with the file system to create a bridge between the two: an HTTP server that allows remote access to a file system. Such a server has all kinds of uses—it allows web applications to store and share data, or it can give a group of people shared access to a bunch of files.

  • When we treat files as HTTP resources, the HTTP methods GET, PUT, and DELETE can be used to read, write, and delete the files, respectively. We will interpret the path in the request as the path of the file that the request refers to.

    파일을 HTTP 리소스로 다룰 때, GET, PUT, DELETE 메소드는 각각 읽기, 쓰기, 삭제를 위해 사용될 수 있다.

  • We probably don’t want to share our whole file system, so we’ll interpret these paths as starting in the server’s working directory, which is the directory in which it was started. If I ran the server from /tmp/public/, then a request for /file.txt should refer to /tmp/public/file.txt.

  • We’ll build the program piece by piece, using an object called methods to store the functions that handle the various HTTP methods. Method handlers are async functions that get the request object as argument and return a promise that resolves to an object that describes the response.

  • const {createServer} = require("http");
    
    const methods = Object.create(null);
    
    createServer((request, response) => {
        let handler = methods[request.method] || notAllowed;
        handler(request)
            .catch(error => {
                if (error.status != null) return error;
                return {body: String(error), status: 500};
            })
            .then(({body, status = 200, type = "text/plain"}) => {
                response.writeHead(status, {"Content-Type": type});
                if (body && body.pipe) body.pipe(response); 
                    // readable stream 끝에 도달하면 .end() 자동으로 호출
                else response.end(body);
            });
    }).listen(8000);
    
    async function notAllowed(request) {
        return {
            status: 405,
            body: `Method ${request.method} not allowed.`
        };
    }
  • This starts a server that just returns 405 error responses, which is the code used to indicate that the server refuses to handle a given method.

    405 에러; 해당 메소드를 거절한다.

  • When the value of body is a readable stream, it will have a pipe method that is used to forward all content from a readable stream to a writable stream. If not, it is assumed to be either null (no body), a string, or a buffer, and it is passed directly to the response’s end method.

    body가 읽을 수 있는 스트림일 때, 이는 읽을 수 있는 스트림으로부터 모든 내용을 쓸 수 있는 스트림으로 전달할 수 있는 pipe 메소드를 가질 것이다.

  • To figure out which file path corresponds to a request URL, the urlPath function uses Node’s built-in url module to parse the URL. It takes its pathname, which will be something like "/file.txt", decodes that to get rid of the %20-style escape codes, and resolves it relative to the program’s working directory.

    url 내장 모듈, ...

  • const {parse} = require("url");
    const {resolve, sep} = require("path");
    
    const baseDirectory = process.cwd();
    
    function urlPath(url) {
        let {pathname} = parse(url);
        let path = resolve(decodeURIComponent(pathname).slice(1));
        if (path != baseDirectory &&
            !path.startsWith(baseDirectory + sep)) {
            throw {status: 403, body: "Forbidden"};
        } 
        return path;
    }
  • As soon as you set up a program to accept network requests, you have to start worrying about security. In this case, if we aren’t careful, it is likely that we’ll accidentally expose our whole file system to the network.

  • /../secret_file

  • To avoid such problems, urlPath uses the resolve function from the path module, which resolves relative paths. ... The process.cwd function can be used to find this working directory. The sep binding from the path package is the system’s path separator. ... the HTTP status code indicating that access to the resource is forbidden.

    path 모듈의 resolve, sep & process.cwd, 403 forbidden

  • We’ll set up the GET method to return a list of files when reading a directory and to return the file’s content when reading a regular file.

  • One tricky question is what kind of Content-Type header we should set when returning a file’s content. Since these files could be anything, our server can’t simply return the same content type for all of them. NPM can help us again here. The mime package (content type indicators like text/plain are also called MIME types) knows the correct type for a large number of file extensions.

    파일은 어떤 것이라도 될 수 있는데, 그렇다면 Content-type를 어떻게 정해야하는가?: NPM의 mime 패키지를 사용한다; 이는 많은 확장자들에 대해 정확한 MIME 타입을 알려준다.

  • When a requested file does not exist, the correct HTTP status code to return is 404. We’ll use the stat function, which looks up information about a file, to find out both whether the file exists and whether it is a directory.

    404 Not found, stat은 파일의 정보를 찾아보는 함수인데, 이를 사용하여 파일이 존재하는지, 그리고 디렉터리인지를 확인한다.

  • const {createReadStream} = require("fs");
    const {stat, readdir} = require("fs").promises;
    const mime = require("mime");
    
    methods.GET = async function(request) {
        let path = urlPath(request.url);
        let stats;
        try {
            stats = await stat(path);
        } catch (error) {
            if (error.code != "ENOENT") throw error;
            else return {status: 404, body: "File not found"};
        }
        if (stats.isDirectory()) {
            return {body: (await readdir(path)).join("\n")};
        } else {
            return {body: createReadStream(path), 
                    type: mime.getType(path)};
        }
    };
  • When the file does not exist, stat will throw an error object with a code property of "ENOENT". These somewhat obscure, Unix-inspired codes are how you recognize error types in Node.

    stat은 파일이 존재하지 않을 때 code 프로퍼티의 값이 "ENOENT"인 에러 객체를 던진다.

  • The stats object returned by stat tells us a number of things about a file, such as its size (size property) and its modification date (mtime property).

    stat에 의해 반환된 객체는 파일에 대한 여러 정보를 담고 있다. (size, mtime, isDirectory, ...)

  • We use readdir to read the array of files in a directory and return it to the client. For normal files, we create a readable stream with createReadStream and return that as the body, along with the content type that the mime package gives us for the file’s name.

    readdir, createReadStream, mime

  • const {rmdir, unlink} = require("fs").promises;
    
    methods.DELETE = async function(request) {
        let path = urlPath(request.url);
        let stats;
        try {
            stats = await stat(path);
        } catch (error) {
            if (error.code != "ENOENT") throw error;
            else return {status: 204};
        }
        if (stats.isDirectory()) await rmdir(path);
        else await unlink(path);
        return {status: 204};
    }
  • When an HTTP response does not contain any data, the status code 204 (“no content”) can be used to indicate this. Since the response to deletion doesn’t need to transmit any information beyond whether the operation succeeded, that is a sensible thing to return here.

    HTTP 리스폰스가 어떠한 데이터도 포함하지 않을 때, 상태 코드 204가 사용된다.

  • You may be wondering why trying to delete a nonexistent file returns a success status code, rather than an error. When the file that is being deleted is not there, you could say that the request’s objective is already fulfilled. The HTTP standard encourages us to make requests idempotent, which means that making the same request multiple times produces the same result as making it once. In a way, if you try to delete something that’s already gone, the effect you were trying to do has been achieved—the thing is no longer there.

    HTTP 표준은 리퀘스트가 멱등적이기를 권장한다.

  • const {createWriteStream} = require("fs");
    
    function pipeStream(from, to) {
        return new Promise((resolve, reject) => {
            from.on("error", reject);
            to.on("error", reject);
            to.on("finish", resolve);
            from.pipe(to);
        });
    }
    
    methods.PUT = async function(request) {
        let path = urlPath(request.url);
        await pipeStream(request, createWriteStream(path));
        return {status: 204};
    };
  • But since pipe isn’t written to return a promise, we have to write a wrapper, pipeStream, that creates a promise around the outcome of calling pipe.

  • When something goes wrong when opening the file, createWriteStream will still return a stream, but that stream will fire an "error" event. The output stream to the request may also fail, for example if the network goes down. So we wire up both streams’ "error" events to reject the promise. When pipe is done, it will close the output stream, which causes it to fire a "finish" event. That’s the point where we can successfully resolve the promise (returning nothing).

    "error", "finish"

  • The command line tool curl, widely available on Unix-like systems (such as macOS and Linux), can be used to make HTTP requests. The following session briefly tests our server. The -X option is used to set the request’s method, and -d is used to include a request body.

    Unix-like systems의 curl: HTTP 리퀘스트를 만드는데 사용될 수 있다. -X 옵션은 리퀘스트의 메소드를, -d 옵션은 리퀘스트의 바디를 설정하는데 사용할 수 있다.

Summary

  • Node is a nice, small system that lets us run JavaScript in a nonbrowser context. It was originally designed for network tasks to play the role of a node in a network. But it lends itself to all kinds of scripting tasks, and if writing JavaScript is something you enjoy, automating tasks with Node works well.
  • All input and output in Node is done asynchronously, unless you explicitly use a synchronous variant of a function, such as readFileSync. When calling such asynchronous functions, you provide callback functions, and Node will call them with an error value and (if available) a result when it is ready.

    노드의 모든 입출력은 비동기적으로 수행된다(동기적 버전을 사용하지 않는다면).

Exercises