【Node.js実戦習得】第3回:fs・pathモジュールで学ぶファイル操作、非同期処理(async/await)、そして環境依存の罠

社員ブログ

画面がないNode.jsの「標準モジュール」とは?

酒本先輩、前回は console.log でターミナルに文字を出しただけでしたけど、今回は何をするんですか?

今回はファイル操作をやりましょう。
ブラウザのJavaScriptだと、セキュリティの関係でユーザーのパソコン内のファイルを勝手に作ったり読んだりできなかったですね?

はい、勝手にファイルを書き換えられたらウイルスと同じですからね。

そう。
でもNode.jsはサーバーサイドで動くから、サーバー内やパソコン内のファイルを自由に読み書きできるの。
そのためにNode.jsがあらかじめ用意してくれている機能のことを『標準モジュール』と呼ぶわ。

標準モジュール……。
新しく外部からライブラリをインストールしなくても、最初から使える機能ってことですね。

そう。
今回はその中から、ファイルシステムを操作する fs(File System)モジュール と、パスを安全に扱う path モジュール を使って、実務の基礎を一気に学んでいきましょう。

まずは基本の「同期処理」でファイルを読み書きする

まずは処理の流れが分かりやすい『同期処理』から試しましょうか。
前回の nodejs-sample ディレクトリの中に、新しく file_sync.js というファイルを作って、VS Codeで開いてみて。

touch file_sync.js

開いたら、次のコードを書いて保存してみて。

// file_sync.js

// 1. fsモジュールを読み込む
const fs = require('fs');

const content = 'Node.jsから自動生成されたテキストファイルです。\n';

console.log('【1】ファイルの書き込み開始...');

// 2. 同期処理でファイルを書き込む
fs.writeFileSync('output.txt', content, 'utf8');
console.log('【2】ファイルの書き込み終了');

console.log('【3】ファイルの読み込み開始...');

// 3. 同期処理でファイルを読み込む
const data = fs.readFileSync('output.txt', 'utf8');
console.log('【4】--- 読み込んだ中身 ---');
console.log(data);
console.log('【5】ファイルの処理終了');

書けました!一番上の require('fs') っていうのが、ブラウザのJSでは見かけない書き方ですね。

そうそう。
Node.jsで標準モジュールや外部のライブラリを読み込むときは、この require() という関数を使うのよ。
じゃあ、さっそく実行してみて。

 

node file_sync.js
# 出力結果
【1】ファイルの書き込み開始...
【2】ファイルの書き込み終了
【3】ファイルの読み込み開始...
【4】--- 読み込んだ中身 ---
Node.jsから自動生成されたテキストファイルです。

【5】ファイルの処理終了

フォルダの中に新しく output.txt が作られて、その中身もちゃんと読み込めました!
ログの番号も【1】から【5】まで順番通りに出力されています。

そう。
関数の最後についている Sync は『同期(Synchronous)』の略で、処理が完全に終わるまで次の行へ進まずにその場でじっと待つ、という意味なの。直感的で分かりやすいでしょ?」

はい、いつものJavaScriptと同じ挙動なので分かりやすいです。

 

⚠️ なぜ実務で「Sync(同期)」を使ってはいけないのか?

ただ、ここからがバックエンド開発の重要な分かれ道よ。

実務の現場では、この Sync とつく関数は原則として使ってはいけないとされているの。

そうなんですね!

例えば、Webサーバーが動いていて、世界中から同時に多くのアクセスが来ていると想像して。

そこで誰か1人のリクエストが『1つの巨大なファイルを readFileSync で読み込む』という処理を始めたらどうなると思う?

読み込みが終わるまで、その行で処理が止まります。

そう。
その数秒間、サーバー全体の動きがフリーズするの
つまり、1人のせいで他の多くのお客さまは、ページを表示したいだけなのに、その巨大ファイルが読み込み終わるまで待たされることになるわ。

それは大惨事ですね……。

1人の重い処理のせいで、システム全体が巻き添えを食らってしまいますね。

だから待ち時間を無駄にしない『非同期処理』が必要になるのよ。

Node.jsは裏側で『ファイルを読み込んでいる間、終わったら教えてね!

その間他のお客さまの処理をしてくるから!』という動きができるわ。

これが大量のアクセスをサクサク捌ける理由よ。

