노드js (3)

💻 Programming

[ Node.js in Action ] 13장 요약

/**
 *  웹서버 그 이상의 것들
 * - Socket IO
 * - TCP/IP 네트워킹
 * - OS와 상호작용하기 위한 툴
 * - 커맨드라인 툴 개발하기
 */


13.1. Socket.io


Socket IO 는 WebSocket과 유사한 기능을 제공하면서도, broadcasting, volatile 메시징을 편리하게 구현할 수 있는 API를 제공합니다.

WebSocket은 WebSocket 프로토콜을 사용하는데 아직 완성된 프로토콜이 아니라서 브라우저 마다 지원여부에도 차이가 있다는 문제가 있습니다. Socket IO를 이용할 경우 이 문제를 해결할 수 있는데, Socket IO는 WebSocket을 지원하지 않는 브라우저의 경우에 각 브라우저별로 다양한 방법을 통해 내부적으로 메시지를 보낼 수 있다고 합니다.


 A minimal Socket.IO application that pushes the server’s time to connected clients
 A Socket.IO application that triggers page refreshes when CSS files are edited


13.1.1 초간단 Socket.IO 애플리케이션 만들기


<< 간단한 Socket IO clock-server 프로그램 >> ( 시간이 표시가 안되는데 이유를 모르겠음 ;;;; )


13.1.2 Socket.IO를 이용한 페이지와 CSS reloading 하기


웹페이지 디자인을 할 때 일반적인 순서

1 Open the web page in multiple browsers.
2 Look for styling on the page that needs adjusting.
3 Make changes to one or more stylesheets.
4 Manually reload all the web browsers.
5 Go back to step 2.


<< 위 4번 과정을 쉽게 만들어주는 프로그램 예제 >> ( 이놈도 제대로 동작을 하지 않음...;;;)


13.1.3 Socket.IO의 또 다른 사용법


Socket.IO는 프로그레스 바를 계속 업데이트해서 수치가 올라가는 것 보여주는 기능에 적합함




13.2 TCP/IP 네트워킹


노드는 네트워킹 애플리케이션에 매우 적합. 네트워킹은 많은 I/O를 요구하기 때문.


 Working with buffers and binary data
 Creating a TCP server
 Creating a TCP client


13.2.1 Buffers와 Binary Data 다루기


Buffer 데이타 타입은 노드에서 제공하는 특별한 데이타 타입이다.

고정 길이의 raw binary data처럼 동작하며, C에서의 malloc()이나 C++, JAVA에서의 new 키워드라고 생각해도 된다.

기본적으로 Stream 클래스에 의해 데이타 이벤트 내에 반환된다.

Buffer는 숫자 0~ 255까지만 저장할 수 있는 non-resizable 배열이다.


TEXT DATA VS. BINARY DATA

1. -----------------------------------

var b = new Buffer("121234869");
console.log(b.length);
9
console.log(b);
<Buffer 31 32 31 32 33 34 38 36 39>


2. -----------------------------------

var b = new Buffer(4);
b.writeInt32LE(121234869, 0);
console.log(b.length);
4
console.log(b);
<Buffer b5 e5 39 07>

-----------------------------------


13.2.2 TCP 서버 만들기 


Socket 클래스는 client와 server 양쪽에서 모두 사용됨

It’s a Stream subclass that’s both readable and writable (bidirectional).

That is, it emits data events when input data has been read from the socket,

and it has write() and end() functions for sending output data.


WRITING DATA


var net = require('net');
net.createServer(function (socket) {
socket.write('Hello World!\r\n');
socket.end();
}).listen(1337);
console.log('listening on port 1337');


$ telnet localhost 1337


READING DATA


socket.on('data', function (data) {
console.log('got "data"', data);
});


data 인자는 기본적으로 Buffer인스턴스이다.

setEncoding()을 이용해서 decoded String을 인자로 넘길 수도 있다.


socket.on('end', function () {
console.log('socket has ended');
});


end 이벤트를 리스닝함으로써 불필요한 데이터 전송을 하지 않을 수 있다.


SAMPLE TEST  


var net = require('net');
var socket = net.connect({ host: process.argv[2], port: 22 });
socket.setEncoding('utf8');
socket.once('data', function (chunk) {
console.log('SSH server version: %j', chunk.trim());
socket.end();
});


$ node client.js github.com


SOCKET.PIPE() 을 사용하여 두 개의 스트림 연결하기


