본문 바로가기

객체 지향과 함수형 프로그래밍

상태와 동작을 객체로 묶는 방식과 순수 함수·불변성을 중심으로 한 방식을 비교해 문제에 맞게 조합합니다.

지금까지 자바스크립트의 문법과 실행 원리, 그리고 앞 글 프로토타입과 상속에서 객체가 기능을 물려받는 방식까지 살펴봤습니다. 이번에는 코드를 어떤 방식으로 나누고 설계할지로 시야를 넓힙니다. 프로그램을 작성할 때 따르는 사고방식이나 원칙을 프로그래밍 패러다임(programming paradigm)이라고 하며, 어떤 패러다임을 고르느냐에 따라 코드를 읽고 고치고 확장하기 쉬운 정도가 달라집니다.

자바스크립트는 한 가지 패러다임에 묶이지 않고 여러 패러다임의 특징을 함께 쓸 수 있는 언어입니다. 특히 객체 지향 프로그래밍(object-oriented programming, OOP)과 함수형 프로그래밍(functional programming, FP)의 특징이 널리 쓰입니다. 이 글에서는 두 패러다임의 기본 개념을 정리하고, 같은 잔액 변경을 두 방식으로 구현해 비교합니다. 앞 글의 class와 클로저, 5장의 객체 참조와 얕은 복사를 알고 있으면 예제를 따라갈 수 있습니다.

상태 소유권과 변환 방식 선택

FLOWCHART · WEB PLATFORM CONTRACT

상태 소유권과 변환 방식 선택

상태의 수명과 규칙을 한 객체가 관리할지, 입력값을 순수 함수로 변환할지 판단한다. 두 접근은 조합할 수 있으며, 경계에서 변경·복사 책임을 정한다.

입금 상태의 소유와 이전 값 보존같은10000원에5000원을 입금하는 작업에서 객체의메서드, 순수변환, 두방식조합으로 세 비교 경로가 각각 갈라진다. 메서드는 같은계좌의 현재잔액을 갱신하고 순수변환은 이전값과 새값을 보관할 수 있다. 조합은 검증과 계산을 함수에 두고 계좌가 변경과이력을 관리하는 예시다. 모바일도 세 경로는 원입력에서 독립적으로 시작한다.같은 입금: 10000 + 5000무엇을 보존할 것인가?객체의 메서드같은 계좌의 잔액 갱신변경 규칙을 메서드에 둠현재 잔액 15000순수 변환입력 10000은 유지결과 15000을 반환이전·다음 값 보관두 방식 조합입금액 검증·계산은 함수계좌가 변경·이력 관리책임을 경계에서 구분세 가지 선택은 직렬 단계가 아니라 비교 대상
입금 상태의 소유와 이전 값 보존같은10000원에5000원을 입금하는 작업에서 객체의메서드, 순수변환, 두방식조합으로 세 비교 경로가 각각 갈라진다. 메서드는 같은계좌의 현재잔액을 갱신하고 순수변환은 이전값과 새값을 보관할 수 있다. 조합은 검증과 계산을 함수에 두고 계좌가 변경과이력을 관리하는 예시다. 모바일도 세 경로는 원입력에서 독립적으로 시작한다.입금액 5000이전 잔액 10000객체의 메서드같은 계좌의 잔액 갱신변경 규칙을 메서드에 둠현재 잔액 15000순수 변환입력 10000은 유지결과 15000을 반환이전·다음 값 보관두 방식 조합입금액 검증·계산은 함수계좌가 변경·이력 관리책임을 경계에서 구분

Object method

한 owner가 stable identity와 invariant를 캡슐화하고 공유 alias를 제한할 때

Pure function

이전 값과 입력에서 새 값을 만들며 snapshot 비교·history·undo·deterministic test가 중요할 때

Hybrid boundary

성능상 내부 hot loop를 mutable하게 유지하면서 외부에는 immutable snapshot을 제공할 때. 복사·동기화·identity 교체의 책임을 경계에 둔다.

순수한 변환과 부작용의 책임을 구분한다

