๐ŸŽฎ ๊ฒŒ์ž„ ์„œ๋ฒ„ ํ”„๋กœ๊ทธ๋ž˜๋ฐ
ํ•™์Šต ํ—ˆ๋ธŒ

๋ชจ๋ฐ”์ผ RTS ์„œ๋ฒ„ ํ”„๋กœ๊ทธ๋ž˜๋จธ๊ฐ€ ๋˜๊ธฐ ์œ„ํ•œ ๋ชจ๋“  ๊ฒƒ์„ ๋‹จ๊ณ„๋ณ„๋กœ ํ•™์Šตํ•˜์„ธ์š”. C#, Node.js, WebSocket, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ถ€ํ„ฐ ์‹ค์‹œ๊ฐ„ PvP ์„œ๋ฒ„ ๊ตฌํ˜„๊นŒ์ง€!

C# Node.js MySQL Linux WebSocket

๐Ÿ—บ๏ธ ํ•™์Šต ๋กœ๋“œ๋งต

์•„๋ž˜ ์ˆœ์„œ๋Œ€๋กœ ํ•™์Šตํ•˜๋ฉด ๊ฒŒ์ž„ ์„œ๋ฒ„ ๊ฐœ๋ฐœ์ž์˜ ๊ธฐ์ดˆ๋ฅผ ํƒ„ํƒ„ํžˆ ์Œ“์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

1

C# ๊ธฐ์ดˆ

๋ณ€์ˆ˜, ํ•จ์ˆ˜, ํด๋ž˜์Šค

2

Node.js

์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ ๊ธฐ์ดˆ

3

๋„คํŠธ์›Œํฌ

TCP/UDP, HTTP

4

WebSocket

์‹ค์‹œ๊ฐ„ ์–‘๋ฐฉํ–ฅ ํ†ต์‹ 

5

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

MySQL & MongoDB

6

Linux ์„œ๋ฒ„

์„œ๋ฒ„ ์šด์˜ ๊ธฐ์ดˆ

7

๊ฒŒ์ž„ ์„œ๋ฒ„

๋™๊ธฐํ™” & ๋ฃจํ”„

๐Ÿ“š ํ•™์Šต ์ฃผ์ œ

์นด๋“œ๋ฅผ ํด๋ฆญํ•˜๋ฉด ์ƒ์„ธ ๋‚ด์šฉ๊ณผ ์ฝ”๋“œ ์˜ˆ์ œ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”ท

1. C# ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๊ธฐ์ดˆ

โญ ์ž…๋ฌธ

C#์€ ๊ฒŒ์ž„ ๊ฐœ๋ฐœ์—์„œ ๊ฐ€์žฅ ๋„๋ฆฌ ์‚ฌ์šฉ๋˜๋Š” ์–ธ์–ด์ž…๋‹ˆ๋‹ค. Unity ๊ฒŒ์ž„ ์—”์ง„๋„ C#์„ ์‚ฌ์šฉํ•˜์ฃ . ์„œ๋ฒ„ ๊ฐœ๋ฐœ์—์„œ๋„ ASP.NET์„ ํ™œ์šฉํ•œ ๊ณ ์„ฑ๋Šฅ ์„œ๋ฒ„ ๊ตฌ์ถ•์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก ์™œ C#์„ ๋ฐฐ์›Œ์•ผ ํ•˜๋‚˜์š”?

  • ๊ฒŒ์ž„ ์—…๊ณ„ ํ‘œ์ค€: Unity + ์„œ๋ฒ„ ์–‘์ชฝ์—์„œ ์‚ฌ์šฉ
  • ๊ฐ•๋ ฅํ•œ ํƒ€์ž… ์‹œ์Šคํ…œ: ๋ฒ„๊ทธ๋ฅผ ์ค„์—ฌ์คŒ
  • ๋น„๋™๊ธฐ ์ง€์›: async/await๋กœ ํšจ์œจ์ ์ธ ์„œ๋ฒ„ ๊ตฌํ˜„
  • ๋†’์€ ์„ฑ๋Šฅ: .NET 8+ ์—์„œ ๋งค์šฐ ๋น ๋ฆ„
C#
// C# ๊ธฐ๋ณธ ์ž๋ฃŒํ˜• (Data Types)

// ์ˆซ์žํ˜•
int hp = 100;           // ์ •์ˆ˜: -21์–ต ~ 21์–ต
float speed = 3.5f;      // ์‹ค์ˆ˜ (์†Œ์ˆ˜์ )
double damage = 125.75;   // ์ •๋ฐ€ ์‹ค์ˆ˜

// ๋ฌธ์ž์—ด
string name = "์ „์‚ฌ";     // ํ…์ŠคํŠธ

// ์ฐธ/๊ฑฐ์ง“
bool isAlive = true;     // true ๋˜๋Š” false

// ๊ฒŒ์ž„์—์„œ ์ž์ฃผ ์“ฐ์ด๋Š” ํŒจํ„ด
int[] scores = { 100, 85, 92 };  // ๋ฐฐ์—ด
List<string> players = new();    // ๋™์  ๋ฆฌ์ŠคํŠธ

// ๋”•์…”๋„ˆ๋ฆฌ (key-value ์Œ)
Dictionary<string, int> inventory = new()
{
    { "๊ฒ€", 1 },
    { "ํฌ์…˜", 5 }
};

// ๊ฒŒ์ž„์—์„œ ์‚ฌ์šฉ ์˜ˆ์‹œ
Console.WriteLine($"{name}์˜ HP: {hp}");
// ์ถœ๋ ฅ: ์ „์‚ฌ์˜ HP: 100