The pipe() 함수는 readable 스트림에서 데이터를 읽어와서 쓰기가능한 스트림에 데이터를 쓰는 역할을 합니다.


비정상 연결끊김 핸들링하기

클라이언트가 netcat을 썼을 때 Ctrl-D 대신 Ctrl-C로 커넥션을 종료시키면 서버쪽 소켓이 깔끔하게 종료가 되지 않는데, 이 부분에 대한 뒷처리를 하려면 아래처럼 close 이벤트를 리스닝하면 된다.


socket.on('close', function () {
console.log('client disconnected');
});


FINAL TEST APP


server


var net = require('net');
net.createServer(function (socket) {
console.log('socket connected!');
socket.on('data', function (data) {
console.log('"data" event', data);
});
socket.on('end', function () {
console.log('"end" event');
});
socket.on('close', function () {
console.log('"close" event');
});
socket.on('error', function (e) {
console.log('"error" event', e);
});
socket.pipe(socket);
}).listen(1337);


client


var net = require('net');
var host = process.argv[2];
var port = Number(process.argv[3]);
var socket = net.connect(port, host);
socket.on('connect', function () {
process.stdin.pipe(socket);
socket.pipe(process.stdout);
process.stdin.resume();
});
socket.on('end', function () {
process.stdin.pause();
});



13.3 Tools for interacting with the operating system


 The global process object—Contains information about the current process, such
as the arguments given to it and the environment variables that are currently set
 The fs module—Contains the high-level ReadStream and WriteStream classes
that you’re familiar with by now, but also houses low-level functions that we’ll
look at
 The child_process module—Contains both low-level and high-level interfaces
for spawning child processes, as well as a special way to spawn node instances
with a two-way message-passing channel


13.3.1 The process global singleton


Every Node process has a single global process object that every module shares access
to. Useful information about the process and the context it’s running in can be found
in this object.


the most interesting feature of the process object is that it’s an EventEmitter instance, which emits very special events,
such as exit and uncaughtException.


USING PROCESS.ENV TO GET AND SET ENVIRONMENT VARIABLES


SPECIAL EVENTS EMITTED BY PROCESS


 exit gets emitted right before the process exits.


process.on('exit', function (code) {
console.log('Exiting...');
});


 uncaughtException gets emitted any time an unhandled error is thrown.


process.on('uncaughtException', function (err) {
console.error('got uncaught exception:', err.message);
process.exit(1);
});
throw new Error('an uncaught exception');


CATCHING SIGNALS SENT TO THE PROCESS 


three signals that Node handles by default


 SIGINT—Sent by your shell when you press Ctrl-C. Node’s default behavior is to
kill the process, but this can be overridden with a single listener for SIGINT on
process.
 SIGUSR1—When this signal is received, Node will enter its built-in debugger.
 SIGWINCH—Sent by your shell when the terminal is resized. Node resets
process.stdout.rows and process.stdout.columns and emits a resize event
when this is received.


13.3.2 Using the filesystem module


MOVING A FILE 


fs.rename()

원격으로 이동시키려면 stream을 이용해서 복사해야 함. rename은 원격지로 이동하는 것을 지원하지 않음.


WATCHING A DIRECTORY OR FILE FOR CHANGES 


fs.watch() - 플랫폼의 네이티브 파일 변경 알림 API를 이용함. (플랫폼 차이로 인해 unreliable 함)

fs.watchFile()


USING COMMUNITY MODULES: FSTREAM AND FILED 


fstream을 이용하면 cp -rp srcDir destDir 명령어를 쉽게 구현할 수 있다.


filed 인스턴스는 req, res 객체에 접근이 가능하다.

http.createServer(function (req, res) {
req.pipe(filed('path/to/static/files')).pipe(res);
});

위 코드는 파일이 캐쉬되었을 때 304 Not Modified 오류를 뱉어내고 파일을 디스크에서 열거나 읽는 행위는 하지 않는다.



13.3.3 Spawning external processes


 cp.exec()—A high-level API for spawning commands and buffering the result
in a callback
 cp.spawn()—A low-level API for spawning single commands into a Child-
Process object
 cp.fork()—A special way to spawn additional Node processes with a built-in
IPC channel


자식 프로세스의 장단점 (PROS AND CONS TO CHILD PROCESSES)

 

cons : child process 로 실행되는 애플리케이션이 이미 클라이언트에 설치가 되어있어야 한다.