순수 함수는 같은 입력에 같은 결과를 주고 외부 상태를 바꾸지 않는다. 모든 호출이 반드시 새 객체를 만들어야 하는 것은 아니다. 객체 내부의 상태 변경과 순수한 데이터 변환을 조합하되 공유 참조와 snapshot의 변경 책임을 명확히 한다.

위 그림은 잔액 10000원에 5000원을 입금하는 같은 작업을 세 가지 방식으로 비교합니다. 세 경로는 차례로 거치는 단계가 아니라 같은 입력에서 따로 출발하는 선택지입니다. 객체의 메서드 방식은 같은 계좌 객체의 잔액을 15000으로 바꾸고, 변경 규칙을 메서드 안에 둡니다. 순수 변환 방식은 입력인 10000을 그대로 두고 15000이라는 새 값을 돌려주므로, 이전 값과 다음 값을 모두 보관할 수 있습니다. 두 방식을 조합하면 입금액 검증과 계산은 함수가 맡고, 계좌 객체는 잔액을 바꾸고 이력을 남기는 일을 맡습니다.

그림의 아래쪽은 언제 어느 쪽을 고를지 정리합니다. 객체의 메서드는 한 객체가 오래 유지되는 상태와 지켜야 할 규칙을 감싸고, 여러 곳에서 같은 상태를 마음대로 바꾸지 못하게 할 때 알맞습니다. 순수 함수는 이전 값과 새 값을 비교하거나, 이력과 되돌리기가 필요하거나, 같은 입력에 같은 결과를 확인하는 테스트가 중요할 때 알맞습니다. 조합 방식은 성능 때문에 내부에서는 값을 직접 바꾸면서 바깥에는 바뀌지 않는 복사본을 내보낼 때 쓰며, 이때는 복사와 갱신을 어느 쪽이 맡는지 분명히 정해 둬야 합니다.


객체 지향 프로그래밍

객체 지향 프로그래밍은 프로그램을 객체 단위로 나눠 설계하는 패러다임입니다. 데이터(속성)와 그 데이터를 다루는 기능(메서드)을 하나의 객체로 묶고, 객체들이 서로 메서드를 호출하며 프로그램이 동작합니다. OOP의 핵심 원칙은 캡슐화, 상속, 추상화, 다형성 네 가지입니다.

캡슐화

캡슐화(encapsulation)는 데이터와 그 데이터를 다루는 메서드를 하나로 묶고, 객체의 내부를 바깥에서 직접 건드리지 못하게 하는 것입니다. 바깥에서는 미리 정한 메서드로만 데이터를 다루므로, 내부 구현을 바꿔도 메서드를 쓰는 코드는 영향을 덜 받습니다. 자바스크립트에서는 앞에서 본 클로저나 class의 # 비공개 필드(private field)로 바깥의 직접 접근을 막을 수 있습니다. class 문법은 ES2015에, 비공개 필드는 ES2022에 추가되었습니다.

// class를 이용한 캡슐화 예시
class Account {
    #balance; // 프라이빗 필드 (ES2022)

    constructor(initialBalance) {
        if (!Number.isFinite(initialBalance) || initialBalance < 0) {
            throw new Error("초기 잔액은 0 이상의 숫자여야 합니다.");
        }
        this.#balance = initialBalance;
    }

    deposit(amount) {
        if (!Number.isFinite(amount) || amount <= 0) {
            console.log("유효하지 않은 입금액입니다.");
            return;
        }
        this.#balance += amount;
        console.log(`입금: ${amount}, 현재 잔액: ${this.#balance}`);
    }

    withdraw(amount) {
        if (Number.isFinite(amount) && amount > 0 && this.#balance >= amount) {
            this.#balance -= amount;
            console.log(`출금: ${amount}, 현재 잔액: ${this.#balance}`);
        } else {
            console.log("잔액 부족 또는 유효하지 않은 금액.");
        }
    }

    getBalance() {
        return this.#balance;
    }
}

const myAccount = new Account(10000);
myAccount.deposit(5000);
myAccount.withdraw(2000);
console.log(myAccount.getBalance());
// console.log(myAccount.#balance); // 클래스 밖에서는 직접 접근 불가 (SyntaxError)