実務の定番「async/await」で非同期処理に書き換える

非同期処理って大切ですね。
でも非同期処理ってコールバック関数とかPromiseとか書き方が複雑で苦手意識があるんですよね……。

昔のNode.jsはコールバックが多かったけど、今の現場は async/await 構文でスッキリ書くのが標準だから。

さっそく、非同期版に書き換えてみましょう。新しく file_async.js を作ってみて。

touch file_async.js
// file_async.js

// 非同期版(Promise対応)のfsモジュールを読み込む
const fs = require('fs').promises;

const content = '非同期で書き込まれたテキストです。\n';

async function main() {
    try {
        console.log('【1】ファイルの書き込み開始(非同期)...');

        // awaitをつけて非同期処理の完了を待つ
        await fs.writeFile('output-async.txt', content, 'utf8');
        console.log('【2】ファイルの書き込み完了');

        console.log('【3】ファイルの読み込み開始(非同期)...');

        // 読み込みもawaitで待つ
        const data = await fs.readFile('output-async.txt', 'utf8');
        console.log('【4】--- 読み込んだ中身 ---');
        console.log(data);

    } catch (error) {
        console.error('エラーが発生しました:', error);
    }
}

// メイン関数の実行
main();
console.log('【5】main()を実行しました。'

読み込みが require('fs').promises になっていますね。

そう、これをつけることで fs モジュールの関数がPromise(非同期処理)を返すようになるの。

そして、非同期処理を行いたい場所を async function で囲み、処理の前に await を置く。

これが今のNode.jsの基本スタイルね。

なるほど。

じゃあ、これを実行してみます!

node file_sync.js
# 出力結果
【1】ファイルの書き込み開始(非同期)...
【5】main()を実行しました。
【2】ファイルの書き込み完了
【3】ファイルの読み込み開始(非同期)...
【4】--- 読み込んだ中身 ---
非同期で書き込まれたテキストです。

酒本先輩、ログの出力順がおかしいです!

【1】の次にいきなり一番下の【5】が実行されて、その後に【2】【3】【4】が動いています!

非同期処理が正しく動いている証拠ね。

どういうことですか?

【1】でファイルの書き込みを裏側で依頼したあと、Node.jsは『書き込みが終わるのをじっと待つ』ということはしないの。

メイン関数の外側にある【5】の処理を先にサクッと終わらせちゃう。

そして裏側でファイルの書き込みが終わったら、中断していた main 関数の中の【2】に戻ってくる。だからこの順番になるのよ。

なるほど!

【5】が別のお客さまのリクエストだとすれば、ファイルの書き込みを待たせることなく、瞬時に別のお客さまの処理を処理したってことですね!

その通り。

この感覚が掴めれば、Node.jsのバックエンド開発の半分はマスターしたようなものよ。

 

実行する場所で挙動が変わる?「相対パス」の罠

非同期処理の凄さは分かりました。

ところで先輩、今はファイル名だけを指定して同じフォルダ内に作っていますよね。もし別フォルダにあるファイルを読み書きしたいときは、普通に ./assets/config.json みたいな相対パスで指定すればいいんですよね?」

バックエンドの開発では、その『普通の相対パス』が思わぬバグを生む原因になるの。

ちょっと実験してみようか。

node-sample ディレクトリの中に、新しく src というフォルダを作って、その中に path_test.js を作ってみて。

 

mkdir src
touch src/path_test.js

そしたら、同じ src フォルダの中に、読み込み用のテキストファイル secret.txt も作っておこう。

touch src/secret.txt

src/secret.txt の中身には適当に『ひみつの情報』とでも書いて保存して。

その上で、src/path_test.js に以下の読み込みコードを書いてみて。

分かりやすさのために一回 Sync で書くわね。

touch src/path_test.js
// src/path_test.js
const fs = require('fs');

try {
    // 同じフォルダ内にあるはずの secret.txt を相対パスで指定
    const data = fs.readFileSync('./secret.txt', 'utf8');
    console.log('読み込み成功:', data);
} catch (err) {
    console.error('エラーが発生しました:', err.message);
}

書けました。

同じ src フォルダの中に両方あるので、./secret.txt で合っていますよね。

じゃあ、まずは src フォルダに移動して実行してみます。

cd src
node path_test.js
# 出力結果
読み込み成功: ひみつの情報

読み込みができました!

じゃあ次に、一つ上の階層(node-sample)に戻って、そこからフォルダを指定して実行してみて。

 

cd ..
node src/path_test.js

えっ!?

ENOENT: no such file or directory, open './secret.txt' ってエラーが出ました。

ファイルはあるのに、なんで『見つからない』って言われるんですか?

Node.jsにおける「カレントディレクトリ」の正体

ブラウザ上のJavaScriptだと、./ は『そのJSファイルがある場所』を基準にするわよね。

でも、Node.jsの ./ は『そのファイルがある場所』ではなく、『nodeコマンドを実行した場所(カレントディレクトリ)』が基準になるの。

実行した場所……!

つまり、僕が node-sample の階層にいる状態で node src/path_test.js を叩くと、Node.jsは node-sample/secret.txt を探しに行っちゃうのですね。

その通り。

実務ではアプリケーションを起動する場所が固定されていることが多いけれど、バッチ処理やテストの実行時、あるいは環境によって実行場所が変わることはよくあるわ。

実行する場所のせいでファイルが読めなくなるようなコードは、危なくて現場では使えないの。

じゃあ、どうすればどこから実行しても安全なコードにできるんですか?

そこで使うのが、Node.jsが最初から用意してくれている __dirname という変数と、標準の path モジュール

解決策:pathモジュールと __dirname の組み合わせ

src/path_test.js を次のように書き換えてみて。

これが現場での標準的な書き方よ。

// src/path-test.js
const fs = require('fs');
// 1. pathモジュールを読み込む
const path = require('path');

// 2. __dirname の中身を確認してみる
console.log('__dirname の中身:', __dirname);

try {
    // 3. path.resolve を使って、絶対パスを安全に組み立てる
    const absolutePath = path.resolve(__dirname, 'secret.txt');
    console.log('絶対パス:', absolutePath);

    const data = fs.readFileSync(absolutePath, 'utf8');
    console.log('読み込み成功:', data);
} catch (err) {
    console.error('エラーが発生しました:', err.message);
}

書き換えたら、さっきエラーになった node-sample 階層からもう一度実行してみて。

node src/path_test.js
# 出力結果
__dirname の中身: /home/kumaki/node-sample/src
絶対パス: /home/kumaki/node-sample/src/secret.txt
読み込み成功: 極秘情報

今度はちゃんと読み込めました!

ログを見ると、__dirname にはこの path_test.js が置いてあるフォルダのフルパス(絶対パス)が入っているんですね。

そう。

__dirname は、実行した場所がどこであれ、常に『そのファイルが物理的に置かれているディレクトリの絶対パス』を返してくれるの。

これを使えば、実行場所に左右されないコードが書けるわ。

なるほど。

でも、パスを繋げるだけなら __dirname + '/secret.txt' みたいな文字列の結合じゃダメなんですか?

ダメではないけれど、実務では推奨されないわ。

なぜなら、OSによってパスの区切り文字が違うから。

Windowsはバックスラッシュ(\)だけど、MacやLinux(Ubuntu)はスラッシュ(/)でしょ?

何かの処理で問題が起こらない為にもパスを文字で結合するのは避けた方がよいわ

あ、そっか……。

今僕はWSL2のUbuntu上にいますけど、開発メンバーにはWindowsの生環境で動かす人がいるかもしれないですもんね。

その通り。

path.resolve() などの関数を使えば、Node.jsが動いているOSを自動で判別して、環境に合わせた正しい区切り文字で安全に絶対パスを組み立ててくれるのよ。

本日のまとめ

  1. 標準モジュール(fs/path): require で呼び出す、Node.jsに標準搭載された強力な機能。
  2. Sync(同期)の危険性: 処理中にサーバー全体がフリーズするため実務では原則禁止。async/await による非同期処理が必須。
  3. Node.jsの相対パス(./)の基準: 「ファイルのある場所」ではなく、「コマンドを実行した場所(カレントディレクトリ)」。
  4. __dirnamepath モジュール: OSの違いや実行場所の違いに影響されない、頑丈な絶対パスを組み立てるための必須セット。

次回:第4回「Node.jsのイベントループとシングルスレッドの仕組みを理解する」に続く

タイトルとURLをコピーしました