pros : 다른 언어로 쓰여진 애플리케잇션을 이용해서 좀 더 rich한 애플리케이션 개발이 가능하다.


cp.exec()로 명령어 결과 버퍼링하기(BUFFERING COMMAND RESULTS USING CP.EXEC())


cp.exec(), is useful for when you want to invoke a command, and you only care about the final result. This API allows you to enter full sequences of commands, including multiple processes piped to one another.


SPAWNING COMMANDS WITH A STREAM INTERFACE USING CP.SPAWN()


cp.spawn() returns a ChildProcess object that you can interact with.


ex)

var child = cp.spawn('ls', [ '-l' ]);
// stdout is a regular Stream instance, which emits 'data',
// 'end', etc.
child.stdout.pipe(fs.createWriteStream('ls-result.txt'));
child.on('exit', function (code, signal) {
// emitted when the child process exits
});


DISTRIBUTING THE WORKLOAD USING CP.FORK()


Like cp.spawn(), cp.fork() returns a ChildProcess object.

The major difference is the API added by the IPC channel: the child process now has a child.send (message) function, and the script being invoked by fork() can listen for process.on('message') events.


피보나치 수열 계산 예제


13.4 커맨드라인 툴 개발하기(Developing command-line tools )


 Parsing command-line arguments
 Working with stdin and stdout streams
 Adding pretty colors to the output using ansi.js


13.4.1 Parsing command-line arguments


Node provides you with the process.argv property, which is an array of strings, which are the arguments that were used when Node was invoked.

The first entry of the array is the Node executable, and the second entry is the name of your script.


ex)

var args = process.argv.slice(2);
console.log(args);


$ node args.js
[]


$ node args.js hello world
[ 'hello', 'world' ]


$ node args.js "tobi is a ferret"
[ 'tobi is a ferret' ]


어떤 옵션이 들어왔느냐에 따라 usage프린트하는 예제


13.4.2 stdin, stdout 가지고 놀기 (Working with stdin and stdout)


 process.stdin—A ReadStream to read input data from
 process.stdout—A WriteStream to write output data to


WRITING OUTPUT DATA WITH PROCESS.STDOUT


console.log() 내에서 process.stdout.write()이 사용됨.


(이클립스에서 argument 설정해주고 실행한다고 가정)

var target = process.argv[2];
process.stdout.write('Entered value is : ' + target);


PROCESS.STDIN 로 입력값 읽어오기 (READING INPUT DATA WITH PROCESS.STDIN )

 

Before you can read from stdin, you must call process.stdin.resume() to indicate that your script is interested in data from stdin.

After that, stdin acts like any other readable stream, emitting data events as data is received from the output of another process, or as the user enters keystrokes into the terminal window.


나이 검증하는 예제


DIAGNOSTIC LOGGING WITH PROCESS.STDERR 


 process.stderr을 직접 사용하지말고 console.error()를 사용하자.


13.4.3 출력에 색 입히기 (Adding colored output)


ANSI 이스케이프 코드 작성하기 (CREATING AND WRITING ANSI ESCAPE CODES )


(cmd 창에서 ansi.js 실행)


console.log('\033[43m\033[35mhello\033[39m\033[49m');

var ansi = require('ansi');
var cursor = ansi(process.stdout);

cursor
.fg.green()
.write('This should be green.')
.fg.reset()
.write('\n');
 



💻 Programming

[ Node.js in Action ] 3장 요약

3장에서는 아래와 같은 내용을 다룹니다.


 Organizing your code into modules
 Coding conventions
 Handling one-off events with callbacks
 Handling repeating events with event emitters
 Implementing serial and parallel flow control
 Leveraging flow-control tools


3.1 모듈을 이용한 코드 재사용 및 정리


PHP나 Ruby같은 언어에서 import하면 global scope에 영향을 미친다.

이런 것을 피하기 위해서 PHP는 namespaces를 사용하고, Ruby는 modules를 사용한다.

하지만 namespaces를 사용한다고 해도 overwrite의 가능성을 완전히 배제할 수는 없다.

Node에서는 이런 위험을 완전히 배제한다.

우선 Node의 module은 gloabl scope를 변경하지 않는다. 또한, module을 만들 때 개발자가 어떤 기능, 변수를 외부에서 사용가능 하게 할지 선택 할 수 있다. ( 이건 자바에서 접근제한자, public 또는 private,를 생각하면 된다. )