출력은 입금: 5000, 현재 잔액: 15000, 출금: 2000, 현재 잔액: 13000, 13000입니다. #balance는 클래스 밖에서 읽거나 바꿀 수 없고, 바깥 코드는 deposit(), withdraw(), getBalance()로만 잔액을 다룹니다.

비공개 필드는 접근을 막을 뿐 잘못된 값을 걸러 주지는 않습니다. 그래서 메서드마다 Number.isFinite()로 금액을 확인합니다. 이 검사가 없으면 문자열 "5000"을 입금했을 때 "5000" > 0은 참이 되지만, +=가 숫자 덧셈 대신 문자열을 이어 붙여 잔액이 "100005000"이 됩니다.

더 깊이: # 필드는 얼마나 숨겨질까

#balance 같은 비공개 필드는 이름 앞에 밑줄을 붙여 _balance처럼 쓰던 예전의 관례와 다릅니다. 관례는 약속일 뿐이라 바깥에서 account._balance로 얼마든지 읽고 바꿀 수 있지만, # 이름은 언어가 막습니다. 클래스 밖에서 account.#balance라고 적는 것은 실행하기 전에 문법 오류가 되고, account["#balance"]처럼 대괄호로 문자열을 넣어도 비공개 필드에는 닿지 않습니다. Object.keys(), for...in 같은 열거나 Object 메서드로도 보이지 않으며, structuredClone()도 이 필드를 복사하지 않습니다.

비공개 필드는 일반 속성과 다르게 실패합니다. 일반 속성은 없으면 undefined가 나오지만, 비공개 필드를 갖지 않은 객체에서 obj.#balance를 읽으면 TypeError가 납니다. 그래서 클래스의 정적 메서드가 다른 객체를 받을 때는 #balance in obj로 그 객체가 이 클래스의 비공개 필드를 가졌는지 먼저 확인할 수 있습니다. 이 식은 필드가 있으면 true, 없으면 false를 돌려줄 뿐 오류를 내지 않습니다.

Object.freeze()도 비공개 필드에는 효과가 없습니다. 객체를 얼려도 클래스의 메서드는 #balance를 계속 바꿀 수 있으므로, 불변 객체를 만들려는 의도라면 메서드가 필드를 바꾸지 않도록 따로 설계해야 합니다. 규칙은 MDN의 비공개 속성 설명에 정리되어 있습니다.

상속

상속(inheritance)은 부모 클래스의 속성과 메서드를 자식 클래스가 물려받아 그대로 쓰거나 확장하는 것입니다. 공통 기능을 부모에 한 번만 적으면 되므로 코드를 다시 쓰기 쉽고, 앞 글처럼 자바스크립트에서는 프로토타입 체인이나 class의 extends로 구현합니다.

// 상속 예시 (앞 글에서 다룬 class 상속)
class Vehicle {
    constructor(brand) {
        this.brand = brand;
    }
    start() {
        console.log(`${this.brand} 시동을 겁니다.`);
    }
}

class Car extends Vehicle {
    constructor(brand, model) {
        super(brand);
        this.model = model;
    }
    drive() {
        console.log(`${this.brand} ${this.model}가 운전 중입니다.`);
    }
}

const myCar = new Car("현대", "쏘나타");
myCar.start(); // 현대 시동을 겁니다. (부모 클래스의 메서드)
myCar.drive(); // 현대 쏘나타가 운전 중입니다. (자식 클래스의 메서드)

추상화

추상화(abstraction)는 복잡한 내부 구현을 숨기고, 쓰는 쪽에 필요한 기능만 드러내는 것입니다. 쓰는 사람은 객체가 무엇을 하는지만 알면 되고 어떻게 하는지는 몰라도 됩니다. 앞의 Account를 쓰는 코드는 deposit(5000)이 입금한다는 것만 알면 되고, 잔액을 어떤 필드에 어떻게 저장하는지는 몰라도 됩니다.

다형성

