From b7ca45591ed8c8334cda7ada4e144b41ddf25459 Mon Sep 17 00:00:00 2001 From: Doeun <112849712+nemobim@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:33:53 +0900 Subject: [PATCH 1/6] =?UTF-8?q?Rename=2004.=ED=8C=A9=ED=86=A0=EB=A6=AC=5F?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20to=2004.=ED=8C=A9=ED=86=A0=EB=A6=AC=5F?= =?UTF-8?q?=ED=8C=A8=ED=84=B4.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...5\214\251\355\206\240\353\246\254_\355\214\250\355\204\264.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "doeun/04.\355\214\251\355\206\240\353\246\254_\355\214\250\355\204\264" => "doeun/04.\355\214\251\355\206\240\353\246\254_\355\214\250\355\204\264.md" (100%) diff --git "a/doeun/04.\355\214\251\355\206\240\353\246\254_\355\214\250\355\204\264" "b/doeun/04.\355\214\251\355\206\240\353\246\254_\355\214\250\355\204\264.md" similarity index 100% rename from "doeun/04.\355\214\251\355\206\240\353\246\254_\355\214\250\355\204\264" rename to "doeun/04.\355\214\251\355\206\240\353\246\254_\355\214\250\355\204\264.md" From 94001798b6a3cbc63c404faf83c051eefa4eadad Mon Sep 17 00:00:00 2001 From: Doeun <112849712+nemobim@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:34:05 +0900 Subject: [PATCH 2/6] =?UTF-8?q?Rename=2005.=EC=8B=B1=EA=B8=80=ED=86=A4=5F?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20to=2005.=EC=8B=B1=EA=B8=80=ED=86=A4=5F?= =?UTF-8?q?=ED=8C=A8=ED=84=B4.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...4\213\261\352\270\200\355\206\244_\355\214\250\355\204\264.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "doeun/05.\354\213\261\352\270\200\355\206\244_\355\214\250\355\204\264" => "doeun/05.\354\213\261\352\270\200\355\206\244_\355\214\250\355\204\264.md" (100%) diff --git "a/doeun/05.\354\213\261\352\270\200\355\206\244_\355\214\250\355\204\264" "b/doeun/05.\354\213\261\352\270\200\355\206\244_\355\214\250\355\204\264.md" similarity index 100% rename from "doeun/05.\354\213\261\352\270\200\355\206\244_\355\214\250\355\204\264" rename to "doeun/05.\354\213\261\352\270\200\355\206\244_\355\214\250\355\204\264.md" From 2230445fd12d199b4aabcc83f116bf34df7ac187 Mon Sep 17 00:00:00 2001 From: Doeun <112849712+nemobim@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:47:03 +0900 Subject: [PATCH 3/6] =?UTF-8?q?Create=2006.=EC=BB=A4=EB=A7=A8=EB=93=9C=5F?= =?UTF-8?q?=ED=8C=A8=ED=84=B4.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...0\353\223\234_\355\214\250\355\204\264.md" | 689 ++++++++++++++++++ 1 file changed, 689 insertions(+) create mode 100644 "doeun/06.\354\273\244\353\247\250\353\223\234_\355\214\250\355\204\264.md" diff --git "a/doeun/06.\354\273\244\353\247\250\353\223\234_\355\214\250\355\204\264.md" "b/doeun/06.\354\273\244\353\247\250\353\223\234_\355\214\250\355\204\264.md" new file mode 100644 index 0000000..5554b2e --- /dev/null +++ "b/doeun/06.\354\273\244\353\247\250\353\223\234_\355\214\250\355\204\264.md" @@ -0,0 +1,689 @@ +# 커맨드 패턴 + +## 상황) 만능 리모컨 만들기 + +리모컨에 메서드를 연결해서 만능 리모컨을 만들어보자. +다만 리모컨의 성능이 좋지 않아 **on, off 버튼**만 있다. + +--- + +## 설계1) 기기를 직접 연결하기 + +리모컨 버튼에 `light.on()` 같은 메서드를 바로 연결한다. + +```ts +class Remote { + private light: Light; + + onPressed() { + this.light.on(); // 조명만 켤 수 있음 + } +} +``` + +- 에어컨으로 바꾸고 싶으면? → 리모컨 코드를 직접 수정해야 한다 +- 협력업체가 `turnOn()`으로 이름을 바꾸면? → 리모컨도 전부 수정해야 한다 +- 기기가 100개라면? → `if`문도 100개... + +**문제:** 리모컨이 기기의 내부 구현을 너무 많이 알고 있다. 기기가 바뀔 때마다 리모컨도 함께 흔들린다. + +--- + +## 설계2) 커맨드 객체를 중간에 놓기 + +리모컨과 기기 사이에 **커맨드(Command) 객체**라는 중간 다리를 놓는다. + +``` +리모컨 → command.execute() → [커맨드 객체] → light.on() +``` + +각자의 역할을 정리하면 이렇다. + +- **리모컨**: "난 뭔지 모르지만, 버튼에 연결된 객체의 `execute()`만 호출할게." +- **커맨드 객체**: "난 조명을 품고 있고, `execute()`가 불리면 `light.on()`을 실행시켜 줄게." +- **조명**: 그냥 자기 일(`on()`, `off()`)만 한다. + +```ts +interface Command { + execute(): void; +} + +class LightOnCommand implements Command { + constructor(private light: Light) {} + + execute() { + this.light.on(); + } +} + +class Remote { + private command: Command; + + setCommand(command: Command) { + this.command = command; + } + + onPressed() { + this.command.execute(); // 뭔지 몰라도 그냥 실행 + } +} +``` + +이제 리모컨은 `execute()` 하나만 안다. 조명이든 에어컨이든, 커맨드 객체만 갈아끼우면 된다. + +> "요청하는 쪽"과 "실행하는 쪽"을 분리한다. +> 리모컨이 **무엇을** 실행할지 몰라도, 커맨드 객체가 **어떻게** 실행할지 안다. + +--- + +## 커맨드 패턴이란? + +> 커맨드 패턴을 사용하면 요청 내역을 객체로 캡슐화해서, 객체를 서로 다른 요청 내역에 따라 매개변수화할 수 있다. 이러면 요청을 큐에 저장하거나, 로그로 기록하거나, 작업 취소 기능을 사용할 수 있다. + +이 정의에서 핵심만 뽑으면 **3가지 등장인물**이 있다. + +| 요소 | 역할 | 리모컨 비유 | +|------|------|------------| +| **수신자 (Receiver)** | 실제 일을 하는 객체 | Light, AirConditioner | +| **커맨드 (Command)** | 모든 명령 객체가 따라야 할 인터페이스 | LightOnCommand | +| **호출자 (Invoker)** | 명령을 보관했다가 버튼이 눌리면 실행하는 객체 | RemoteControl | + +``` +┌─────────────┐ ┌──────────────┐ ┌─────────────┐ +│ Invoker │──────▶│ Command │──────▶│ Receiver │ +│ (리모컨) │ has │ (명령 객체) │ has │ (조명 등) │ +│ │ │ │ │ │ +│ slot. │ │ execute() │ │ on() │ +│ execute() │ │ undo() │ │ off() │ +└─────────────┘ └──────────────┘ └─────────────┘ + 뭔지 몰라도 어떻게 할지를 실제로 일하는 + 그냥 실행 캡슐화 놈 +``` + +이제 이 구조를 직접 코드로 만들어보자. + +--- + +## 직접 구현해보기 + +### 1. '명령'의 규격 만들기 + +리모컨이 어떤 기기든 상관없이 `execute()`만 부를 수 있으려면, 모든 명령 객체가 똑같은 모양(인터페이스)을 가져야 한다. + +```ts +interface Command { + execute(): void; +} +``` + +이 `Command` 인터페이스를 사용해서 **'조명을 켜는 명령 객체'**를 만들어보자. + +```ts +class LightOnCommand implements Command { + constructor(private light: Light) {} + + execute() { + this.light.on(); + } +} +``` + +### 2. 리모컨(Invoker) 구현하기 + +리모컨은 버튼이 눌렸을 때 어떤 명령을 실행할지 알고 있어야 한다. +하지만 앞서 말했듯이, 리모컨 코드 안에 `light.on()`을 직접 써넣으면 안 된다! + +대신 리모컨은 Command 인터페이스를 저장할 **슬롯(Slot)**을 가지고 있으면 된다. + +```ts +class RemoteControl { + private slot: Command = new NoCommand(); // 명령을 담을 공간 + + setCommand(command: Command) { + this.slot = command; + } + + buttonWasPressed() { + this.slot.execute(); + } +} +``` + +> ❓ `NoCommand`가 뭔지 궁금하다면 [아래 참고 섹션](#참고-nocommand가-왜-필요한가요)을 확인하자. 간단히 말하면 `null` 대신 넣어두는, "아무것도 안 하는" 안전한 기본 객체다. + +### 3. 조합하기 + +```ts +// 협력업체에서 제공한 클래스 +class Light { + on() { console.log("조명이 켜졌습니다. 💡"); } + off() { console.log("조명이 꺼졌습니다. 🌑"); } +} + +// 1. 리모컨(Invoker) 생성 +const remote = new RemoteControl(); + +// 2. 조명(Receiver) 생성 +const light = new Light(); + +// 3. 조명을 켜는 명령(Command) 생성 및 조명 연결 +const lightOn = new LightOnCommand(light); + +remote.setCommand(lightOn); +remote.buttonWasPressed(); // "조명이 켜졌습니다. 💡" +``` + +전등을 끄는 버튼이나 에어컨을 켜는 버튼을 추가하고 싶다면? `RemoteControl` 클래스의 코드를 **한 줄도 수정할 필요가 없다**. 새로운 커맨드 객체를 만들어서 끼워넣기만 하면 된다. + +--- + +## 커맨드 패턴으로 할 수 있는 것들 + +기본 구조를 이해했으니, 이제 커맨드 패턴이 진짜 힘을 발휘하는 장면들을 하나씩 살펴보자. + +### 1. 매개변수화 (Parameterization) + +"객체를 서로 다른 요청 내역에 따라 매개변수화할 수 있다"는 말은, **실행 시점에 어떤 명령을 수행할지 갈아 끼울 수 있다**는 뜻이다. + +```ts +// ❌ 일반적인 방식: 코드 수준에서 결정됨 +if (type === "LIGHT") light.on(); + +// ✅ 커맨드 패턴: 실행 중에 객체(매개변수)를 전달해서 동작을 결정 +remote.setCommand(new LightOnCommand(light)); +``` + +이렇게 하면 `RemoteControl`은 어떤 구체적인 동작이 들어올지 몰라도, 전달받은 객체(매개변수)에 따라 다르게 행동하는 **범용적인 도구**가 된다. + +### 2. 작업 취소 (Undo) — 기본편 + +명령 객체는 실행(`execute`)하는 방법뿐만 아니라, **자신이 실행되기 전 상태로 되돌리는 방법(`undo`)**도 스스로 알고 있다. + +```ts +interface Command { + execute(): void; + undo(): void; +} + +class LightOnCommand implements Command { + constructor(private light: Light) {} + + execute() { + this.light.on(); + } + + undo() { + // 켰던 걸 되돌리려면? 꺼야죠! + this.light.off(); + } +} +``` + +리모컨은 마지막으로 실행한 명령을 기억해 두었다가, `undo` 버튼이 눌리면 그 명령의 `undo()`를 호출하면 된다. + +```ts +class RemoteControl { + private slot: Command = new NoCommand(); + private undoCommand: Command = new NoCommand(); // 마지막 명령을 기억할 변수 + + setCommand(command: Command) { + this.slot = command; + } + + buttonWasPressed() { + this.slot.execute(); + this.undoCommand = this.slot; // 실행 직후, "이게 마지막이었어"라고 저장 + } + + undoButtonWasPressed() { + this.undoCommand.undo(); // 기억해둔 명령의 undo()를 호출 + } +} +``` + +조명처럼 켜기/끄기만 있는 기기는 `undo`가 단순하다. 하지만 **상태가 여러 단계인 기기**라면 어떨까? + +### 3. 작업 취소 (Undo) — 선풍기 속도 조절 편 + +선풍기는 보통 꺼짐 → 약풍 → 중풍 → 강풍, 이렇게 여러 단계가 있다. +"강풍으로 올려!" 명령을 실행한 뒤 `undo`하면, 단순히 "꺼!" 가 아니라 **이전 단계로 되돌아가야** 한다. + +```ts +class CeilingFan { + static readonly OFF = 0; + static readonly LOW = 1; + static readonly MEDIUM = 2; + static readonly HIGH = 3; + + private speed: number = CeilingFan.OFF; + private name: string; + + constructor(name: string) { + this.name = name; + } + + high() { this.speed = CeilingFan.HIGH; console.log(`${this.name} 선풍기: 강풍 💨💨💨`); } + medium() { this.speed = CeilingFan.MEDIUM; console.log(`${this.name} 선풍기: 중풍 💨💨`); } + low() { this.speed = CeilingFan.LOW; console.log(`${this.name} 선풍기: 약풍 💨`); } + off() { this.speed = CeilingFan.OFF; console.log(`${this.name} 선풍기: 꺼짐`); } + + getSpeed() { return this.speed; } +} +``` + +핵심은 `execute()` 전에 **현재 속도를 저장**해두는 것이다. 그래야 `undo()` 할 때 "직전 상태"로 정확히 되돌릴 수 있다. + +```ts +class CeilingFanHighCommand implements Command { + private prevSpeed: number; // 👈 이전 속도를 기억할 변수 + + constructor(private fan: CeilingFan) {} + + execute() { + this.prevSpeed = this.fan.getSpeed(); // 💡 실행 전 현재 속도를 저장 + this.fan.high(); + } + + undo() { + // 저장해둔 이전 속도로 되돌린다 + switch (this.prevSpeed) { + case CeilingFan.HIGH: this.fan.high(); break; + case CeilingFan.MEDIUM: this.fan.medium(); break; + case CeilingFan.LOW: this.fan.low(); break; + case CeilingFan.OFF: this.fan.off(); break; + } + } +} +``` + +```ts +const fan = new CeilingFan("거실"); +const fanHigh = new CeilingFanHighCommand(fan); + +const remote = new RemoteControl(); +remote.setCommand(fanHigh); + +// 현재: 꺼짐(OFF) +remote.buttonWasPressed(); // "거실 선풍기: 강풍 💨💨💨" (OFF → HIGH) +remote.undoButtonWasPressed(); // "거실 선풍기: 꺼짐" (HIGH → OFF로 복원!) +``` + +조명은 on/off 두 가지뿐이라 `undo`가 항상 반대 동작이었다. 하지만 선풍기처럼 상태가 여러 단계인 경우, **"실행 전 상태를 스냅샷으로 저장"**해두어야 정확한 `undo`가 가능하다. 이게 커맨드 패턴에서 `undo`를 구현할 때 가장 중요한 포인트다. + +### 4. 여러 동작을 한 번에 — 매크로 커맨드 (MacroCommand) + +지금까지는 버튼 하나에 명령 하나를 연결했다. 그런데 현실에서는 **버튼 하나로 여러 기기를 동시에 제어**하고 싶은 경우가 많다. + +예를 들어 "영화 모드" 버튼을 누르면 이런 일이 한꺼번에 일어나면 좋겠다. + +- 조명을 끈다 +- 선풍기를 약풍으로 켠다 +- TV를 켠다 + +각각을 따로따로 누르는 건 너무 번거롭다. 이럴 때 **여러 커맨드를 하나로 묶는 매크로 커맨드**를 만들 수 있다. + +```ts +class MacroCommand implements Command { + constructor(private commands: Command[]) {} + + execute() { + // 가지고 있는 명령들을 순서대로 전부 실행 + for (const command of this.commands) { + command.execute(); + } + } + + undo() { + // 실행의 역순으로 되돌린다 + for (const command of [...this.commands].reverse()) { + command.undo(); + } + } +} +``` + +`MacroCommand` 자체도 `Command` 인터페이스를 구현한다는 점이 중요하다. +리모컨 입장에서는 이게 매크로인지 단일 명령인지 **구분할 필요가 없다**. 그냥 `execute()`만 부르면 된다. + +```ts +// 기기(Receiver) 준비 +class TV { + on() { console.log("TV가 켜졌습니다. 📺"); } + off() { console.log("TV가 꺼졌습니다."); } +} + +const light = new Light(); +const fan = new CeilingFan("거실"); +const tv = new TV(); + +// 개별 커맨드 생성 +const lightOff = new LightOffCommand(light); +const fanLow = new CeilingFanLowCommand(fan); +const tvOn = new TVOnCommand(tv); + +// 매크로로 묶기 +const movieMode = new MacroCommand([lightOff, fanLow, tvOn]); + +// 리모컨에 연결 +const remote = new RemoteControl(); +remote.setCommand(movieMode); + +// 버튼 한 번이면 끝! +remote.buttonWasPressed(); +// "조명이 꺼졌습니다. 🌑" +// "거실 선풍기: 약풍 💨" +// "TV가 켜졌습니다. 📺" + +// undo도 한 번에 전부 되돌린다 (역순으로) +remote.undoButtonWasPressed(); +// "TV가 꺼졌습니다." +// "거실 선풍기: 꺼짐" (이전 상태로 복원) +// "조명이 켜졌습니다. 💡" +``` + +`undo()`에서 **역순으로 되돌리는 이유**가 있다. 조명을 끄고 → TV를 켠 순서라면, 되돌릴 때는 TV를 먼저 끄고 → 조명을 켜야 원래 상태로 정확히 복원된다. 마치 접시를 쌓을 때 마지막에 올린 접시를 먼저 빼는 것과 같다. (LIFO — Last In, First Out) + +### 5. 요청을 큐에 쌓거나 로그로 기록하기 + +명령이 객체 형태(`Command` 인스턴스)로 존재하기 때문에, 이를 리스트나 큐에 차곡차곡 쌓을 수 있다. + +- **작업 큐**: 네트워크 상태가 안 좋을 때 명령들을 큐에 쌓아두었다가, 나중에 한꺼번에 `execute()`를 차례대로 호출할 수 있다. (예: 포토샵의 작업 내역, 데이터베이스 트랜잭션) +- **로그 기록**: 명령 객체에 `timestamp`나 `user` 정보를 담아 로그로 남기면, 시스템이 다운되었을 때 로그에 쌓인 커맨드들을 다시 실행(Replay)해서 상태를 복구할 수 있다. + +이 "로그 + Replay" 개념을 좀 더 구체적으로 살펴보자. + +--- + +## 더 활용하기: 커맨드 패턴으로 복구 시스템 만들기 + +여기까지 읽었다면 이런 생각이 들 수 있다. "큐랑 로그 기록은 알겠는데, 실제로 어떻게 쓴다는 거지?" + +쉬운 예시로 생각해보자. 스프레드시트 앱을 만들고 있다고 치자. 사용자가 열심히 셀 값을 수정하다가 갑자기 브라우저가 꺼져버렸다. 이 앱이 저장 버튼 없이도 **자동 복구**를 지원한다면 얼마나 좋을까? + +커맨드 패턴의 "로그 기록 + Replay" 조합이면 가능하다. + +### 핵심 아이디어 + +1. 사용자의 모든 동작을 **커맨드 객체로** 만든다. +2. 커맨드가 실행될 때마다 **로그(히스토리)에 차곡차곡 쌓는다.** +3. 주기적으로 **체크포인트(스냅샷)**를 저장한다. +4. 크래시가 나면? 마지막 체크포인트부터 로그에 쌓인 커맨드를 **순서대로 다시 실행(Replay)**한다. + +```ts +// 스프레드시트의 셀 값을 변경하는 커맨드 +class ChangeCellCommand implements Command { + private prevValue: string; + + constructor( + private sheet: Spreadsheet, + private cell: string, // "A1", "B3" 같은 셀 주소 + private newValue: string + ) {} + + execute() { + this.prevValue = this.sheet.getCell(this.cell); // 이전 값 저장 + this.sheet.setCell(this.cell, this.newValue); + } + + undo() { + this.sheet.setCell(this.cell, this.prevValue); // 이전 값으로 복원 + } + + // 💡 로그 저장용: 커맨드를 직렬화(serialize)할 수 있다 + serialize(): string { + return JSON.stringify({ + type: "CHANGE_CELL", + cell: this.cell, + newValue: this.newValue, + timestamp: Date.now(), + }); + } +} +``` + +```ts +class CommandLogger { + private history: string[] = []; + private checkpoint: string = ""; // 스냅샷 + + // 커맨드 실행 + 로그 기록을 한 번에 + executeAndLog(command: Command & { serialize(): string }) { + command.execute(); + this.history.push(command.serialize()); + } + + // 주기적으로 현재 상태를 통째로 저장 + saveCheckpoint(sheet: Spreadsheet) { + this.checkpoint = JSON.stringify(sheet.getAllData()); + this.history = []; // 체크포인트 이후의 로그만 남긴다 + } + + // 복구: 체크포인트 복원 → 이후 커맨드 Replay + recover(sheet: Spreadsheet) { + // 1단계: 마지막 체크포인트로 되돌린다 + sheet.restoreFrom(JSON.parse(this.checkpoint)); + + // 2단계: 체크포인트 이후의 커맨드들을 순서대로 다시 실행 + for (const log of this.history) { + const data = JSON.parse(log); + const command = new ChangeCellCommand(sheet, data.cell, data.newValue); + command.execute(); + } + } +} +``` + +```ts +const sheet = new Spreadsheet(); +const logger = new CommandLogger(); + +// 사용자가 셀을 수정할 때마다 커맨드로 실행 + 로그 기록 +logger.executeAndLog(new ChangeCellCommand(sheet, "A1", "이름")); +logger.executeAndLog(new ChangeCellCommand(sheet, "A2", "김도은")); +logger.executeAndLog(new ChangeCellCommand(sheet, "B1", "점수")); + +// 5분마다 체크포인트 저장 +logger.saveCheckpoint(sheet); + +// 이후 추가 작업... +logger.executeAndLog(new ChangeCellCommand(sheet, "B2", "100")); +logger.executeAndLog(new ChangeCellCommand(sheet, "B3", "95")); + +// 💥 브라우저 크래시! + +// 다시 접속하면 → 자동 복구 +logger.recover(sheet); +// 체크포인트(A1, A2, B1) 복원 → 이후 커맨드(B2, B3) Replay +// 사용자는 아무것도 잃어버리지 않는다! +``` + +이게 가능한 이유는 **명령이 "객체"이기 때문**이다. 단순히 `sheet.setCell("A1", "이름")`을 호출하는 것이었다면 이런 로그나 Replay 자체가 불가능하다. 하지만 커맨드 객체는 "무엇을, 어디에, 어떤 값으로" 바꿨는지를 통째로 담고 있기 때문에 저장하고, 전송하고, 나중에 다시 실행할 수 있는 것이다. + +> 이 패턴은 실제로 Google Docs의 실시간 동시 편집, 게임의 리플레이 시스템, 데이터베이스의 WAL(Write-Ahead Log) 등에서 핵심 원리로 사용된다. + +--- + +## 실무에서 만나는 커맨드 패턴 + +리모컨 예제만 보면 "실무에서 이걸 어디 쓰지?" 싶을 수 있다. 하지만 우리가 매일 쓰는 도구들 속에 커맨드 패턴이 숨어 있다. + +### 어디서 쓰이고 있을까? + +**Ctrl+Z (Undo/Redo)** + +포토샵, 피그마, 노션, VS Code — 모두 커맨드 패턴 기반이다. "텍스트 입력", "도형 이동", "색상 변경" 같은 동작 하나하나가 커맨드 객체로 만들어져서 히스토리 스택에 쌓인다. `execute()` → 스택에 push, `undo()` → 스택에서 pop + 되돌리기, `redo()` → redo 스택에서 꺼내서 다시 실행. + +**트랜잭션 시스템** + +"주문하기" 버튼을 누르면 재고 차감 → 결제 → 배송 등록이 순서대로 일어난다. 중간에 결제가 실패하면? 이미 차감한 재고를 되돌려야 한다. 각 단계를 커맨드로 만들어두면 실패 지점부터 역순으로 `undo()`를 호출해서 롤백할 수 있다. (이 방식을 **보상 트랜잭션(Saga 패턴)**이라고도 부른다.) + +**작업 스케줄러 / 큐** + +"지금 당장이 아니라 나중에 실행해야 할 작업"이 있을 때, 커맨드 객체를 큐에 넣어두고 워커가 하나씩 꺼내서 `execute()`를 호출한다. 이메일 발송, 이미지 리사이즈, 알림 전송 같은 비동기 작업들이 이런 구조를 따른다. + +### 프론트엔드에서의 커맨드 패턴 + +프론트엔드 개발에서도 커맨드 패턴의 원리는 곳곳에서 작동한다. 이름만 다를 뿐, 구조는 놀라울 정도로 비슷하다. + +**Redux / Zustand 액션** + +```ts +// Redux의 action = 커맨드 객체 +dispatch({ type: "ADD_TODO", payload: { text: "블로그 쓰기" } }); +``` + +| 커맨드 패턴 | Redux | +|-----------|-------| +| 커맨드 객체 (무엇을 할지 담은 객체) | 액션 객체 | +| Invoker (실행을 요청하는 쪽) | dispatch | +| Receiver (실제로 상태를 바꾸는 쪽) | 리듀서 | + +Redux DevTools에서 "시간 여행 디버깅(Time Travel Debugging)"이 가능한 이유가 바로 이것이다. 액션이 전부 객체로 로그에 쌓여 있으니까, 특정 시점으로 되돌리거나 다시 실행하는 게 가능하다. 앞서 살펴본 "복구 시스템"과 같은 원리다. + +**TanStack Query의 Optimistic Update** + +```ts +useMutation({ + mutationFn: updateTodo, + onMutate: async (newTodo) => { + // execute: 서버 응답 전에 낙관적으로 UI를 먼저 반영 + const previousTodos = queryClient.getQueryData(["todos"]); + queryClient.setQueryData(["todos"], (old) => [...old, newTodo]); + return { previousTodos }; // 💡 이전 상태를 저장해둔다 + }, + onError: (err, newTodo, context) => { + // undo: 서버 요청이 실패하면 저장해둔 이전 상태로 롤백 + queryClient.setQueryData(["todos"], context.previousTodos); + }, +}); +``` + +여기서 `onMutate`가 `execute()` + 이전 상태 저장, `onError`가 `undo()`에 해당한다. 선풍기 예시에서 `prevSpeed`를 저장해두고 `undo()` 때 복원하는 것과 정확히 같은 구조다. + +**React Router의 네비게이션 차단** + +```ts +// "저장하지 않은 변경사항이 있습니다. 정말 나가시겠습니까?" +const blocker = useBlocker(() => hasUnsavedChanges); +``` + +페이지 이동이라는 "명령"이 들어왔을 때, 조건에 따라 실행을 보류하거나 차단하는 것도 커맨드 패턴의 매개변수화 개념과 맞닿아 있다. 명령 자체를 객체처럼 가로채서, 실행 여부를 나중에 결정할 수 있기 때문이다. + +### 혹시 나도 커맨드 패턴을 쓴 적이 있을까? + +커맨드 패턴이라고 인식하지 못했을 뿐, 다음과 같은 코드를 작성해본 적이 있다면 이미 커맨드 패턴의 원리를 활용한 것이다. + +```ts +// 1. 폼에서 submit 핸들러를 분리해서 넘기기 +const handleSubmit = () => { api.createPatient(formData); }; +