๐ŸŽฏ ์‹ค์ „ ํฌ์ธํŠธ

๊ฒŒ์ž„ ์„œ๋ฒ„์—์„œ๋Š” int๋กœ HP/์ ์ˆ˜, float๋กœ ์ขŒํ‘œ/์†๋„, string์œผ๋กœ ์œ ์ €ID, Dictionary๋กœ ํ”Œ๋ ˆ์ด์–ด ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

C#
// ํ•จ์ˆ˜ (Method) - ๋™์ž‘์„ ์ •์˜

// ๊ธฐ๋ณธ ํ•จ์ˆ˜
static int CalculateDamage(int attack, int defense)
{
    int damage = attack - defense;
    return Math.Max(damage, 0); // ์ตœ์†Œ 0
}

// ๊ธฐ๋ณธ๊ฐ’์ด ์žˆ๋Š” ๋งค๊ฐœ๋ณ€์ˆ˜
static void Heal(ref int hp, int amount = 20)
{
    hp += amount;
    Console.WriteLine($"ํšŒ๋ณต! HP: {hp}");
}

// ์—ฌ๋Ÿฌ ๊ฐ’ ๋ฐ˜ํ™˜ (ํŠœํ”Œ)
static (int x, int y) GetPosition()
{
    return (100, 200);
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
int dmg = CalculateDamage(50, 20);
Console.WriteLine($"๋ฐ๋ฏธ์ง€: {dmg}");  // ๋ฐ๋ฏธ์ง€: 30

int playerHp = 80;
Heal(ref playerHp);        // ํšŒ๋ณต! HP: 100

var pos = GetPosition();
Console.WriteLine($"์œ„์น˜: ({pos.x}, {pos.y})");
C#
// ํด๋ž˜์Šค - ๊ฒŒ์ž„ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ์„ค๊ณ„๋„

class Player
{
    // ์†์„ฑ (Properties)
    public string Name { get; set; }
    public int Hp { get; private set; }
    public int Attack { get; set; }

    // ์ƒ์„ฑ์ž (Constructor)
    public Player(string name, int hp, int attack)
    {
        Name = name;
        Hp = hp;
        Attack = attack;
    }

    // ๋ฉ”์„œ๋“œ
    public void TakeDamage(int damage)
    {
        Hp -= damage;
        if (Hp < 0) Hp = 0;
        Console.WriteLine($"{Name}์ด {damage} ๋ฐ๋ฏธ์ง€! HP: {Hp}");
    }

    public bool IsAlive() => Hp > 0;
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
var warrior = new Player("์ „์‚ฌ", 100, 25);
var mage = new Player("๋งˆ๋ฒ•์‚ฌ", 70, 40);

warrior.TakeDamage(mage.Attack);
// ์ถœ๋ ฅ: ์ „์‚ฌ์ด 40 ๋ฐ๋ฏธ์ง€! HP: 60

๐Ÿ—๏ธ ๊ฒŒ์ž„ ์„œ๋ฒ„์—์„œ ํด๋ž˜์Šค ํ™œ์šฉ

  • Player: ์ ‘์†ํ•œ ์œ ์ € ์ •๋ณด ๊ด€๋ฆฌ
  • GameRoom: ๋งค์น˜/๋ฐฉ ๊ด€๋ฆฌ
  • Unit: ๊ฒŒ์ž„ ์† ์œ ๋‹› (๋ณ‘์‚ฌ, ๊ฑด๋ฌผ ๋“ฑ)
  • Packet: ์„œ๋ฒ„-ํด๋ผ์ด์–ธํŠธ ๊ฐ„ ์ฃผ๊ณ ๋ฐ›๋Š” ๋ฐ์ดํ„ฐ
C#
// ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ - ๊ฒŒ์ž„ ์„œ๋ฒ„์˜ ํ•ต์‹ฌ!
// async/await = ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋™์•ˆ ๋‹ค๋ฅธ ์ผ์„ ํ•  ์ˆ˜ ์žˆ์Œ

class GameServer
{
    // ๋น„๋™๊ธฐ๋กœ ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ ๋ฐ›๊ธฐ
    public async Task StartServer()
    {
        Console.WriteLine("์„œ๋ฒ„ ์‹œ์ž‘!");
        
        while (true)
        {
            // ํ”Œ๋ ˆ์ด์–ด ์—ฐ๊ฒฐ์„ ๊ธฐ๋‹ค๋ฆผ (๋‹ค๋ฅธ ์ž‘์—… ๊ฐ€๋Šฅ)
            var player = await AcceptPlayer();
            Console.WriteLine($"{player} ์ ‘์†!");
            
            // ๋น„๋™๊ธฐ๋กœ ๊ฒŒ์ž„ ์ฒ˜๋ฆฌ (blocking ์—†์Œ)
            _ = HandlePlayer(player); // fire-and-forget
        }
    }

    async Task<string> AcceptPlayer()
    {
        await Task.Delay(1000); // 1์ดˆ ๋Œ€๊ธฐ
        return "Player_001";
    }

    async Task HandlePlayer(string player)
    {
        // ๋น„๋™๊ธฐ๋กœ DB์—์„œ ์œ ์ € ๋ฐ์ดํ„ฐ ๋กœ๋“œ
        var data = await LoadFromDB(player);
        await SendToClient(player, data);
    }

    async Task<string> LoadFromDB(string playerId)
    {
        await Task.Delay(100);
        return $"{{\"hp\": 100, \"score\": 0}}";
    }

    async Task SendToClient(string player, string data)
    {
        await Task.Delay(50);
        Console.WriteLine($"{player}์—๊ฒŒ ๋ฐ์ดํ„ฐ ์ „์†ก ์™„๋ฃŒ");
    }
}

โšก ์™œ ๋น„๋™๊ธฐ๊ฐ€ ์ค‘์š”ํ• ๊นŒ์š”?

๋™๊ธฐ(Sync) ๋ฐฉ์‹์€ ํ•œ ๋ช…์˜ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋™์•ˆ ๋‹ค๋ฅธ ๋ชจ๋“  ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ๊ธฐ๋‹ค๋ ค์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋น„๋™๊ธฐ(Async)๋Š” ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋™์•ˆ ๋‹ค๋ฅธ ํ”Œ๋ ˆ์ด์–ด๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด ์ˆ˜์ฒœ ๋ช… ๋™์‹œ์ ‘์†์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๐ŸŸข

2. Node.js ์„œ๋ฒ„ ๊ธฐ์ดˆ

โญ ์ž…๋ฌธ

Node.js๋Š” JavaScript๋กœ ์„œ๋ฒ„๋ฅผ ๋งŒ๋“œ๋Š” ๋Ÿฐํƒ€์ž„์ž…๋‹ˆ๋‹ค. ๋น„๋™๊ธฐ์— ์ตœ์ ํ™”๋˜์–ด ์žˆ์–ด ์‹ค์‹œ๊ฐ„ ๊ฒŒ์ž„ ์„œ๋ฒ„์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก Node.js ์žฅ์ 

  • ์‰ฌ์šด ๋ฌธ๋ฒ•: JavaScript๋ฅผ ์ด๋ฏธ ์•Œ๋ฉด ๋ฐ”๋กœ ์‹œ์ž‘
  • ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜: ์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ์— ํƒ์›”
  • npm ์ƒํƒœ๊ณ„: ํ•„์š”ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ํ’๋ถ€
  • ๋น ๋ฅธ ๊ฐœ๋ฐœ: C#๋ณด๋‹ค prototype์ด ๋น ๋ฆ„
JavaScript (Node.js)
// server.js - ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ HTTP ์„œ๋ฒ„
const http = require('http');

// ์„œ๋ฒ„ ์ƒ์„ฑ
const server = http.createServer((req, res) => {
  // ๋ชจ๋“  ์š”์ฒญ์— ์‘๋‹ต
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({
    message: '๊ฒŒ์ž„ ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰์ค‘์ž…๋‹ˆ๋‹ค!',
    status: 'online',
    players: 0
  }));
});

// ํฌํŠธ 3000์—์„œ ๋Œ€๊ธฐ
server.listen(3000, () => {
  console.log('๐ŸŽฎ ์„œ๋ฒ„ ์‹œ์ž‘: http://localhost:3000');
});

์‹คํ–‰: node server.js โ†’ ๋ธŒ๋ผ์šฐ์ €์—์„œ localhost:3000 ์ ‘์†

JavaScript (Express)
// Express๋กœ ๊ฒŒ์ž„ API ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ
// ์„ค์น˜: npm install express
const express = require('express');
const app = express();
app.use(express.json());

// ํ”Œ๋ ˆ์ด์–ด ๋ฐ์ดํ„ฐ (๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅ)
const players = new Map();

// [POST] ํ”Œ๋ ˆ์ด์–ด ์ƒ์„ฑ
app.post('/api/players', (req, res) => {
  const { name } = req.body;
  const player = {
    id: P${players.size + 1},
    name,
    hp: 100,
    score: 0,
    createdAt: new Date()
  };
  players.set(player.id, player);
  res.json({ success: true, player });
});

// [GET] ํ”Œ๋ ˆ์ด์–ด ์กฐํšŒ
app.get('/api/players/:id', (req, res) => {
  const player = players.get(req.params.id);
  if (!player) {
    return res.status(404).json({ error: 'ํ”Œ๋ ˆ์ด์–ด ์—†์Œ' });
  }
  res.json(player);
});

// ์„œ๋ฒ„ ์‹œ์ž‘
app.listen(3000, () => {
  console.log('๐ŸŽฎ API ์„œ๋ฒ„ ์‹œ์ž‘!');
});
JavaScript (Event)
// EventEmitter - ๊ฒŒ์ž„ ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ
const EventEmitter = require('events');
const gameEvents = new EventEmitter();

// ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก
gameEvents.on('playerJoin', (player) => {
  console.log(`๐ŸŽฎ ${player}๋‹˜์ด ์ž…์žฅ!`);
});

gameEvents.on('playerAttack', (attacker, target, damage) => {
  console.log(`โš”๏ธ ${attacker} โ†’ ${target}: ${damage} ๋ฐ๋ฏธ์ง€`);
});

gameEvents.on('gameOver', (winner) => {
  console.log(`๐Ÿ† ${winner} ์Šน๋ฆฌ!`);
});

// ๊ฒŒ์ž„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
gameEvents.emit('playerJoin', '์ „์‚ฌ');
gameEvents.emit('playerJoin', '๋งˆ๋ฒ•์‚ฌ');
gameEvents.emit('playerAttack', '์ „์‚ฌ', '๋งˆ๋ฒ•์‚ฌ', 25);
gameEvents.emit('gameOver', '์ „์‚ฌ');

๐ŸŽฏ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜์ด๋ž€?

"A๊ฐ€ ์ผ์–ด๋‚˜๋ฉด B๋ฅผ ํ•ด๋ผ"๋ฅผ ๋ฏธ๋ฆฌ ๋“ฑ๋กํ•ด๋‘๋Š” ๋ฐฉ์‹. ๊ฒŒ์ž„์—์„œ ํ”Œ๋ ˆ์ด์–ด ์ ‘์†, ๊ณต๊ฒฉ, ์ฃฝ์Œ ๋“ฑ์„ ์ด๋ฒคํŠธ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉด ์ฝ”๋“œ๊ฐ€ ๊น”๋”ํ•ด์ง‘๋‹ˆ๋‹ค.

๐ŸŒ

3. ๋„คํŠธ์›Œํฌ ๊ธฐ์ดˆ (TCP/UDP)

โญโญ ์ค‘๊ธ‰

๊ฒŒ์ž„ ์„œ๋ฒ„์˜ ํ•ต์‹ฌ์€ ๋„คํŠธ์›Œํฌ์ž…๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ์ฃผ๊ณ ๋ฐ›์„์ง€ ์ดํ•ดํ•ด์•ผ ํ•ด์š”.

ํ•ญ๋ชฉ TCP UDP
์‹ ๋ขฐ์„ฑ โœ… ๋ฐ์ดํ„ฐ ๋ณด์žฅ (์žฌ์ „์†ก) โŒ ๋ณด์žฅ ์•ˆ ๋จ (ไธขๅคฑ ๊ฐ€๋Šฅ)
์†๋„ ์ƒ๋Œ€์ ์œผ๋กœ ๋А๋ฆผ ๋งค์šฐ ๋น ๋ฆ„
์—ฐ๊ฒฐ ์—ฐ๊ฒฐ ํ•„์š” (3-way handshake) ์—ฐ๊ฒฐ ์—†์ด ์ „์†ก
๊ฒŒ์ž„ ์šฉ๋„ ๋กœ๊ทธ์ธ, ์ฑ„ํŒ…, ์•„์ดํ…œ, ๊ฒฐ์ œ ์‹ค์‹œ๊ฐ„ ์ „ํˆฌ, ์ด๋™, ์Šคํ‚ฌ

๐ŸŽฎ ๊ฒŒ์ž„์—์„œ์˜ ํ™œ์šฉ

  • TCP: "์•„์ดํ…œ์„ ๊ตฌ๋งคํ–ˆ๋‹ค" โ†’ ๋ฐ์ดํ„ฐไธขๅคฑ๋˜๋ฉด ์•ˆ ๋จ
  • UDP: "์บ๋ฆญํ„ฐ๊ฐ€ x,y๋กœ ์ด๋™์ค‘" โ†’ไธขไบ†? ๋‹ค์Œ ํŒจํ‚ท์ด ๊ณง ์˜ด
  • RTS ๊ฒŒ์ž„: ๋Œ€๋ถ€๋ถ„ TCP ์‚ฌ์šฉ (๋ช…๋ น้กบๅบ๊ฐ€ ์ค‘์š”)
Node.js - TCP ์„œ๋ฒ„
// TCP ์†Œ์ผ“ ์„œ๋ฒ„ - ๊ฒŒ์ž„ ์„œ๋ฒ„์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ
const net = require('net');
const clients = new Set();

const server = net.createServer((socket) => {
  console.log('๐Ÿ”— ์ƒˆ ํ”Œ๋ ˆ์ด์–ด ์—ฐ๊ฒฐ!');
  clients.add(socket);

  // ๋ฐ์ดํ„ฐ ์ˆ˜์‹ 
  socket.on('data', (data) => {
    const message = data.toString();
    console.log(`๐Ÿ“จ ์ˆ˜์‹ : ${message}`);
    
    // ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ
    for (const client of clients) {
      if (client !== socket) {
        client.write(message);
      }
    }
  });

  // ์—ฐ๊ฒฐ ํ•ด์ œ
  socket.on('end', () => {
    clients.delete(socket);
    console.log('๐Ÿ‘‹ ํ”Œ๋ ˆ์ด์–ด ํ‡ด์žฅ');
  });
});

server.listen(5000, () => {
  console.log('๐ŸŽฎ TCP ๊ฒŒ์ž„ ์„œ๋ฒ„: ํฌํŠธ 5000');
});
๐Ÿ”Œ

4. WebSocket ์‹ค์‹œ๊ฐ„ ํ†ต์‹ 

โญโญ ์ค‘๊ธ‰

WebSocket์€ HTTP๋ฅผ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜์—ฌ ์–‘๋ฐฉํ–ฅ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ๊ฒŒ์ž„์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” ํ†ต์‹  ๋ฐฉ์‹์ด์—์š”.

๐Ÿ“ก HTTP vs WebSocket

  • HTTP: ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์š”์ฒญ โ†’ ์„œ๋ฒ„๊ฐ€ ์‘๋‹ต (๋‹จ๋ฐฉํ–ฅ, ์ผํšŒ์„ฑ)
  • WebSocket: ์—ฐ๊ฒฐ ์œ ์ง€ โ†’ ์–‘์ชฝ์ด ์ž์œ ๋กญ๊ฒŒ ์ „์†ก (์‹ค์‹œ๊ฐ„)
  • ๊ฒŒ์ž„: WebSocket์œผ๋กœ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฒŒ์ž„ ์ƒํƒœ๋ฅผ ์ฃผ๊ณ ๋ฐ›์Œ
Node.js + ws ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
// WebSocket ๊ฒŒ์ž„ ์„œ๋ฒ„
// ์„ค์น˜: npm install ws
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

// ๊ฒŒ์ž„ ์ƒํƒœ
const gameState = {
  players: new Map(),
  bullets: []
};

wss.on('connection', (ws) => {
  const playerId = P${gameState.players.size + 1};
  console.log(`๐ŸŽฎ ${playerId} ์ ‘์†!`);

  // ์ดˆ๊ธฐ ์ƒํƒœ ์ „์†ก
  ws.send(JSON.stringify({
    type: 'init',
    playerId,
    players: Object.fromEntries(gameState.players)
  }));

  // ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ 
  ws.on('message', (data) => {
    const msg = JSON.parse(data);
    
    switch (msg.type) {
      case 'move':
        // ์ด๋™ โ†’ ๋ชจ๋“  ํ”Œ๋ ˆ์ด์–ด์—๊ฒŒ ์•Œ๋ฆผ
        gameState.players.set(playerId, {
          x: msg.x, y: msg.y
        });
        broadcast({
          type: 'playerMoved',
          playerId,
          x: msg.x,
          y: msg.y
        });
        break;

      case 'attack':
        // ๊ณต๊ฒฉ โ†’ ๋ฐ๋ฏธ์ง€ ๊ณ„์‚ฐ ํ›„ ์ „ํŒŒ
        broadcast({
          type: 'attack',
          attacker: playerId,
          target: msg.targetId,
          damage: msg.damage
        });
        break;
    }
  });

  // ์—ฐ๊ฒฐ ํ•ด์ œ
  ws.on('close', () => {
    gameState.players.delete(playerId);
    broadcast({ type: 'playerLeft', playerId });
  });
});

// ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „์†ก
function broadcast(data) {
  const msg = JSON.stringify(data);
  wss.clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(msg);
    }
  });
}