다형성(polymorphism)은 같은 이름의 메서드가 객체의 종류에 따라 다르게 동작하는 것입니다. 자바스크립트에서는 자식 클래스가 부모의 메서드를 다시 정의하는 메서드 오버라이딩(overriding)으로 흔히 구현합니다.

class Animal {
    sound() {
        console.log("동물 소리!");
    }
}

class Cat extends Animal {
    sound() { // 부모의 sound 메서드를 재정의 (오버라이딩)
        console.log("야옹!");
    }
}

class Dog extends Animal {
    sound() { // 부모의 sound 메서드를 재정의 (오버라이딩)
        console.log("멍멍!");
    }
}

const animals = [new Animal(), new Cat(), new Dog()];

animals.forEach(animal => animal.sound());
/*
결과:
동물 소리!
야옹!
멍멍!
*/
// 동일한 sound() 메서드를 호출했지만, 객체 타입에 따라 다른 동작을 보입니다.

세 객체 모두 sound()를 똑같이 호출했지만, 앞 글에서 본 것처럼 각 객체의 프로토타입 체인에서 먼저 찾은 sound가 실행되므로 결과가 다릅니다. 호출하는 쪽은 객체의 종류를 따지지 않고 같은 방식으로 다룰 수 있어, 새 동물 클래스를 추가해도 forEach 코드는 고치지 않아도 됩니다.

OOP의 장단점

OOP는 클래스와 상속으로 코드를 다시 쓰고 확장하기 쉽고, 캡슐화 덕분에 내부를 바꿔도 다른 코드에 미치는 영향이 적습니다. 잘 나눈 객체는 따로 개발하고 테스트할 수 있으며, 새 기능을 추가할 때 기존 코드를 덜 고쳐도 됩니다.

반면 처음 설계에 시간이 들고, 클래스 계층이 복잡해지면 이해하기 어려워집니다. 특히 상속이 깊어지면 부모 클래스의 작은 변경이 예상하지 못한 자식 클래스에서 문제를 일으킬 수 있습니다.


함수형 프로그래밍

함수형 프로그래밍은 프로그램을 순수 함수(pure function)들의 조합으로 보고, 데이터를 바꾸지 않는 불변성(immutability)을 중시하는 패러다임입니다. 상태 변경과 부작용(side effect)을 줄여 코드의 동작을 예측하기 쉽게 하고 버그가 생길 여지를 줄이는 데 초점을 둡니다.

순수 함수

순수 함수는 같은 입력에 항상 같은 결과를 돌려주고, 함수 바깥의 어떤 상태도 바꾸지 않는 함수입니다. 바깥 변수를 수정하거나, 콘솔에 출력하거나, 네트워크 요청을 보내는 일은 모두 부작용입니다.

// 순수 함수 (Pure Function)
function add(a, b) {
    return a + b; // 항상 동일한 입력에 동일한 출력, 외부 상태 변경 없음
}

// 순수하지 않은 함수 (Impure Function) 예시
let total = 0;
function addToTotal(num) {
    total += num; // 외부 변수 total을 변경 (부작용)
    return total;
}
// console.log() 또한 부작용입니다.

add(2, 3)은 언제 몇 번을 호출해도 5이고 바깥에 아무 흔적도 남기지 않습니다. 반면 addToTotal(3)은 바깥의 total을 바꾸므로 두 번 호출하면 3, 6으로 결과가 달라집니다. 바깥 값을 바꾸지 않더라도 Date.now()나 Math.random()처럼 호출할 때마다 달라지는 값을 읽어 결과를 만드는 함수는 같은 입력에 같은 결과를 보장하지 못하므로 순수 함수가 아닙니다.

불변성

불변성은 한 번 만든 데이터를 바꾸지 않는 것입니다. 값을 바꿔야 할 때는 원본을 복사한 새 데이터에 변경을 적용합니다. 원본이 그대로 남으므로 언제 어떤 값이 있었는지 추적하기 쉽고, 같은 데이터를 쓰는 다른 코드가 뜻하지 않게 영향을 받지 않습니다.

// 불변성 유지 (배열 복사)
const originalArray = [1, 2, 3];
const newArray = [...originalArray, 4]; // 전개 구문으로 새로운 배열 생성

