JavaScript에서 Node.js로: 무대 뒤에서 실제로 일어나는 일 이해하기 (3부)

작성자

카테고리:

← 피드로
DEV Community · Sudhanshu code · 2026-07-24 개발(SW)

From JavaScript to Node.js: Understanding What Really Happens Behind the Scenes (Part 3)

Synchronous vs Asynchronous Node.js: What Really Happens Inside fs.readFile()?

In the previous article, we learned that Node.js is much more than the V8 engine.

We discovered that a simple file operation travels through multiple layers:

JavaScript
    ↓
V8 Engine
    ↓
Node APIs
    ↓
C++ Bindings
    ↓
libuv
    ↓
Operating System
    ↓
Hardware

Enter fullscreen mode Exit fullscreen mode

But one important question still remains unanswered.

If reading a file requires communicating with the operating system and the storage device…

Why doesn’t Node.js freeze while waiting for the file?

To answer that, we first need to understand one of the most important ideas in computer science.

What Does “Blocking” Actually Mean?

Most tutorials define blocking like this:

“Blocking means the program waits.”

That definition is correct…

but it doesn’t explain what is actually waiting.

Let’s look deeper.

Consider this program.

console.log("Start");

const fs = require("fs");

const data = fs.readFileSync("message.txt", "utf8");

console.log(data);

console.log("End");

Enter fullscreen mode Exit fullscreen mode

Many developers say:

“Node waits.”

But technically, that’s not what happens.

The current JavaScript execution cannot continue because the current function has not finished.

The JavaScript thread is occupied.

Imagine the execution like a single employee working at a desk.

Current Task

↓

Read File

↓

Not Finished Yet

Enter fullscreen mode Exit fullscreen mode

Until that task finishes…

the employee cannot start another task.

This is called blocking.

Understanding the Call Stack

Before discussing asynchronous programming, we need to understand the Call Stack.

The Call Stack is a data structure that keeps track of which JavaScript function is currently executing.

Imagine it as a stack of books.

Top

console.log()

main()

Bottom

Enter fullscreen mode Exit fullscreen mode

Whenever a function starts, it is pushed onto the stack.

When it finishes, it is removed.

Only the function at the top of the stack can execute.

JavaScript executes one stack frame at a time.

This is one of the reasons JavaScript is called single-threaded.

Example

function greet() {
    console.log("Hello");
}

console.log("Start");

greet();

console.log("End");

Enter fullscreen mode Exit fullscreen mode

Call Stack visualization

console.log("Start")

↓

Removed

↓

greet()

↓

console.log("Hello")

↓

Removed

↓

console.log("End")

Enter fullscreen mode Exit fullscreen mode

Only one frame executes at a time.

There is no parallel execution happening here.

What Happens Inside readFileSync()?

Let’s follow the execution carefully.

const fs = require("fs");

console.log("1");

const text = fs.readFileSync("notes.txt", "utf8");

console.log(text);

console.log("2");

Enter fullscreen mode Exit fullscreen mode

Execution begins.

console.log("1")

↓

Output

1

Enter fullscreen mode Exit fullscreen mode

Next comes

fs.readFileSync(...)

Enter fullscreen mode Exit fullscreen mode

Node sends the request down the runtime layers.

JavaScript

↓

Node API

↓

C++ Binding

↓

Operating System

↓

SSD

Enter fullscreen mode Exit fullscreen mode

At this moment…

JavaScript cannot continue.

The Call Stack still contains

readFileSync()

Enter fullscreen mode Exit fullscreen mode

Until it returns,

this line

console.log("2");

Enter fullscreen mode Exit fullscreen mode

cannot execute.

The result arrives.

The stack becomes free.

Execution continues.

Final Output

1

(File Content)

2

Enter fullscreen mode Exit fullscreen mode

Nothing surprising happened.

The execution simply stopped until the file arrived.

This is synchronous execution.

Why Is It Called Synchronous?

Because every operation happens in sequence.

Task A

↓

Complete

↓

Task B

↓

Complete

↓

Task C

Enter fullscreen mode Exit fullscreen mode

Nothing jumps ahead.

Nothing overlaps.

Every step waits for the previous one.

Now Let’s Look at readFile()

const fs = require("fs");

console.log("1");

fs.readFile("notes.txt", "utf8", (err, text) => {
    console.log(text);
});

console.log("2");

Enter fullscreen mode Exit fullscreen mode

Many beginners imagine that JavaScript starts another thread.

It doesn’t.

Let’s see the real flow.

Step 1

console.log("1")

Enter fullscreen mode Exit fullscreen mode