console.log('๐Ÿ”Œ WebSocket ์„œ๋ฒ„: ws://localhost:8080');
HTML + JavaScript
<!-- ๊ฒŒ์ž„ ํด๋ผ์ด์–ธํŠธ (๋ธŒ๋ผ์šฐ์ €) -->
<script>
const ws = new WebSocket('ws://localhost:8080');

ws.onopen = () => {
  console.log('์„œ๋ฒ„์— ์—ฐ๊ฒฐ๋จ!');
};

// ์„œ๋ฒ„์—์„œ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ 
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  
  switch (msg.type) {
    case 'init':
      console.log(`๋‚ด ID: ${msg.playerId}`);
      break;
    case 'playerMoved':
      updatePlayerPosition(msg.playerId, msg.x, msg.y);
      break;
    case 'attack':
      showAttackEffect(msg.attacker, msg.target, msg.damage);
      break;
  }
};

// ์„œ๋ฒ„๋กœ ์ด๋™ ๋ฐ์ดํ„ฐ ์ „์†ก
function sendMove(x, y) {
  ws.send(JSON.stringify({ type: 'move', x, y }));
}

// ์„œ๋ฒ„๋กœ ๊ณต๊ฒฉ ์ „์†ก
function sendAttack(targetId, damage) {
  ws.send(JSON.stringify({
    type: 'attack', targetId, damage
  }));
}
</script>
๐Ÿ—„๏ธ

5. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (MySQL)

โญโญ ์ค‘๊ธ‰

ํ”Œ๋ ˆ์ด์–ด ์ •๋ณด, ์•„์ดํ…œ, ์ „์  ๋“ฑ์„ ์˜๊ตฌ ์ €์žฅํ•˜๋ ค๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

DBํŠน์ง•๊ฒŒ์ž„์—์„œ์˜ ์šฉ๋„
MySQL ๊ด€๊ณ„ํ˜•, SQL ๋ฌธ๋ฒ• ์œ ์ € ์ •๋ณด, ์•„์ดํ…œ, ์ „์ 
MongoDB NoSQL, JSON ํ˜•ํƒœ ๋กœ๊ทธ, ์„ค์ •, ์œ ์—ฐํ•œ ๋ฐ์ดํ„ฐ
SQL
-- ๊ฒŒ์ž„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒ์„ฑ
CREATE DATABASE game_server;
USE game_server;

-- ํ”Œ๋ ˆ์ด์–ด ํ…Œ์ด๋ธ”
CREATE TABLE players (
  id        INT AUTO_INCREMENT PRIMARY KEY,
  username  VARCHAR(50) UNIQUE NOT NULL,
  email     VARCHAR(100),
  level     INT DEFAULT 1,
  gold      INT DEFAULT 0,
  hp        INT DEFAULT 100,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- ์•„์ดํ…œ ํ…Œ์ด๋ธ”
CREATE TABLE items (
  id        INT AUTO_INCREMENT PRIMARY KEY,
  name      VARCHAR(50),
  type      ENUM('weapon', 'armor', 'potion'),
  power     INT
);

-- CRUD ๊ธฐ๋ณธ ์˜ˆ์ œ
-- ์ƒ์„ฑ (Create)
INSERT INTO players (username, email) VALUES ('์ „์‚ฌ', 'warrior@game.com');

-- ์กฐํšŒ (Read)
SELECT * FROM players WHERE level >= 10;

-- ์ˆ˜์ • (Update)
UPDATE players SET gold = gold + 100 WHERE username = '์ „์‚ฌ';

-- ์‚ญ์ œ (Delete)
DELETE FROM players WHERE hp <= 0;
Node.js + MySQL
// MySQL ์—ฐ๋™ - ์„ค์น˜: npm install mysql2
const mysql = require('mysql2/promise');

// ์—ฐ๊ฒฐ ํ’€ ์ƒ์„ฑ (์„ฑ๋Šฅ ์ตœ์ ํ™”)
const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'game_server',
  waitForConnections: true,
  connectionLimit: 10
});