console.log(originalArray); // 결과: [1, 2, 3] (원본은 변경되지 않음)
console.log(newArray);    // 결과: [1, 2, 3, 4]

// 불변성 유지 (객체 복사)
const originalUser = { name: "Alice", age: 30 };
const updatedUser = { ...originalUser, age: 31 }; // 새로운 객체 생성

console.log(originalUser); // 결과: { name: 'Alice', age: 30 } (원본은 변경되지 않음)
console.log(updatedUser);  // 결과: { name: 'Alice', age: 31 }

코드 주석에서 전개 구문이라고 부른 스프레드 문법(spread syntax) ...은 얕은 복사입니다. 이 예제의 name과 age는 원시값이라 문제가 없지만, 속성 값이 객체라면 복사본과 원본이 그 객체를 함께 가리킵니다. 또 const는 변수에 다른 값을 다시 넣지 못하게 할 뿐 객체의 속성을 바꾸는 것까지 막지는 않습니다. 중첩된 값을 바꿀 때는 바뀌는 경로에 있는 객체를 하나하나 새로 만들어야 합니다. 스프레드 문법의 자세한 규칙은 8장의 구조 분해 할당과 스프레드 연산자에서 다룹니다.

고차 함수

고차 함수(higher-order function)는 함수를 인수로 받거나 함수를 돌려주는 함수입니다. 배열의 map(), filter(), reduce()가 대표적이며, 함수형 프로그래밍에서 데이터를 변환하는 기본 도구입니다.

const numbers = [1, 2, 3, 4, 5];

// map: 배열의 각 요소에 함수를 적용하여 새로운 배열 반환 (불변성 유지)
const squaredNumbers = numbers.map(num => num * num);
console.log(squaredNumbers); // 결과: [1, 4, 9, 16, 25]

// filter: 조건에 맞는 요소만 걸러내어 새로운 배열 반환 (불변성 유지)
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // 결과: [2, 4]

// reduce: 배열의 모든 요소를 하나의 값으로 축소
const sum = numbers.reduce((accumulator, current) => accumulator + current, 0);
console.log(sum); // 결과: 15

map()은 각 요소에 함수를 적용한 새 배열을, filter()는 조건에 맞는 요소만 담은 새 배열을 돌려주며 원본 numbers는 바뀌지 않습니다. reduce()의 누적값은 0에서 시작해 1, 3, 6, 10, 15로 늘어 마지막에 15가 됩니다.

다만 메서드 이름이 순수함을 보장하지는 않습니다. 이 예제의 콜백은 요소를 바꾸지 않지만, 콜백 안에서 요소 객체의 속성을 수정하면 map()이 새 배열을 돌려줘도 원본 배열의 객체는 바뀝니다.

FP의 장단점

FP는 순수 함수와 불변성 덕분에 코드의 동작을 예측하기 쉽고, 부작용이 없는 함수는 입력과 결과만 확인하면 되므로 단위 테스트를 쓰기 쉽습니다. 데이터를 바꾸지 않으므로 여러 작업이 같은 데이터를 함께 읽어도 안전하고, 함수 단위로 나뉘어 다시 쓰기도 좋습니다.

반면 데이터를 바꾸는 대신 복사하므로 큰 데이터에서는 성능에 영향을 줄 수 있습니다. 복잡한 상태를 여러 함수로 넘겨 가며 관리하면 데이터의 흐름이 길어질 수 있고, 처음 접하는 사람에게는 익숙해지는 데 시간이 걸립니다.


같은 입금을 두 방식으로

앞의 Account는 같은 객체의 잔액을 직접 바꿉니다. 다음 함수는 같은 입금을 순수 함수로 표현해, 입력 잔액을 바꾸는 대신 새 잔액을 돌려줍니다. 잘못된 금액이 들어오면 조용히 넘어가지 않고 예외를 던집니다.