Node에서는 외부에 노출시키는 것을 export한다고 한다. ( public으로 만든다는 것을 의미함 )

export의 방법은 첫째, 둘 이상의 기능 및 변수를 export할 때에는 exports객체의 속성을 세팅해주면 된다. 둘째, 하나의 기능이나 변수만 export할 때에는 module.exports 속성을 이용한다.


3.1.1 모듈 생성하기


모듈은 a.하나의 파일이거나, b.디렉토리내에 하나 이상의 파일로 구성될 수 있다.

디렉토리 구조일 경우에는 디렉토리 내부에 index.js파일을 main파일로 사용한다. ( 이 파일 명은 overridden 가능하다. )


하나의 파일을 모듈로 만들 때에는 아래처럼 정의한다.

--------------- convert.js -------------

var num = 0.5;

exports.convertNum = function(myNum) {
     return Math.round(myNum / num);
}

----------------------------------------

위 코드를 보면 exports의 속성으로 convertNum 함수를 정의했다. 이는 외부에서 접근이 가능한 public 함수이다. 하지만 num 변수는 exports속성으로 정의하지 않았으므로 convert.js파일 내에서만 접근 가능한 private 변수이다.


이렇게 만들어진 모듈을 test-convert.js 파일에서 사용하려면 아래처럼 할 수 있다.


--------------- test-convert.js -------------

var convert = require('./convert');

console.log(convert.convertNum(5));

-----------------------------------------

여기서 require()에 명시한 "./convert"는 현재 디렉토리의 convert.js파일을 import한다는 의미이다. "./"를 붙이지 않으면 오류가 난다. require할 때 core module에 대해서는 상대경로를 사용하지 않고 바로 모듈명만 명시하도록 되어있는데 convert.js는 core module이 아니라 방금 우리가 만든 module이기 때문이다.

require()는 convert.js에 명시한 exports의 속성을 return한다. 따라서 var convert에는 convert.js(모듈)에서 정의한 exports 객체의 내용,즉, convertNum 함수가 할당되었다고 할 수 있다. 


3.1.2 module.exports 사용하기 ( single 변수, 함수, 객체 export 시 사용 ) 


module을 객체지향 관점에서 사용하고자 할 때에는 생성자 함수를 return하도록 한 뒤 아래처럼 사용할 수도 있을 것이다.


---------------- convert.js --------------

var Convert = function (num){

     this.num = num;

}

Convert.prototype.convertNum = function(myNum) {
     return Math.round(myNum / num);
}

// exports = Convert; ( 이렇게 사용 금지 )

module.exports = Convert; 

---------------------------------------

------------- test-convert2.js ------------

var Convert = require('./convert');

var num = 0.5

var convert = new Convert(num);

console.log(convert.convertNum(5));

---------------------------------------

위 코드에서 중요한 부분은 module.exports = Convert; 라인이다.


첫 예제에서 exports를 이용해서 property값을 설정함으로써 convert모듈에서 정의한 convertNum() 함수를 사용할 수 있었다. 이는 exports가 module.exports를 참조하는 global변수였기에 가능했다. 따라서 exports = Convert; 라고 해버리면 exports는 더이상 module.exports를 참조하는 변수가 아닌것이 된다. 


3.1.3 node_modules 디렉토리


앞에서 module을 require할 때 core module이 아니면 상대 경로를 적어주고, core module이면 그냥 이름을 써도 된다고 했다.

그러면 내부적으로 어떤 로직을 타길래 core module은 이름만 적어도 되는 걸까? Node가 module을 찾는 로직은 아래와 같다.



 

자, 그럼 내가 만든 모듈을 상대경로 지정을 하지 않고 쓰려면 어떻게 하면 될까?

그냥 node_modules 안에 넣어버리면 된다. 실제로 npm(Node Package Manager)를 이용하여 install을 하면 node_modules 디렉토리에 설치가 된다.


3.1.4 주의사항

첫번째 주의사항은 디렉토리 module 사용상 주의사항이다.

디렉토리 묶음으로 module을 만들었을 때에는 package.json에 명시하지 않는한 index.js파일이 존재해야한다.

아래 그림은 connect module을 설치했을 때 볼 수 있는 패키지 트리이다.


connect 디렉토리 안에 index.js라는 파일이 있는 것을 볼 수 있다. 

이제 이 파일명을 index.js 가 아닌 index_2.js 로 바꿔보자.

그리고 테스트 프로그램을 하나 만들어서 require('connect'); 한 줄만 입력해보고 실행해보자. 