// ํ”Œ๋ ˆ์ด์–ด ์กฐํšŒ
async function getPlayer(username) {
  const [rows] = await pool.execute(
    'SELECT * FROM players WHERE username = ?',
    [username]
  );
  return rows[0];
}

// ํ”Œ๋ ˆ์ด์–ด ์ƒ์„ฑ
async function createPlayer(username, email) {
  const [result] = await pool.execute(
    'INSERT INTO players (username, email) VALUES (?, ?)',
    [username, email]
  );
  return result.insertId;
}

// ๊ณจ๋“œ ์—…๋ฐ์ดํŠธ
async function addGold(playerId, amount) {
  await pool.execute(
    'UPDATE players SET gold = gold + ? WHERE id = ?',
    [amount, playerId]
  );
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
async function main() {
  const id = await createPlayer('๋งˆ๋ฒ•์‚ฌ', 'mage@game.com');
  await addGold(id, 500);
  const player = await getPlayer('๋งˆ๋ฒ•์‚ฌ');
  console.log(player);
  // { id: 1, username: '๋งˆ๋ฒ•์‚ฌ', gold: 500, ... }
}
๐Ÿง

6. Linux ์„œ๋ฒ„ ์šด์˜

โญ ์ž…๋ฌธ

๊ฒŒ์ž„ ์„œ๋ฒ„๋Š” ๋Œ€๋ถ€๋ถ„ Linux์—์„œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ๋ช…๋ น์–ด๋ฅผ ์•Œ์•„๋‘๋ฉด ์„œ๋ฒ„๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”.

Bash
# ํŒŒ์ผ & ๋””๋ ‰ํ† ๋ฆฌ
ls -la          # ํŒŒ์ผ ๋ชฉ๋ก (์ˆจ๊น€ ํŒŒ์ผ ํฌํ•จ)
cd /home        # ๋””๋ ‰ํ† ๋ฆฌ ์ด๋™
mkdir game      # ํด๋” ์ƒ์„ฑ
cp file1 file2  # ํŒŒ์ผ ๋ณต์‚ฌ
mv old new      # ํŒŒ์ผ ์ด๋™/์ด๋ฆ„ ๋ณ€๊ฒฝ
rm file.txt     # ํŒŒ์ผ ์‚ญ์ œ

# ํŒŒ์ผ ๋‚ด์šฉ ๋ณด๊ธฐ
cat server.log  # ์ „์ฒด ์ถœ๋ ฅ
tail -f log.txt # ์‹ค์‹œ๊ฐ„ ๋กœ๊ทธ ๋ชจ๋‹ˆํ„ฐ๋ง โญ
head -20 log.txt # ์•ž 20์ค„
grep "error" log.txt # "error" ํฌํ•จ ์ค„ ์ฐพ๊ธฐ

# ๋„คํŠธ์›Œํฌ
curl http://localhost:3000  # HTTP ์š”์ฒญ ํ…Œ์ŠคํŠธ
netstat -tlnp    # ์—ด๋ฆฐ ํฌํŠธ ํ™•์ธ
ping google.com  # ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ํ™•์ธ

# ํŒŒ์ผ ํŽธ์ง‘ (nano ์—๋””ํ„ฐ)
nano server.js   # ํŒŒ์ผ ํŽธ์ง‘ (Ctrl+X โ†’ Y โ†’ Enter ์ €์žฅ)

# ๊ถŒํ•œ ๊ด€๋ฆฌ
chmod +x start.sh  # ์‹คํ–‰ ๊ถŒํ•œ ๋ถ€์—ฌ
ls -la            # ๊ถŒํ•œ ํ™•์ธ
Bash - ํ”„๋กœ์„ธ์Šค ๊ด€๋ฆฌ
# ํ”„๋กœ์„ธ์Šค ํ™•์ธ
ps aux           # ๋ชจ๋“  ํ”„๋กœ์„ธ์Šค ๋ชฉ๋ก
ps aux | grep node # node ํ”„๋กœ์„ธ์Šค๋งŒ ์ฐพ๊ธฐ
top              # ์‹ค์‹œ๊ฐ„ CPU/๋ฉ”๋ชจ๋ฆฌ ๋ชจ๋‹ˆํ„ฐ

# ํ”„๋กœ์„ธ์Šค ์ข…๋ฃŒ
kill PID         # ์ •์ƒ ์ข…๋ฃŒ (์‹œ๊ทธ๋„ 15)
kill -9 PID      # ๊ฐ•์ œ ์ข…๋ฃŒ (์‹œ๊ทธ๋„ 9)

# โ˜… ๊ฒŒ์ž„ ์„œ๋ฒ„๋ฅผ ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ์‹คํ–‰ (์ค‘์š”!)
nohup node server.js > output.log 2>&1 &
# nohup: ํ„ฐ๋ฏธ๋„ ๋‹ซ์•„๋„ ๊ณ„์† ์‹คํ–‰
# > output.log: ๋กœ๊ทธ๋ฅผ ํŒŒ์ผ๋กœ ์ €์žฅ
# &: ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์‹คํ–‰

# โ˜… systemd๋กœ ์„œ๋ฒ„ ์ž๋™ ์‹œ์ž‘ (ํ”„๋กœ๋•์…˜์šฉ)
# /etc/systemd/system/game-server.service ์ƒ์„ฑ:
cat > /etc/systemd/system/game-server.service << 'EOF'
[Unit]
Description=Game Server
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/game-server
ExecStart=/usr/bin/node server.js
Restart=always

[Install]
WantedBy=multi-user.target
EOF

# ์„œ๋น„์Šค ๋“ฑ๋ก & ์‹œ์ž‘
systemctl daemon-reload
systemctl enable game-server
systemctl start game-server
systemctl status game-server # ์ƒํƒœ ํ™•์ธ

๐Ÿ’ก ์™œ systemd๋ฅผ ์จ์•ผ ํ•˜๋‚˜์š”?

nohup์€ ๊ฐ„๋‹จํ•˜์ง€๋งŒ ์„œ๋ฒ„๊ฐ€ ์ฃฝ์œผ๋ฉด ์ž๋™์œผ๋กœ ์žฌ์‹œ์ž‘ ์•ˆ ๋ฉ๋‹ˆ๋‹ค. systemd๋Š” ์„œ๋ฒ„๊ฐ€ ์ฃฝ์–ด๋„ ์ž๋™์œผ๋กœ ์‚ด๋ฆฌ๊ณ , ๋ถ€ํŒ… ์‹œ ์ž๋™ ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค. ์‹ค๋ฌด์—์„œ ํ•„์ˆ˜!

๐ŸŽฏ

7. ๊ฒŒ์ž„ ์„œ๋ฒ„ ํŠนํ™” ๊ฐœ๋…

โญโญโญ ์‹ฌํ™”

์ด์ œ ์ง„์งœ ๊ฒŒ์ž„ ์„œ๋ฒ„์˜ ํ•ต์‹ฌ ๊ฐœ๋…๋“ค์„ ๋ฐฐ์›Œ๋ด…์‹œ๋‹ค!

๐Ÿ”„ ๊ฒŒ์ž„ ๋ฃจํ”„ (Game Loop)๋ž€?

๊ฒŒ์ž„ ์„œ๋ฒ„๋Š” 1์ดˆ์— ์—ฌ๋Ÿฌ ๋ฒˆ(ํ‹ฑ) ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. ๋ณดํ†ต 20~60ํ‹ฑ/์ดˆ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ํ‹ฑ๋งˆ๋‹ค ๋ชจ๋“  ์œ ๋‹›์˜ ์œ„์น˜, HP, ์ƒํƒœ๋ฅผ ๊ณ„์‚ฐํ•ด์š”.

Node.js - ๊ฒŒ์ž„ ๋ฃจํ”„
// ๊ฐ„๋‹จํ•œ ๊ฒŒ์ž„ ๋ฃจํ”„ (20ํ‹ฑ/์ดˆ)
const TICK_RATE = 20; // ์ดˆ๋‹น 20ํšŒ
const TICK_INTERVAL = 1000 / TICK_RATE; // 50ms

const gameWorld = {
  units: new Map(),
  
  update() {
    // ๋ชจ๋“  ์œ ๋‹› ์—…๋ฐ์ดํŠธ
    for (const [id, unit] of this.units) {
      // ์ด๋™ ์ฒ˜๋ฆฌ
      unit.x += unit.vx;
      unit.y += unit.vy;
      
      // ์ถฉ๋Œ ๊ฒ€์‚ฌ
      this.checkCollisions(unit);
      
      // ๋ฒ„ํ”„/๋””๋ฒ„ํ”„ ํƒ€์ด๋จธ
      this.updateEffects(unit);
    }
    
    // ๊ฒŒ์ž„ ์Šน๋ฆฌ ์กฐ๊ฑด ์ฒดํฌ
    this.checkWinCondition();
  },
  
  broadcastState(clients) {
    const state = {
      type: 'gameState',
      tick: this.tickCount,
      units: Array.from(this.units.values())
    };
    const data = JSON.stringify(state);
    clients.forEach(c => c.send(data));
  }
};

// ๊ฒŒ์ž„ ๋ฃจํ”„ ์‹œ์ž‘
let tickCount = 0;
const gameLoop = setInterval(() => {
  gameWorld.update();
  gameWorld.broadcastState(wss.clients);
  tickCount++;
}, TICK_INTERVAL);

console.log(`๐ŸŽฎ ๊ฒŒ์ž„ ๋ฃจํ”„ ์‹œ์ž‘ (${TICK_RATE} tick/s)`);

๐Ÿ“ก ์ƒํƒœ ๋™๊ธฐํ™” (State Synchronization)

๋ชจ๋“  ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ๊ฐ™์€ ๊ฒŒ์ž„ ํ™”๋ฉด์„ ๋ณด๋ ค๋ฉด ์„œ๋ฒ„๊ฐ€ ๊ฒŒ์ž„ ์ƒํƒœ๋ฅผ ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „์†กํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋™๊ธฐํ™” ์ „๋žต
// ์ „๋žต 1: ์ „์ฒด ์ƒํƒœ ์ „์†ก (๋‹จ์ˆœํ•˜์ง€๋งŒ ๋ฐ์ดํ„ฐ้‡ ํผ)
// ๋งค ํ‹ฑ ๋ชจ๋“  ์œ ๋‹›์˜ ์ „์ฒด ์ƒํƒœ๋ฅผ ๋ณด๋ƒ„
// ์žฅ์ : ๊ตฌํ˜„ ์‰ฌ์›€ / ๋‹จ์ : ๋Œ€์—ญํญ ๋งŽ์ด ์‚ฌ์šฉ

// ์ „๋žต 2: ๋ธํƒ€ ์ „์†ก (๋ณ€๊ฒฝ๋ถ„๋งŒ ์ „์†ก) โญ ๊ถŒ์žฅ
function getDeltaState(prevState, currentState) {
  const delta = {};
  for (const [id, unit] of currentState) {
    const prev = prevState.get(id);
    if (!prev || JSON.stringify(prev) !== JSON.stringify(unit)) {
      delta[id] = unit; // ๋ณ€๊ฒฝ๋œ ๊ฒƒ๋งŒ ํฌํ•จ
    }
  }
  return delta;
}

// ์ „๋žต 3: ๋ณด๊ฐ„ (Interpolation)
// ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ถ€๋“œ๋Ÿฌ์šด ์›€์ง์ž„์„ ์œ„ํ•ด
// ์„œ๋ฒ„: 50ms๋งˆ๋‹ค ์œ„์น˜ ์ „์†ก
// ํด๋ผ์ด์–ธํŠธ: ์ค‘๊ฐ„ ์œ„์น˜๋ฅผ ๊ณ„์‚ฐํ•ด์„œ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ํ‘œ์‹œ
function interpolate(from, to, t) {
  return from + (to - from) * t; // t: 0~1
}

๐Ÿค ๋งค์น˜๋ฉ”์ดํ‚น์ด๋ž€?

PvP ๊ฒŒ์ž„์—์„œ ์‹ค๋ ฅ์ด ๋น„์Šทํ•œ ํ”Œ๋ ˆ์ด์–ด๋ผ๋ฆฌ ๋งค์นญํ•ด์ฃผ๋Š” ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค.

Node.js - ๊ฐ„๋‹จํ•œ ๋งค์น˜๋ฉ”์ดํ‚น
// ๋งค์น˜๋ฉ”์ดํ‚น ํ ์‹œ์Šคํ…œ
class MatchMaker {
  constructor() {
    this.queue = []; // ๋Œ€๊ธฐ์—ด
    this.ratingRange = 100; // ๋งค์นญ ํ—ˆ์šฉ ๋ฒ”์œ„
  }

  // ํ”Œ๋ ˆ์ด์–ด ํ์— ์ถ”๊ฐ€
  addPlayer(player) {
    this.queue.push({
      ...player,
      joinedAt: Date.now()
    });
    console.log(`[๋งค์น˜๋ฉ”์ดํ‚น] ${player.name} ๋Œ€๊ธฐ์—ด ์ง„์ž… (rating: ${player.rating})`);
    this.tryMatch();
  }

  // ๋งค์นญ ์‹œ๋„
  tryMatch() {
    // rating ์ˆœ์œผ๋กœ ์ •๋ ฌ
    this.queue.sort((a, b) => a.rating - b.rating);

    for (let i = 0; i < this.queue.length - 1; i++) {
      const p1 = this.queue[i];
      const p2 = this.queue[i + 1];

      // rating ์ฐจ์ด๊ฐ€ ๋ฒ”์œ„ ์•ˆ์ด๋ฉด ๋งค์นญ!
      if (Math.abs(p1.rating - p2.rating) <= this.ratingRange) {
        this.queue.splice(i, 2); // ํ์—์„œ ์ œ๊ฑฐ
        this.createMatch(p1, p2);
        return;
      }
    }
  }

  // ๋งค์น˜ ์ƒ์„ฑ
  createMatch(p1, p2) {
    console.log(`๐ŸŽฎ ๋งค์น˜ ์„ฑ์‚ฌ! ${p1.name} vs ${p2.name}`);
    // ์—ฌ๊ธฐ์„œ ๊ฒŒ์ž„ ๋ฃธ์„ ๋งŒ๋“ค๊ณ  ๋‘ ํ”Œ๋ ˆ์ด์–ด๋ฅผ ์ดˆ๋Œ€
  }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
const matchmaker = new MatchMaker();
matchmaker.addPlayer({ name: '์ „์‚ฌ', rating: 1200 });
matchmaker.addPlayer({ name: '๋งˆ๋ฒ•์‚ฌ', rating: 1180 });
// ์ถœ๋ ฅ: ๐ŸŽฎ ๋งค์น˜ ์„ฑ์‚ฌ! ๋งˆ๋ฒ•์‚ฌ vs ์ „์‚ฌ

โ“ ํ•™์Šต ํ€ด์ฆˆ

๋ฐฐ์šด ๋‚ด์šฉ์„ ํ™•์ธํ•ด๋ณด์„ธ์š”!

Q1. RTS ๊ฒŒ์ž„์—์„œ ์‹ค์‹œ๊ฐ„ ์ „ํˆฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•  ๋•Œ ๊ฐ€์žฅ ์ ํ•ฉํ•œ ํ”„๋กœํ† ์ฝœ์€?
TCP
UDP
FTP
SMTP
Q2. ์—ฌ๋Ÿฌ ํ”Œ๋ ˆ์ด์–ด์˜ ์š”์ฒญ์„ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” C# ํ‚ค์›Œ๋“œ๋Š”?
static
async/await
sealed
volatile
Q3. ๋ธŒ๋ผ์šฐ์ €์™€ ์„œ๋ฒ„ ๊ฐ„ ์‹ค์‹œ๊ฐ„ ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์„ ์œ„ํ•œ ๊ธฐ์ˆ ์€?
AJAX
WebSocket
HTTP Polling
FTP
Q4. ํ”Œ๋ ˆ์ด์–ด์˜ ์ „์ , ์•„์ดํ…œ ๋“ฑ ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ธฐ ์ ํ•ฉํ•œ DB๋Š”?
MySQL
Excel
๋ฉ”๋ชจ์žฅ
Redis (์บ์‹œ ์ „์šฉ)
Q5. Linux์—์„œ ๊ฒŒ์ž„ ์„œ๋ฒ„๋ฅผ ์ž๋™ ์‹œ์ž‘/์žฌ์‹œ์ž‘ํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” ๋„๊ตฌ๋Š”?
nohup
systemd
cron
grep
ํ•™์Šต ์ง„๋„: 0/7 (0%)