function depositValueForPractice(balance, amount) {
    if (!Number.isFinite(balance) || balance < 0 ||
        !Number.isFinite(amount) || amount <= 0) {
        throw new Error("유한한 잔액과 양수 입금액이 필요합니다.");
    }
    return balance + amount;
}
const previousBalanceForPractice = 10000;
const nextBalanceForPractice = depositValueForPractice(previousBalanceForPractice, 5000);
console.log(previousBalanceForPractice, nextBalanceForPractice);

출력은 10000 15000입니다. 이전 값과 다음 값이 따로 남아 있으므로, 화면에 이전 잔액을 다시 보여 주거나 두 값을 비교하기 쉽습니다. 함수 안에서는 DOM이나 바깥 변수를 바꾸지 않으며, console.log()는 결과를 확인하기 위한 바깥 코드입니다. 이 예제는 금액이 유효한 숫자인지만 확인하며, 실제 화폐의 소수점 처리나 동시에 들어온 입금 처리까지 다루지는 않습니다.

그림의 세 번째 경로처럼 두 방식을 함께 쓸 수도 있습니다. 다음 코드는 앞 예제에 이어 실행하며, 검증과 계산은 depositValueForPractice()에 맡기고 객체는 잔액을 바꾸고 이전 값을 기록하는 일만 합니다.

class HistoryAccount {
    #balance;
    #history = [];

    constructor(initialBalance) {
        this.#balance = initialBalance;
    }

    deposit(amount) {
        const next = depositValueForPractice(this.#balance, amount); // 검증과 계산은 순수 함수
        this.#history.push(this.#balance); // 바꾸기 전의 잔액을 기록
        this.#balance = next;
    }

    undo() {
        if (this.#history.length > 0) {
            this.#balance = this.#history.pop();
        }
    }

    getBalance() {
        return this.#balance;
    }
}

const historyAccount = new HistoryAccount(10000);
historyAccount.deposit(5000);
console.log(historyAccount.getBalance()); // 15000
historyAccount.undo();
console.log(historyAccount.getBalance()); // 10000

deposit()은 순수 함수가 돌려준 새 값으로 잔액을 바꾸기 전에 기존 잔액을 #history에 넣어 두고, undo()는 마지막으로 기록한 값을 꺼내 되돌립니다. 계산 규칙은 순수 함수라 따로 테스트하기 쉽고, 상태를 바꾸는 일은 한 객체 안에만 있어 다른 코드가 잔액을 직접 바꿀 수 없습니다.


두 패러다임을 함께 쓰기

자바스크립트는 객체 리터럴, 프로토타입, class 같은 객체 지향의 특징과, 함수를 값처럼 다루는 일급 함수(first-class function), 클로저, 고차 함수 같은 함수형의 특징을 모두 갖춘 멀티 패러다임(multi-paradigm) 언어입니다. 그래서 문제의 성격에 따라 두 방식을 골라 섞어 쓸 수 있습니다.

객체 지향은 잔액처럼 오래 유지되는 상태와 그 상태를 바꾸는 규칙을 한 객체가 맡아 관리할 때 알맞습니다. 이름이 UI 컴포넌트라고 해서 객체 지향인 것은 아니며, 상태와 그 상태를 바꾸는 코드가 실제로 한 곳에 묶여 있는지를 봐야 합니다. 함수형은 데이터를 다른 모양으로 바꾸는 처리, 같은 입력에 같은 결과가 중요한 계산, 여러 곳에서 다시 쓰는 작은 유틸리티 함수에 알맞습니다. 웹 애플리케이션에서 화면의 상태 변화를 관리하거나 비동기 작업의 결과를 가공할 때도 함수형 방식이 자주 쓰입니다.

두 패러다임은 하나를 고르면 다른 하나를 버려야 하는 관계가 아닙니다. 기능을 설계할 때는 상태가 얼마나 오래 유지되는지, 데이터를 얼마나 자주 다른 모양으로 바꾸는지, 이전 값을 보관해야 하는지를 기준으로 어느 쪽을 중심에 둘지 정하면 됩니다.


정리