그럼 오류가 날 것이다.

일반적으로 디렉토리 구조의 module에서는 index.js 라는 파일명을 사용하지만, 굳이 바꾸고 싶다면 package.json 파일에 명시해주면 된다.


package.json 파일을 열어서 "main" 을 찾아보자. 아마 없을 것이다. 아래처럼 main속성을 넣어보자.

{  // package.json 1번째 라인

"main":"index_2.js"

--생략--

} // package.json 마지막 라인


이제 다시 실행해보면 에러없이 실행되는 것을 확인할 수 있다. ( core module이니 파일명은 다시 index.js로 바꿔주자. Now~~ )


참고로 아래 그림은 directory 형태의 module을 require했을 때 처리되는 로직이다.



두 번째 주의사항은 "monkey patching"이다. 원숭이 패치? 이름이 참 희한하다.

monkey patching은 module을 객체로 caching하는 Node의 능력 때문에 가능한 것으로, 만약 애플리케이션에서 서로 다른 두 파일이 하나의 module을 require한다고 가정해보자. 이 때 동일한 module을 두 번 require했다고해서 module파일을 두 번 읽어들일 필요가 없도록 Node에서 캐시에 넣어둔다. 하지만, 이 두번 째 require에서 이 캐시에 들어있는 module정보를 변경(alter)할 수 있는 기회를 갖게 된다. 이렇게 변경하는 것을 monkey patching이라고 하는데, 이것을 잘 이용하면 module이 다른 module의 동작(behavior)를 수정할 수 있을 뿐 아니라 개발자가 다른 버전의 module을 만들 필요가 없도록 해준다. ( 이건 동적 BCI라고 이해했다. )


3.2 비동기식 프로그래밍 테크닉


3.2.1 Callback함수를 이용한 one-off 이벤트 핸들링


var fs = require('fs');
fs.readFile('./blah.txt', function(err, data) {
     if (err) { throw err };
     // do something
});


3.2.2 EventEmitter를 이용한 반복 이벤트 핸들링 


var net = require('net');
var server = net.createServer(function(socket) {
     socket.on('data', function(data) {
          socket.write(data);
     });
});
server.listen(8080);


1회성 이벤트 처리시 once 사용.


var net = require('net');
var server = net.createServer(function(socket) {
     socket.once ('data', function(data) {
          socket.write(data);
     });
});
server.listen(8080);


EventEmitter 확장하기


function Watcher(watchDir, processedDir) {
     this.watchDir = watchDir;
     this.processedDir = processedDir;
}

var events = require('events'), util = require('util');
util.inherits(Watcher, events.EventEmitter);     // == Watcher.prototype = new events.EventEmitter();


var fs = require('fs'), watchDir = './watch', processedDir = './done';
Watcher.prototype.watch = function() {
     var watcher = this;
     fs.readdir(this.watchDir, function(err, files) {
          if (err) throw err;
          for(var index in files) {
               watcher.emit('process', files[index]);
          }
     })
}
Watcher.prototype.start = function() {
     var watcher = this;
     fs.watchFile(watchDir, function() {
          watcher.watch();
     });
}

var watcher = new Watcher(watchDir, processedDir);


watcher.on('process', function process(file) {
     var watchFile = this.watchDir + '/' + file;
     var processedFile = this.processedDir + '/' + file.toLowerCase();
     fs.rename(watchFile, processedFile, function(err) {
          if (err) throw err;
     });
});


watcher.start();



3.2.3 비동기식 어플 개발시 주의할 점


동기식에서는 프로그램의 실행 순서가 명확하지만 비동기식에서는 실행 순서가 명확하지 않다. 

Node의 event loop는 비동기 로직이 완전히 완료되었는지 추적을 합니다. 아무리 비동기 로직이라도 완전히 끝나지 않았다면 프로세스는 종료되지 않습니다.


[ Javascript 의 Closure에 대한 개념 링크 ]



3.3 비동기식 로직을 순서대로 처리하기


Nimble 을 이용한 sequencing 처리

Nimble 이외에도 Step, Seq와 같은 module이 이미 존재함. 가져다 쓰면 됨.





[[궁금증 들.....]]

1. 의존성모듈 package.json 파일명을 다른걸로 바꿀 수 있나??

npmjs.com에서 말하는 npm에서 말하는 package라 함은 아래와 같습니다.


