Node.js プロセス管理:child_process モジュールの活用事例

Node.js でアプリケーションを開発していると、「この処理、メインプロセスで実行すると重すぎてレスポンスが悪くなってしまう...」という経験はありませんか?
そんな時に活躍するのが、Node.js のchild_process
モジュールです。このモジュールを使うことで、重い処理を別プロセスに分離したり、外部コマンドを実行したりできるようになります。
今回は、Node.js におけるプロセス管理の要となるchild_process
モジュールについて、基本的な仕組みから実践的な活用事例まで詳しく解説していきます。
背景
なぜプロセス管理が必要なのか?
Node.js は単一スレッドで動作するため、CPU 集約的な処理や時間のかかる処理を実行すると、その間他のリクエストがブロックされてしまいます。
例えば、以下のような処理を考えてみましょう。
javascript// CPU集約的な処理の例
function heavyCalculation(n) {
let result = 0;
for (let i = 0; i < n; i++) {
result += Math.sqrt(i);
}
return result;
}
// Express.jsでのエンドポイント例
app.get('/heavy', (req, res) => {
// この処理中、他のリクエストは待機状態になる
const result = heavyCalculation(10000000);
res.json({ result });
});
このコードでは、/heavy
エンドポイントにアクセスすると、計算が完了するまで他のすべてのリクエストが待機状態になってしまいます。
単一プロセスの限界
Node.js の単一プロセス実行には以下のような限界があります。
問題点 | 影響 | 具体例 |
---|---|---|
ブロッキング処理 | 他のリクエストが待機状態になる | 大量データの計算処理 |
メモリ制限 | 単一プロセスのメモリ上限に制約される | 大きなファイルの処理 |
障害の影響範囲 | 一つのエラーでアプリ全体が停止 | 未処理例外による強制終了 |
CPU 利用効率 | マルチコアを活用できない | 並列処理が困難 |
マルチプロセスの利点
child_process
モジュールを使ってマルチプロセス化することで、以下のメリットが得られます。
- 非ブロッキング実行: 重い処理を別プロセスで実行し、メインプロセスは他のリクエストを処理し続けられる
- リソース分散: 複数のプロセスで CPU やメモリを効率的に活用
- 障害の局所化: 子プロセスでエラーが発生してもメインプロセスは継続動作
- スケーラビリティ: 処理能力に応じてプロセス数を調整可能
課題
従来の同期処理の問題点
従来の同期処理では、以下のような問題が発生します。
javascriptconst fs = require('fs');
// 同期的なファイル読み込み(問題のあるコード)
app.get('/read-large-file', (req, res) => {
try {
// この処理中、サーバー全体がブロックされる
const data = fs.readFileSync(
'./large-file.txt',
'utf8'
);
const processedData = processLargeData(data);
res.json({ data: processedData });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
function processLargeData(data) {
// 重い処理のシミュレーション
return data
.split('\n')
.map((line) => {
return line.toUpperCase().trim();
})
.filter((line) => line.length > 0);
}
この例では、大きなファイルの読み込みと処理が完了するまで、他のすべてのリクエストが待機状態になってしまいます。
メインプロセスでの重い処理が抱える問題
メインプロセスで重い処理を実行すると、以下の問題が発生します。
- レスポンス性の低下: ユーザーからのリクエストに対する応答が遅くなる
- メモリ使用量の増大: 大量のデータを処理する際にメモリ不足が発生する可能性
- エラー波及: 処理中にエラーが発生すると、アプリケーション全体に影響する
- スケーラビリティの制限: 処理能力の向上が困難
解決策
child_process モジュールの 4 つの主要メソッド
Node.js のchild_process
モジュールには、4 つの主要なメソッドがあります。それぞれ異なる用途と特徴を持っているため、適切な使い分けが重要です。
メソッド | 用途 | 特徴 | 適用場面 |
---|---|---|---|
spawn | 外部コマンド実行 | ストリーミング対応、大量データ処理 | 長時間実行、リアルタイム出力 |
exec | シェルコマンド実行 | バッファリング、簡単な使用 | 短時間実行、結果一括取得 |
execFile | 実行ファイル直接実行 | セキュリティ重視、高速 | バイナリ実行、セキュア環境 |
fork | Node.js スクリプト実行 | IPC 通信、プロセス間データ交換 | CPU 集約処理、ワーカープロセス |
1. spawn メソッド
spawn
は最も基本的なメソッドで、外部コマンドを実行します。ストリーミングに対応しているため、大量のデータを扱う処理に適しています。
javascriptconst { spawn } = require('child_process');
// ファイル一覧を取得する例
function listFiles(directory) {
return new Promise((resolve, reject) => {
// lsコマンドを実行
const ls = spawn('ls', ['-la', directory]);
let output = '';
let errorOutput = '';
// 標準出力からデータを受信
ls.stdout.on('data', (data) => {
output += data.toString();
});
// エラー出力からデータを受信
ls.stderr.on('data', (data) => {
errorOutput += data.toString();
});
// プロセス終了時の処理
ls.on('close', (code) => {
if (code === 0) {
resolve(output);
} else {
reject(
new Error(
`Command failed with code ${code}: ${errorOutput}`
)
);
}
});
});
}
// 使用例
app.get('/files/:directory', async (req, res) => {
try {
const files = await listFiles(req.params.directory);
res.json({ files: files.split('\n') });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
2. exec メソッド
exec
はシェルコマンドを実行し、結果をバッファに格納して一括で返します。簡単な処理に適しています。
javascriptconst { exec } = require('child_process');
const util = require('util');
// execをPromise化
const execAsync = util.promisify(exec);
// Gitの情報を取得する例
async function getGitInfo() {
try {
// 現在のブランチ名を取得
const { stdout: branch } = await execAsync(
'git branch --show-current'
);
// 最新コミットのハッシュを取得
const { stdout: commit } = await execAsync(
'git rev-parse HEAD'
);
// 変更されたファイル数を取得
const { stdout: changes } = await execAsync(
'git status --porcelain | wc -l'
);
return {
branch: branch.trim(),
commit: commit.trim().substring(0, 7),
changes: parseInt(changes.trim()),
};
} catch (error) {
throw new Error(`Git command failed: ${error.message}`);
}
}
// APIエンドポイントとして使用
app.get('/git-info', async (req, res) => {
try {
const gitInfo = await getGitInfo();
res.json(gitInfo);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
3. execFile メソッド
execFile
は実行ファイルを直接実行するため、シェルを経由せずセキュリティ面で優れています。
javascriptconst { execFile } = require('child_process');
const util = require('util');
const execFileAsync = util.promisify(execFile);
// 画像変換ツール(ImageMagick)を使用する例
async function convertImage(inputPath, outputPath, format) {
try {
// convertコマンドを直接実行(シェルを経由しない)
const { stdout, stderr } = await execFileAsync(
'convert',
[
inputPath,
'-format',
format,
'-quality',
'85',
outputPath,
]
);
return { success: true, output: stdout };
} catch (error) {
throw new Error(
`Image conversion failed: ${error.message}`
);
}
}
// 画像変換API
app.post('/convert-image', async (req, res) => {
const { inputPath, outputPath, format } = req.body;
try {
const result = await convertImage(
inputPath,
outputPath,
format
);
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
4. fork メソッド
fork
は Node.js スクリプトを別プロセスで実行し、プロセス間通信(IPC)を可能にします。CPU 集約的な処理に最適です。
javascriptconst { fork } = require('child_process');
// CPU集約的な処理を行うワーカープロセス
function createWorker(scriptPath) {
return new Promise((resolve, reject) => {
// 子プロセスを作成
const worker = fork(scriptPath);
// メッセージ受信の設定
worker.on('message', (result) => {
resolve(result);
worker.kill(); // 処理完了後にプロセスを終了
});
// エラーハンドリング
worker.on('error', (error) => {
reject(error);
});
// プロセス終了時の処理
worker.on('exit', (code) => {
if (code !== 0) {
reject(
new Error(
`Worker process exited with code ${code}`
)
);
}
});
return worker;
});
}
// データ分析API
app.post('/analyze-data', async (req, res) => {
const { data } = req.body;
try {
// ワーカープロセスを作成
const worker = fork('./workers/data-analyzer.js');
// データを子プロセスに送信
worker.send({ data });
// 結果を待機
worker.on('message', (result) => {
res.json(result);
worker.kill();
});
// タイムアウト設定(30秒)
setTimeout(() => {
worker.kill();
res.status(408).json({ error: 'Processing timeout' });
}, 30000);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
対応するワーカーファイル(workers/data-analyzer.js
):
javascript// データ分析を行うワーカープロセス
process.on('message', (message) => {
const { data } = message;
try {
// CPU集約的なデータ分析処理
const result = analyzeData(data);
// 結果を親プロセスに送信
process.send(result);
} catch (error) {
process.send({ error: error.message });
}
});
function analyzeData(data) {
// 複雑な統計計算のシミュレーション
const numbers = data.filter(
(item) => typeof item === 'number'
);
const sum = numbers.reduce((acc, num) => acc + num, 0);
const average = sum / numbers.length;
const variance =
numbers.reduce(
(acc, num) => acc + Math.pow(num - average, 2),
0
) / numbers.length;
const standardDeviation = Math.sqrt(variance);
// より複雑な処理をシミュレーション
const correlations = calculateCorrelations(numbers);
return {
count: numbers.length,
sum,
average,
variance,
standardDeviation,
correlations,
processedAt: new Date().toISOString(),
};
}
function calculateCorrelations(numbers) {
// 相関係数の計算(簡略化)
const correlations = [];
for (let i = 0; i < Math.min(numbers.length, 100); i++) {
for (
let j = i + 1;
j < Math.min(numbers.length, 100);
j++
) {
correlations.push({
pair: [i, j],
correlation: Math.random(), // 実際の計算は省略
});
}
}
return correlations.slice(0, 10); // 上位10件のみ返す
}
具体例
外部コマンド実行(ファイル変換、データベースダンプ)
実際の開発現場でよく使われる外部コマンドの実行例を見てみましょう。
ファイル変換の例
PDF ファイルを画像に変換する処理をchild_process
で実装してみます。
javascriptconst { spawn } = require('child_process');
const path = require('path');
const fs = require('fs').promises;
class FileConverter {
// PDFを画像に変換
static async convertPdfToImages(pdfPath, outputDir) {
return new Promise((resolve, reject) => {
// ImageMagickのconvertコマンドを使用
const convert = spawn('convert', [
'-density',
'300', // 高解像度設定
'-quality',
'90', // 画質設定
pdfPath,
path.join(outputDir, 'page-%03d.jpg'),
]);
let errorOutput = '';
// エラー出力を監視
convert.stderr.on('data', (data) => {
errorOutput += data.toString();
});
// 進行状況を監視(オプション)
convert.stdout.on('data', (data) => {
console.log(`Conversion progress: ${data}`);
});
convert.on('close', async (code) => {
if (code === 0) {
try {
// 変換されたファイル一覧を取得
const files = await fs.readdir(outputDir);
const imageFiles = files.filter(
(file) =>
file.startsWith('page-') &&
file.endsWith('.jpg')
);
resolve({
success: true,
files: imageFiles,
count: imageFiles.length,
});
} catch (error) {
reject(
new Error(
`Failed to read output directory: ${error.message}`
)
);
}
} else {
reject(
new Error(
`Conversion failed with code ${code}: ${errorOutput}`
)
);
}
});
});
}
}
// Express.jsでの使用例
app.post('/convert-pdf', async (req, res) => {
const { pdfPath, outputDir } = req.body;
try {
// 出力ディレクトリの作成
await fs.mkdir(outputDir, { recursive: true });
// PDF変換の実行
const result = await FileConverter.convertPdfToImages(
pdfPath,
outputDir
);
res.json({
message: 'PDF conversion completed successfully',
...result,
});
} catch (error) {
res.status(500).json({
error: 'PDF conversion failed',
details: error.message,
});
}
});
データベースダンプの例
PostgreSQL のデータベースダンプを作成する処理です。
javascriptconst { spawn } = require('child_process');
const fs = require('fs');
class DatabaseManager {
static async createDump(config) {
const { host, port, database, username, outputPath } =
config;
return new Promise((resolve, reject) => {
// pg_dumpコマンドを実行
const pgDump = spawn('pg_dump', [
'-h',
host,
'-p',
port.toString(),
'-U',
username,
'-d',
database,
'--no-password',
'--verbose',
'--format=custom',
]);
// 出力ファイルストリームを作成
const outputStream = fs.createWriteStream(outputPath);
// ダンプデータを出力ファイルにパイプ
pgDump.stdout.pipe(outputStream);
let errorOutput = '';
pgDump.stderr.on('data', (data) => {
errorOutput += data.toString();
// pg_dumpは進行状況をstderrに出力するため、ログとして記録
console.log(`Dump progress: ${data}`);
});
pgDump.on('close', (code) => {
outputStream.end();
if (code === 0) {
resolve({
success: true,
outputPath,
message: 'Database dump completed successfully',
});
} else {
reject(
new Error(
`Database dump failed with code ${code}: ${errorOutput}`
)
);
}
});
});
}
}
// 定期的なバックアップ処理
async function scheduleBackup() {
const config = {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME,
username: process.env.DB_USER,
outputPath: `./backups/backup-${
new Date().toISOString().split('T')[0]
}.dump`,
};
try {
const result = await DatabaseManager.createDump(config);
console.log('Backup completed:', result);
} catch (error) {
console.error('Backup failed:', error.message);
}
}
CPU 集約的処理の分離(画像処理、データ分析)
CPU 集約的な処理を別プロセスに分離することで、メインプロセスのレスポンス性を保つことができます。
画像処理の例
複数の画像に対してリサイズ処理を並列実行する例です。
javascriptconst { fork } = require('child_process');
const path = require('path');
class ImageProcessor {
constructor(maxWorkers = 4) {
this.maxWorkers = maxWorkers;
this.activeWorkers = 0;
this.queue = [];
}
// 画像リサイズ処理
async resizeImage(imagePath, outputPath, width, height) {
return new Promise((resolve, reject) => {
const task = {
imagePath,
outputPath,
width,
height,
resolve,
reject,
};
if (this.activeWorkers < this.maxWorkers) {
this.processTask(task);
} else {
this.queue.push(task);
}
});
}
processTask(task) {
this.activeWorkers++;
// ワーカープロセスを作成
const worker = fork('./workers/image-resizer.js');
// タスクをワーカーに送信
worker.send({
imagePath: task.imagePath,
outputPath: task.outputPath,
width: task.width,
height: task.height,
});
// 結果を受信
worker.on('message', (result) => {
if (result.success) {
task.resolve(result);
} else {
task.reject(new Error(result.error));
}
worker.kill();
this.activeWorkers--;
// キューに待機中のタスクがあれば処理
if (this.queue.length > 0) {
const nextTask = this.queue.shift();
this.processTask(nextTask);
}
});
worker.on('error', (error) => {
task.reject(error);
worker.kill();
this.activeWorkers--;
});
}
// 複数画像の一括処理
async resizeImages(images) {
const promises = images.map((img) =>
this.resizeImage(
img.input,
img.output,
img.width,
img.height
)
);
return Promise.all(promises);
}
}
// 使用例
const processor = new ImageProcessor(4); // 最大4つの並列処理
app.post('/resize-images', async (req, res) => {
const { images } = req.body;
try {
const results = await processor.resizeImages(images);
res.json({
success: true,
processed: results.length,
results,
});
} catch (error) {
res.status(500).json({
error: 'Image processing failed',
details: error.message,
});
}
});
対応するワーカーファイル(workers/image-resizer.js
):
javascriptconst sharp = require('sharp');
process.on('message', async (task) => {
const { imagePath, outputPath, width, height } = task;
try {
// Sharpライブラリを使用して画像をリサイズ
await sharp(imagePath)
.resize(width, height, {
fit: 'inside',
withoutEnlargement: true,
})
.jpeg({ quality: 85 })
.toFile(outputPath);
process.send({
success: true,
imagePath,
outputPath,
dimensions: { width, height },
});
} catch (error) {
process.send({
success: false,
error: error.message,
imagePath,
});
}
});
プロセス間通信(IPC)を活用したワーカープロセス
より高度なプロセス間通信を活用したワーカープロセスの実装例です。
javascriptconst { fork } = require('child_process');
const EventEmitter = require('events');
class WorkerPool extends EventEmitter {
constructor(workerScript, poolSize = 4) {
super();
this.workerScript = workerScript;
this.poolSize = poolSize;
this.workers = [];
this.availableWorkers = [];
this.taskQueue = [];
this.taskId = 0;
this.initializeWorkers();
}
initializeWorkers() {
for (let i = 0; i < this.poolSize; i++) {
this.createWorker();
}
}
createWorker() {
const worker = fork(this.workerScript);
const workerId = this.workers.length;
worker.workerId = workerId;
worker.isBusy = false;
// ワーカーからのメッセージを処理
worker.on('message', (message) => {
this.handleWorkerMessage(worker, message);
});
// ワーカーエラーの処理
worker.on('error', (error) => {
console.error(`Worker ${workerId} error:`, error);
this.restartWorker(workerId);
});
// ワーカー終了の処理
worker.on('exit', (code) => {
if (code !== 0) {
console.error(
`Worker ${workerId} exited with code ${code}`
);
this.restartWorker(workerId);
}
});
this.workers.push(worker);
this.availableWorkers.push(worker);
}
handleWorkerMessage(worker, message) {
const { type, taskId, result, error } = message;
switch (type) {
case 'task_complete':
worker.isBusy = false;
this.availableWorkers.push(worker);
// タスク完了を通知
this.emit('task_complete', {
taskId,
result,
error,
});
// 待機中のタスクがあれば処理
this.processNextTask();
break;
case 'progress':
this.emit('progress', { taskId, progress: result });
break;
}
}
// タスクを実行
executeTask(taskData) {
return new Promise((resolve, reject) => {
const taskId = ++this.taskId;
const task = {
id: taskId,
data: taskData,
resolve,
reject,
};
// タスク完了イベントのリスナーを設定
const onComplete = ({
taskId: completedTaskId,
result,
error,
}) => {
if (completedTaskId === taskId) {
this.removeListener('task_complete', onComplete);
if (error) {
reject(new Error(error));
} else {
resolve(result);
}
}
};
this.on('task_complete', onComplete);
if (this.availableWorkers.length > 0) {
this.assignTask(task);
} else {
this.taskQueue.push(task);
}
});
}
assignTask(task) {
const worker = this.availableWorkers.shift();
worker.isBusy = true;
worker.send({
type: 'execute_task',
taskId: task.id,
data: task.data,
});
}
processNextTask() {
if (
this.taskQueue.length > 0 &&
this.availableWorkers.length > 0
) {
const task = this.taskQueue.shift();
this.assignTask(task);
}
}
restartWorker(workerId) {
// 古いワーカーを削除
const oldWorker = this.workers[workerId];
if (oldWorker) {
oldWorker.kill();
}
// 新しいワーカーを作成
this.createWorker();
}
// プールを終了
terminate() {
this.workers.forEach((worker) => {
worker.kill();
});
this.workers = [];
this.availableWorkers = [];
}
}
// 使用例
const workerPool = new WorkerPool(
'./workers/data-processor.js',
4
);
// 進行状況の監視
workerPool.on('progress', ({ taskId, progress }) => {
console.log(`Task ${taskId} progress: ${progress}%`);
});
// データ処理API
app.post('/process-data', async (req, res) => {
const { data } = req.body;
try {
const result = await workerPool.executeTask(data);
res.json({
success: true,
result,
});
} catch (error) {
res.status(500).json({
error: 'Data processing failed',
details: error.message,
});
}
});
エラーハンドリングとリソース管理
プロセス管理において重要なエラーハンドリングとリソース管理の実装例です。
javascriptconst { spawn, fork } = require('child_process');
class ProcessManager {
constructor() {
this.activeProcesses = new Map();
this.processTimeout = 30000; // 30秒のタイムアウト
}
// プロセス実行(タイムアウト付き)
async executeWithTimeout(command, args, options = {}) {
return new Promise((resolve, reject) => {
const process = spawn(command, args, options);
const processId = Date.now() + Math.random();
// アクティブプロセスとして登録
this.activeProcesses.set(processId, process);
let output = '';
let errorOutput = '';
// 出力の収集
if (process.stdout) {
process.stdout.on('data', (data) => {
output += data.toString();
});
}
if (process.stderr) {
process.stderr.on('data', (data) => {
errorOutput += data.toString();
});
}
// タイムアウト設定
const timeout = setTimeout(() => {
process.kill('SIGTERM');
this.activeProcesses.delete(processId);
reject(
new Error(
`Process timeout after ${this.processTimeout}ms`
)
);
}, this.processTimeout);
// プロセス終了の処理
process.on('close', (code, signal) => {
clearTimeout(timeout);
this.activeProcesses.delete(processId);
if (signal) {
reject(
new Error(`Process killed by signal ${signal}`)
);
} else if (code === 0) {
resolve({ output, errorOutput });
} else {
reject(
new Error(
`Process exited with code ${code}: ${errorOutput}`
)
);
}
});
// プロセスエラーの処理
process.on('error', (error) => {
clearTimeout(timeout);
this.activeProcesses.delete(processId);
reject(
new Error(`Process error: ${error.message}`)
);
});
});
}
// リソース使用量の監視
async monitorResources(pid) {
try {
const { output } = await this.executeWithTimeout(
'ps',
[
'-p',
pid.toString(),
'-o',
'pid,ppid,%cpu,%mem,vsz,rss,comm',
]
);
const lines = output.trim().split('\n');
if (lines.length < 2) {
throw new Error('Process not found');
}
const data = lines[1].trim().split(/\s+/);
return {
pid: parseInt(data[0]),
ppid: parseInt(data[1]),
cpu: parseFloat(data[2]),
memory: parseFloat(data[3]),
vsz: parseInt(data[4]),
rss: parseInt(data[5]),
command: data[6],
};
} catch (error) {
throw new Error(
`Resource monitoring failed: ${error.message}`
);
}
}
// 全アクティブプロセスの終了
terminateAllProcesses() {
for (const [processId, process] of this
.activeProcesses) {
try {
process.kill('SIGTERM');
// 強制終了のタイムアウト
setTimeout(() => {
if (!process.killed) {
process.kill('SIGKILL');
}
}, 5000);
} catch (error) {
console.error(
`Failed to terminate process ${processId}:`,
error
);
}
}
this.activeProcesses.clear();
}
// プロセス統計の取得
getProcessStats() {
return {
activeProcesses: this.activeProcesses.size,
processes: Array.from(this.activeProcesses.keys()),
};
}
}
// グローバルプロセスマネージャー
const processManager = new ProcessManager();
// アプリケーション終了時のクリーンアップ
process.on('SIGINT', () => {
console.log('Shutting down gracefully...');
processManager.terminateAllProcesses();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('Received SIGTERM, shutting down...');
processManager.terminateAllProcesses();
process.exit(0);
});
// プロセス統計API
app.get('/process-stats', (req, res) => {
const stats = processManager.getProcessStats();
res.json(stats);
});
まとめ
プロセス管理のベストプラクティス
Node.js におけるchild_process
モジュールの活用について、以下のベストプラクティスを心がけましょう。
項目 | ベストプラクティス | 理由 |
---|---|---|
メソッド選択 | 用途に応じた適切なメソッドの選択 | パフォーマンスとセキュリティの最適化 |
エラーハンドリング | 必ずerror イベントとexit イベントを監視 | プロセス異常終了への対応 |
タイムアウト設定 | 長時間実行プロセスにはタイムアウトを設定 | リソースリークの防止 |
リソース管理 | プロセス終了時の適切なクリーンアップ | メモリリークの防止 |
セキュリティ | 外部入力の検証とサニタイゼーション | インジェクション攻撃の防止 |
注意点
- プロセス数の制限: 同時実行プロセス数を制限し、システムリソースを適切に管理する
- メモリ使用量の監視: 子プロセスのメモリ使用量を定期的に監視する
- セキュリティ対策: 外部コマンド実行時は入力値の検証を徹底する
- エラー処理: 子プロセスのエラーが親プロセスに影響しないよう適切に処理する
- ログ管理: プロセスの実行状況とエラーを適切にログに記録する
child_process
モジュールを適切に活用することで、Node.js アプリケーションのパフォーマンスと安定性を大幅に向上させることができます。重い処理を別プロセスに分離し、メインプロセスのレスポンス性を保ちながら、スケーラブルなアプリケーションを構築していきましょう。
関連リンク
- review
もう三日坊主とはサヨナラ!『続ける思考』井上新八
- review
チーム開発が劇的に変わった!『リーダブルコード』Dustin Boswell & Trevor Foucher
- review
アジャイル初心者でも大丈夫!『アジャイルサムライ − 達人開発者への道』Jonathan Rasmusson
- review
人生が作品になる!『自分の中に毒を持て』岡本太郎
- review
体調不良の 99%が解決!『眠れなくなるほど面白い 図解 自律神経の話』小林弘幸著で学ぶ、現代人必須の自律神経コントロール術と人生を変える健康革命
- review
衝撃の事実!『睡眠こそ最強の解決策である』マシュー・ウォーカー著が明かす、99%の人が知らない睡眠の驚くべき真実と人生を変える科学的メカニズム