  • 프로그래밍 패러다임은 코드를 나누고 설계하는 사고방식이며, 자바스크립트는 객체 지향과 함수형을 함께 쓸 수 있습니다.
  • 객체 지향은 데이터와 메서드를 객체로 묶고 캡슐화, 상속, 추상화, 다형성으로 상태와 규칙을 관리합니다.
  • 비공개 필드는 바깥의 접근을 막을 뿐이므로, 잘못된 값을 거르는 검사는 메서드에 따로 넣어야 합니다.
  • 함수형은 같은 입력에 같은 결과를 주고 바깥을 바꾸지 않는 순수 함수와, 원본 대신 복사본을 만드는 불변성을 중시합니다.
  • 스프레드 문법은 얕은 복사이고 const는 재할당만 막으므로, 중첩된 값은 바뀌는 경로의 객체를 모두 새로 만들어야 합니다.
  • 검증과 계산은 순수 함수에, 상태 변경과 이력은 객체에 맡기는 식으로 두 방식을 조합할 수 있습니다.

연습 문제

1. 이전 값 보관하기

순수 함수 예제에 이어 nextBalanceForPractice에 2000원을 다시 입금한 결과를 finalBalanceForPractice에 저장하고, 세 잔액을 한 줄로 출력해 보세요. 같은 일을 Account 클래스로 한다면, 처음 잔액인 10000으로 되돌리기 위해 무엇을 더 기록해야 할까요?

해설 보기

const finalBalanceForPractice = depositValueForPractice(nextBalanceForPractice, 2000); 다음에 console.log(previousBalanceForPractice, nextBalanceForPractice, finalBalanceForPractice);를 실행하면 10000 15000 17000이 출력됩니다. 세 값이 모두 남아 있으므로 원하는 시점의 값으로 화면을 되돌릴 수 있습니다.

Account는 같은 객체의 #balance를 덮어쓰므로, 되돌리려면 HistoryAccount처럼 바꾸기 전의 잔액이나 거래 내역을 따로 기록하고 이를 쓰는 메서드를 만들어야 합니다. 객체 지향으로도 이력을 관리할 수 있고, 순수 함수를 쓴다고 화면 갱신 같은 부작용이 모두 사라지는 것도 아닙니다.

2. 순수 함수 고르기

다음 세 함수 가운데 순수 함수는 어느 것일까요? 순수하지 않은 함수는 이유도 적어 보세요.

function applyBonus(user) {
    user.points += 10;
    return user;
}

function withBonus(user) {
    return { ...user, points: user.points + 10 };
}

function stamp(value) {
    return { value, createdAt: Date.now() };
}
해설 보기

순수 함수는 withBonus뿐입니다. 입력 객체를 바꾸지 않고 새 객체를 돌려주며, 같은 user에는 항상 같은 내용의 결과를 줍니다.

applyBonus는 인수로 받은 객체의 points를 직접 바꾸므로 부작용이 있고, 같은 객체로 두 번 호출하면 결과도 달라집니다. stamp는 바깥을 바꾸지는 않지만 Date.now()를 읽어 호출할 때마다 createdAt이 달라지므로, 같은 입력에 같은 결과를 보장하지 못합니다.

3. 얕은 복사의 함정

다음 코드의 출력은 무엇일까요? original을 바꾸지 않고 copy의 도시만 "부산"으로 바꾸려면 어떻게 고쳐야 할까요?

const original = { name: "Alice", address: { city: "서울" } };
const copy = { ...original };
copy.address.city = "부산";
console.log(original.address.city);
해설 보기

부산이 출력됩니다. 스프레드 문법은 바깥 객체만 새로 만들고, address 속성에는 원본과 같은 객체의 참조를 복사합니다. 그래서 copy.address와 original.address는 같은 객체이고, 한쪽에서 바꾸면 다른 쪽에서도 바뀐 값이 보입니다.

const copy = { ...original, address: { ...original.address, city: "부산" } };처럼 바뀌는 경로의 address 객체도 새로 만들면, original.address.city는 서울로 남습니다.

다음 글 고차 함수와 콜백에서는 이 글에서 잠깐 본 고차 함수를 직접 만들어 보고, 동기 콜백과 비동기 콜백이 실행되는 흐름과 콜백이 깊게 중첩될 때 생기는 문제를 살펴봅니다.