A package is any of:

  • a) a folder containing a program described by a package.json file
  • b) a gzipped tarball containing (a)
  • c) a url that resolves to (b)
  • d) a <name>@<version> that is published on the registry with (c)
  • e) a <name>@<tag> that points to (d)
  • f) a <name> that has a latest tag satisfying (e)
  • g) a git url that, when cloned, results in (a).

또한 module이라 함은


  • A folder with a package.json file containing a main field.
  • A folder with an index.js file in it.
  • A JavaScript file.

로 정의를 하고 있습니다. 

따라서, package.json 파일을 다른 파일명으로 사용한 다는 것은 불가능합니다.


출처 : https://docs.npmjs.com/how-npm-works/packages


2. 이벤트 루프 = 멀티쓰레드????, 비동기와 멀티쓰레드의 차이점??

이벤트 루프는 결국 멀티쓰레드, 폴링과 같은 기술을 사용하는 것이라는 결론입니다. 

아래는 참조할 만한 링크들입니다.


http://jeremyko.blogspot.kr/2012/12/nodejs-thread-poollibuv.html    ( 한글 블로그 )


http://nikhilm.github.io/uvbook/basics.html#event-loops


http://stackoverflow.com/questions/21485920/single-threaded-event-loop-vs-multi-threaded-non-blocking-worker-in-node-js



3. monkey patching - 왜 첫 번째 require할 때 수정가능X ?

이건 사실 질문 자체가 잘못되었던 것 같다. monkey patching에 대해서는 아래 링크에 설명이 나와있다.


http://fredkschott.com/post/2014/09/nodejs-dangerous-module-design-patterns/


결국 monkey patching 이라는 것은 기존에 있던 것을 overwrite한다는 것인데 require를 처음 할 때에는 overwrite할 게 없으니까 동일한 모듈을 다른 파일에서 또 require 할 때에 수정할 수 있는 기회를 준다고 책에서 설명을 한 것 같다. 이 "원숭이 패치"에 대해서는 책에서 자세하게 설명을 안하고 있는데 위 링크에서 설명하는 것을 보면 그 위험성 때문에 그런게 아닌가 싶다. 가독성도 떨어질 것 같고..

💻 Programming

[ Node.js In Action ] 1장 요약

1장은 정말 간단하게 노드를 소개합니다.


Node.js 는 asynchronous하고 event-driven이다.


asynchronous는 비동기식이라는 말이고 비동기식이라 함은 어떤 요청이 들어왔을 때 해당 요청을 처리해야하는 녀석을 호출하고 그 녀석이 일을 끝내기를 기다리는 것이 아니라 나는 또 다른 요청을 기다린다는 말이다.

Event-driven이라는 것은 이벤트가 발생하기를 기다리고 있다가 해당 이벤트가 발생할 때 어떤 일을 한다는 것을 말한다. 여기서 이벤트는 사용자가 버튼을 클릭하는 것이 될 수도 있고, 스크롤바가 제일 밑으로 내려갔다거나, 사용자가 마우스를 특정 영역에 올려놓는다거나 하는 등의 동작을 말한다.


Node.js는 DIRTy 애플리케이션을 위한 플랫폼이다. ( 프레임웍이 아니다 )


여기서 DIRTy라 함은 Data-Intense Real-Time 을 말한다. 


끝에 y는 왜 붙였을까? 궁금해졌다.  

"DIRT애플리케이션"과 "DIRTy애플리케이션"이라는 단어를 비교해보면, dirt라는 단어는 먼지, 떼, 진흙 등을 의미하는 명사이고 dirty는 dirt의 형용사로서 더러운, 지저분한 이라는 의미를 갖고있다. 뭐가 더 듣기 좋은지 한국말로 비교해보자. "먼지 어플", "진흙 어플", "떼 어플"이라는 말과 "더러운 어플", "지저분한 어플"이라는 말, 어떤 이름이 더 자연스럽고 이해하기 쉬운가? 실시간으로 데이터 처리량이 많은 어플을 얘기할 때 "먼지 어플"과 "지저분한 어플", 둘 중 하나를 선택한다면 당연히 지저분한 어플을 선택하는게 사람들이 이해하기가 훨씬 쉽다. 그래서 끝에 y를 붙여서 느낌을 살린거다.

뭐 이건 어디까지나 나의 개인적인 생각이다.


저 두 가지 내용이 1장에서 얘기하는 내용이다.


그럼 2장으로 넘어가볼까?