Output

1

Enter fullscreen mode Exit fullscreen mode

Step 2

JavaScript reaches

fs.readFile(...)

Enter fullscreen mode Exit fullscreen mode

Node prepares a native request.

The request moves through

Node API

↓

C++

↓

libuv

↓

Operating System

Enter fullscreen mode Exit fullscreen mode

Notice something important.

The file has not been read yet.

Only the request has been submitted.

Step 3

At this point…

JavaScript has nothing left to do with that request.

The current function finishes immediately.

The Call Stack becomes free.

Now JavaScript executes

console.log("2");

Enter fullscreen mode Exit fullscreen mode

Output becomes

1

2

Enter fullscreen mode Exit fullscreen mode

The file still hasn’t arrived.

Step 4

The operating system eventually finishes reading the file.

The data returns.

SSD

↓

Operating System

↓

libuv

↓

Node Runtime

Enter fullscreen mode Exit fullscreen mode

The callback is now ready.

Later, when JavaScript is free to execute it, the callback runs and prints the file.

Final Output

1

2

Hello World

Enter fullscreen mode Exit fullscreen mode

This is why asynchronous code appears to “continue.”

JavaScript didn’t keep reading the file.

It simply delegated that responsibility.

The Biggest Misconception

People often say

“Node.js performs asynchronous operations.”

That sentence is incomplete.

A better statement is

Node.js delegates long-running operations to the operating system (through libuv and native bindings), allowing JavaScript to continue executing other work while the result is being prepared.

That is the real reason asynchronous programming exists.

Why Doesn’t V8 Read the File Directly?

Because V8 was never designed to understand operating systems.

V8 understands JavaScript.

It understands things like

const numbers = [1,2,3];

numbers.map(x => x * 2);

Enter fullscreen mode Exit fullscreen mode

It does not understand

  • NTFS
  • ext4
  • APFS
  • TCP sockets
  • DNS
  • File permissions

Those belong to the operating system.

Node bridges that gap.

Does libuv Read the File?

Another common misconception.

Many developers think

“libuv reads the file.”

Not exactly.

In most file operations, the operating system ultimately performs the actual file read.

libuv coordinates the asynchronous workflow, abstracts platform differences, and integrates the result back into Node.js in a consistent way across operating systems.

Think of libuv as an expert traffic controller—not the storage device itself.

Why Is Node.js Fast?

Many articles say

“Node.js is fast because it is asynchronous.”

That’s only part of the story.

Node.js is fast because it avoids wasting the JavaScript thread waiting for slow I/O operations.

Imagine a restaurant.

One waiter.

One customer asks for coffee.

Brewing coffee takes five minutes.

A poor workflow would have the waiter stand beside the coffee machine doing nothing.

A better workflow is:

  • Take the order.
  • Give it to the kitchen.
  • Serve other customers.
  • Return when the coffee is ready.

Node.js follows the second model.

The JavaScript thread stays productive.

Synchronous vs Asynchronous

Synchronous Asynchronous Blocking Non-blocking (for the JavaScript thread) Waits for completion Delegates work and continues Simpler control flow Better scalability for I/O Can freeze execution Keeps the thread available for other work

Neither is “better” in every situation.

Sometimes synchronous code is exactly what you need—especially during startup scripts, configuration loading, or command-line tools.

Engineering is about choosing the right tool.

Key Takeaways

After reading this article, you should be able to explain these ideas confidently:

  • JavaScript executes on a single main thread.
  • The Call Stack allows only one JavaScript function to execute at a time.
  • readFileSync() blocks because the current execution cannot continue until it returns.
  • readFile() submits work to the Node runtime and operating system, allowing JavaScript to continue.
  • V8 executes JavaScript—it does not understand operating-system resources.
  • Node.js extends JavaScript by providing native APIs and runtime infrastructure.
  • libuv helps Node provide efficient, cross-platform asynchronous I/O.

These are not just interview concepts.

They are the foundation of every Express server, every database query, every API request, and every backend application built with Node.js.

Coming Next

In Part 4, we’ll explore one of the most misunderstood topics in Node.js:

Modules, require(), module.exports, exports, Module Wrapper Function, and Module Cache

We’ll answer questions like:

  • What actually happens when you write require("fs")?
  • Is require() a JavaScript feature or a Node.js feature?
  • Why is every file treated as a separate module?
  • How does Node avoid loading the same module twice?
  • What is the Module Wrapper Function?
  • Why do exports and module.exports sometimes behave differently?

By the end of Part 4, you’ll understand Node.js modules from the inside out—not just how to use them, but how they work.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다