본문 바로가기
CS/OOP

객며든 어느 프론트의 엘레강트 오브젝트 정리

by 그냥하는거지뭐~ 2025. 3. 26.

0. 들어가며: 객체지향은 파일을 분류하는 기법이다.

우테코에서의 가장 큰 수확은 제이슨의 엘강오 스터디다. 처음엔 망설였다. '에이.. 프론튼데 객체지향까지 알아야해?' 라는 어리석은 판단으로 하마터면 이 기회를 지나칠 뻔 했지만, 원온원을 했던 제이슨이 DM으로 다시 한 번 권유해줬고, 덕분에 용기 내서 들어가게 됐다.

날 구해준 DM..

 

그렇게 시작하게 된 엘강오 스터디.. 뭐가 가장 많이 바뀌었나

가장 크게 바뀐건, 객체로 생각하는 법을 배우면서 "유기체"와 "관계" 중심으로 코드를 바라보게 됐다는 점이다. 예전엔 대부분의 코드를 함수 중심으로 작성했다. 함수 하나하나는 나쁘지 않았지만, 그 함수들이 서로 협력하거나 흐름을 만드는 방식에는 체계가 없었고, 전체 코드는 어딘가 자연스럽지 못했다. 
 
객체지향을 공부하면서, 제이슨이 말하던 "OOP는 파일을 분류하는 기법입니다"라는 말이 점점 와닿기 시작했다. 아직은 많이 어렵고 배워야 할 것도 많지만, 코드를 짤 때 나름의 규칙과 철학이 생겼다. 
 
누군가 "프론트가 OOP를 알아야 해?" 라고 묻는다면, "일단 공부해보고 불평해라" 라고 말해주고 싶다. (과거의 나에게 하는 말이기도 함)
객체지향을 알고 나서부터는, "이제 class 없이 어케살아..?"가 되어버렸다. 제대로 객며들었다.

  • 작은 객체들을 조합해서 큰 객체를 만들고, 앱 전체는 이 객체들의 협력으로 구성된다.
  • 애플리케이션은 더 이상 명령받지 않고, 스스로 움직이는 유기체처럼 행동한다.
  • 똑똑한 객체가 데이터를 품고, 자신이 할 일을 스스로 수행한다.

이 글은, 엘강오를 읽고 배운 점을 정리한 글이다. 


1. 객체를 존중하는 방법: 이름부터 신경써서 지어줍시다. 

1.1. 이름은 객체에 대한 존중의 시작이다

객체지향 설계는 결국 ‘존중’의 언어다.
객체는 단순한 데이터 구조가 아니라, 
책임을 가진 주체이고, 우리는 그 객체에게 일을 맡기고, 기대하고, 협력한다.
 
그렇다면 설계자로서 우리가 가장 먼저 할 일은 그 객체에게 존중받을 만한 이름을 붙여주는 것이다.
 
객체가 제대로 기능하려면,
이름에서부터 그 객체의 책임과 정체성이 드러나야 한다.
이름을 보면 "이 객체가 누구인지", "무슨 역할을 하는지"가 명확하게 떠올라야 한다.
 
객체의 이름은 객체 자체보다 앞선다. 
그리고 이 이름은 단지 이 객체를 사용하는 client를 위한 설명만이 아니라, 
설계자의 태도이고, 객체를 어떻게 바라보는가에 대한 철학이 드러나는 지점이다.  
 

1.2. class 이름에 -er 을 붙이지 말자

클래스 이름에 -er을 붙이는 순간, 그 객체는 "누군가를 도와주는 유틸", "어딘가의 보조자", "작업자의 손"으로 격하된다.

class Parser {}   // ❌ 누군가를 위해 일하는 도구 느낌
class ParsedCell {} // ✅ 의미 있는 결과물, 상태 자체
  • UserRegisterer → Registration 
  • Manager → Project 
  • Sender → Message 

-er이라는 접미사는 객체를 도우미 함수의 포장지로 만들고,
객체의 책임과 정체성을 뚜렷하게 드러내지 못하게 만든다.
객체는 보조자가 아니라,
자신의 책임을 갖고 스스로 움직이는 존재여야 한다.
 

1.3. 메서드 이름도 잘 짓자

클래스 이름만 중요한 게 아니다. 메서드 이름에도 객체에 대한 태도가 드러난다.
객체 안의 메서드는 크게 두 종류로 나뉜다.

  • builder(빌더): 객체를 만들거나 가공하여 새로운 객체를 반환
  • manipulator(조정자): 외부 상태나 객체의 의미를 직접 변경

① 빌더(builder): 명사형 메서드

  • 무언가를 새로 만들어내는 메서드
  • 항상 새로운 객체를 반환. 항상 리턴값이 존재
  • 내부 상태는 바꾸지 않음 (불변)
  • 이름은 명사처럼 짓는다
class Book {
  withAuthor(author: string): Book {}
  withTitle(title: string): Book {}
}

const book1 = new Book();
const book2 = book1.withTitle("The Elegant Object");
const book3 = book2.withAuthor("Yegor Bugayenko");

→ book1은 변하지 않는다.
→ 빌더는 항상 새로운 객체를 반환한다.
불변성 유지 + 조합 방식을 통해 객체 간 협력을 이끈다.
 
② 조정자(manipulator): 동사형 메서드

  • 실세계의 어떤 상태나 엔티티를 수정하는 책임
  • 반환값은 없음 (void)
  • 이름은 동사처럼 짓는다
  • ex) save, put, remove, quicklyPrint
printer.save();
printer.quicklyPrint();
printer.remove("trash.txt");

→ 조정자는 행동 자체가 핵심이다.
→ “상태를 바꾸는 역할”을 명확히 인식하고 있어야 한다.
 

1.4. Command와 Query는 반드시 분리해야 합니다 (CQS 원칙)

CQS: Command Query Separation

“상태를 변경하는 메서드(Command)는 값을 반환하지 말고,
값을 반환하는 메서드(Query)는 상태를 변경하지 말자.”

 
즉, 하나의 메서드는 하나의 책임만 가져야 한다.
 
잘못된 예시
- saveAndReturnId(): 저장과 반환이 동시에 일어남
- updateAndLog(): 변경과 side effect의 혼합
 

1.5. 메서드를 프로퍼티처럼 쓸 수는 없을까?

가끔은 “이건 getter니까 프로퍼티처럼 써도 되지 않나?” 싶은 메서드가 있다.
예를 들어:

const count = items.length; // 프로퍼티
const price = cart.totalPrice(); // 메서드?

 
이럴 때,
밖에서 보기엔 그 값이 계산되어있든 저장되어 있든 상관없다는게 포인트다. 의미적으로 "속성"이라면, 프로퍼티처럼 보여도 된다. 

  • 단순한 속성이라면 → 프로퍼티처럼 보여도 OK
  • 계산식이 있더라도, 부작용이 없다면 → getter도 괜찮다
  • 하지만 내부에서 뭔가 조작하거나 외부에 영향을 미친다면, 반드시 ()를 붙여 메서드로 표현해야 한다

1.6. 정리

  • 클래스 이름은 객체의 정체성과 책임을 드러내야 한다.
  • -er은 가능한 피하고, 역할 중심의 명사를 쓰자.
  • 메서드는 builder vs manipulator로 구분하자.
  • 하나의 메서드는 하나의 책임만 갖게 하자 (CQS).
  • 프로퍼티처럼 보이는 정보는 진짜 속성처럼 다루자. 하지만, 부작용이 있다면 반드시 함수로 명시하자.
🤔 추가
Q. 빌더 함수의 이름은 어떻게 잘 지을 수 있을까요?
Q. 불변 객체의 빌더 함수 이름은 무조건 명사일까요?

2. 객체의 식별자는 기본적으로 세계 안에서 객체가 위치하는 "좌표"입니다.

객체는 단순히 상태의 집합이 아니다.
객체는 어떤 의미 있는 위치를 가져야 한다. 그게 바로 식별자(identifier)다.
이때 객체의 모든 상태는 곧 식별자다.
객체는 자신의 상태로 정의되며, 그 상태는 쉽게 바뀌어선 안 된다. 
 
Q. identifier(식별자)란 정확히 어떤 뜻인가?

  • 객체는 식별자를 통해 세상과 구별되고, 이 식별자의 불변성을 통해 객체의 존재가 유지된다. 
  • 어떤 객체가 '그 객체'인지 판별하기 위한 좌표, 정체성, 주소. 그게 식별자다.

이렇게 생각해보자. 
주민등록번호는 바뀌지 않지만, 주소는 바뀔 수 있다. 서울에 살든 광주에 살든, 나는 여전히 똑같은 '나'일 것이다. 이때 주소는 상태고, 주민등록번호는 식별자다. 
 
Q. 객체의 식별자가 바뀌는 순간, 그것은 다른 객체다. 
식별자는 객체의 영속성을 보장해준다.
그래서 객체의 식별자가 바뀌는 순간, 그것은 다른 객체가 된다. 

// ❌ 변경 가능한 id → 객체의 정체성 파괴
user.id = "new-id";

 
식별자는 불변이어야 한다. 객체의 상태 중 적어도 하나는 외부에서 바꿀 수 없어야 한다.
그걸 우리는 캡슐화(encapsulation)라고 부른다.

  • class에서 캡슐화하는 방법
class Cash {
  #digits;
  #cents;
  #currency;

  constructor(digits, cents, currency) {
    this.#digits = digits;
    this.#cents = cents;
    this.#currency = currency;
  }
}
  • 클로저를 활용해서 캡슐화하는 방법
function Cash(digits, cents, currency) {
    let _digits = digits;
    let _cents = cents;
    let _currency = currency;

    return {
      getAmount: function() {
        return `${_digits}.${_cents} ${_currency}`;
      }
    };
}

 
Q. 상태가 같은 객체.. 같은 객체인가? 

class Name {
  constructor(
    public readonly first: string,
    public readonly last: string
  ) {}
}
const name1 = new Name("정", "환희");
const name2 = new Name("정", "환희");

name1과 name2는 메모리상으론 분명히 다른 객체다. 하지만 상태가 완전히 동일하다면, 우리는 그 둘을 논리적으로 같은 객체라고 간주할 수 있다. 이게 객체지향의 의도다. 
 
Q. 같다고 보는 이유는 뭘까?
'같다'는 건 사실 인간적인 개념이다. 물리적으로 완전히 같을 필요는 없다.
의미가 같다면, 우리는 그걸 같은 거라고 본다.  
같음을 보장할수록 우발적으로 발생할 수 있는 오류들을 해소할 수 있다. 

  • 예측 가능성이 올라간다
  • 불필요한 조건 분기가 줄어든다
  • 테스트 코드가 간결해진다
  • 캐시나 비교 같은 최적화에도 유리하다

즉, 객체는 그 자체로 고유한 존재여야 하고, 그 고유함을 식별자라는 좌표로 표현해야 한다. 그리고 이 식별자는 바뀌어선 안 된다.
정체성이 흔들리면, 객체도 무너진다.

🤔 추가
Q. 같다고 보면 동시성에서 안전해진다?
동시성 환경의 핵심 문제는 공유된 가변 상태이다. 여러 스레드가 하나의 객체를 동시에 읽거나 수정하면 충돌이 발생한다.
이를 막기 위해선 lock, mutex, synchronized 같은 동기화 비용이 필요하다. 특히 공유 리소스가 많아질수록, 충돌 가능성과 비용도 증가한다.

하지만 불변 객체는 상태가 절대 바뀌지 않기 때문에 안전하다. 내부 상태가 절대 바뀌지 않으므로, 여러 스레드가 동시에 접근해도 절대 충돌하지 않는다.

상태가 같다면 같은 객체로 간주한다 => 상태 기반 동등성(equality by value)
즉, 식별자(id)가 달라도 상태가 같으면 우리는 같은 의미를 가지는 객체로 본다는 뜻이다. 같은 의미를 공유하는 객체는 충돌을 일으킬 이유가 없다. 

즉, 상태가 같다면 그 객체들을 같다고 간주해버리는 게 훨씬 안전하고 빠르다.

+ Q. 왜 UI 스레드는 단일 스레드인가? 왜 멀티 스레드가 아닐까?

3. 불변 객체로 만들자, 가변 객체가 등장하는 시점을 최대한 미루자.

불변 객체는 어떤 방식으로든 자기 자신을 수정할 수 없다. 항상 원하는 상태를 가지는 새로운 객체를 생성해서 반환해야 한다. 

 
Q. 세계는 본질적으로 가변적이기 때문에 불변 객체만으로 세상을 표현하기가 불가능하지 않을까?
맞다. 세상은 가변적이다. 주소도 바뀌고, 통장 잔고도 바뀌고, 심지어 내 기분도 바뀐다. 하지만 그렇다고 해서, 불변 객체로 세상을 모델링할 수 없는 것은 아니다. 
 
오히려, 불변 객체로 표현할수록 세상에 더 정직해진다.
그리고 개발자로서 우리는, “무엇이 바뀌었는가”가 아니라 “이전과 이후가 어떻게 다른가”에 집중하게 된다.
 

3.1. 불변 객체로 만들면 어떤 게 좋은데?

const five = new Cash(5)
const fifty = five.mul(10) // 새로운 객체 반환

이 코드 한 줄만 봐도, 불변 객체가 왜 좋은지 여러 이유를 떠올릴 수 있다.
 

① 예측 가능성

  • five는 절대 바뀌지 않는다.
  • fifty는 five와 완전히 다른 객체다.
  • 식별자가 섞이거나 공유되는 일도 없다.

② 상태 변경 순서로 인한 버그가 없다

가변 객체면 이런 일이 일어날 수 있다. 

mul() {
  this.dollars *= 10;
  delay(); // 어떤 비동기 상황
  this.cents *= 10;
}

cash.mul(10);
console.log(cash.print()); // ❌ 중간 상태가 출력될 수도 있음

전형적인 Race Condition이다. 
그러니까 객체의 상태가 바뀌면, 어떤 줄이 앞에 있어야 하고 어떤 줄이 뒤에 나와야 하는지를 기어가는 일은 프로그래머의 몫이 된다. 이건 엄청 큰 유지보수 이슈이다. 
하지만 불변 객체는 애초에 상태를 바꾸지 않기 때문에 이런 문제가 아예 없다.
 

③ 단순해진다

불변 객체는 기본적으로 더 작아진다. 왜냐하면 불변 객체는 오직 생성자에서만 상태를 초기화할 수 있다. 그래서 점점 객체가 커지면 생성자가 너무 커지고, 개발자는 자연스럽게 뭔가 잘못돼 가고 있다는 사실을 깨닫고 클래스를 분리하게 된다.
결국 코드가 더 작고, 더 명확하고, 더 깔끔해진다. 
 
Q. 함수형 프로그래밍이 미는 순수 함수의 철학과 비슷한데? 
불변 객체를 쓰다 보면 자연스럽게 순수 함수처럼 사고하게 된다.

  • 입력 → 출력
  • 부작용 없음
  • 상태 공유 없음

OOP인데도 FP 같다. 다른 패러다임이지만, "예측 가능한 코드, 복잡도 분리"라는 같은 목적을 이루기 위해 만들어진 것 같다. 불변 객체를 사용하는 OOP는, 철학적으로 순수 함수에 가까운 객체 설계 방식으로 수렴한다. 

패러다임철학목적
OOP객체는 책임을 지고, 협력한다.복잡한 시스템을 객체 간 분리와 책임 분담으로 해소한다.
FP함수는 상태를 바꾸지 않고, 데이터를 흘려보낸다. side effect 없는 계산으로 예측 가능성을 확보한다. 

 
 

3.2. "불변이다"를 다시 정의해보자

다음 예시에서, WebPage는 불변 객체일까, 가변 객체일까? 

class WebPage {
  private readonly uri: URI;

  constructor(uri: URI) {
    this.uri = uri;
  }

  content(): string {
    // HTTP 요청 수행 → 결과는 예측 불가
  }
}

uri 필드를 수정할 수 없고, 구조도 항상 같다. 그런데 content() 결과는 외부 환경에 따라 다르다. 웹 서버 상태, 네트워크 상황 등에 따라 결과는 예측이 불가하다. 그럼 불변인가 가변인가? 
 
그럼에도 불구하고, 이 WebPage 객체는 충성스러운 객체라고 볼 수 있다.
왜냐하면:

  • 항상 같은 uri를 바라보고
  • 그 uri가 가리키는 “진짜 웹 페이지의 현재 상태”를 충실히 반영하기 때문이다.

객체 자신은 변하지 않지만, 객체가 대표하는 세상은 변할 수 있으므로 행동은 달라질 수 있다. 
그리고 객체는 그 변화에 정직하게 반응한다.
 
Q. 그래서 "불변"이란? 
단순히 “출력이 항상 같다”는 뜻이 아니다. 진짜 불변 객체란, 다음을 뜻한다. 

자신은 변하지 않지만, 자신이 대표하는 대상의 변화에 충실하게 반응하는 객체

 
📚 개념 정리
객체는 식별자, 상태, 행동으로 구성된다. 

  • 식별자: 객체를 구분하는 참조, 좌표, 경로 등
  • 상태: 객체가 나타내는 대상의 현재 상태 (파일 크기, 내용 등)
  • 행동: 메시지를 수신했을 때 수행하는 로직

이걸 WebPage에 대입해보자:

  • 식별자: uri
  • 상태: 웹 페이지의 내용
  • 행동: content()를 통해 상태를 읽어오는 행위

이때 중요한 건, 객체 자신은 바뀌지 않았다는 점이다.
 
Q. 충성스러운 객체란? 

  • 자신이 대표하는 실세계 엔티티의 변화를 숨기지 않고 반영한다
  • 행동은 예측 불가능할 수 있지만, 자기 정체성(식별자)은 흔들리지 않는다
  • 외부 세계와의 연결을 유지하며, 사용자가 객체를 통해 세상을 이해할 수 있게 돕는다

즉, 행동은 가변적이어도 객체 자신은 불변성을 유지하고, 그 행동은 일관된 참조점(uri)을 기준으로 작동한다.
 
Q. 상수 객체 vs 충성스러운 객체 vs 가변 객체
식별자와 상태가 완전히 일치하는 객체를 상수 객체라고 부르기로 했다. 대표적으로 Name("정", "환희"), Point(3, 4)가 될 수 있고,
상태 = 식별자 = 불변인 것이다. 그래서 모든 상수 객체는 불변 객체이지만, 모든 불변 객체가 상수 객체는 아니다. 
 
반면 가변 객체는 상태가 언제든지 바뀔 수 있다. 그래서 식별자를 따로 정의해야 한다. 상태가 아무리 바뀌더라도 “얘는 그때 그 객체”라고 말해줄 수 있는 누군가는 있어야 한다는 뜻이다. 
 
즉, 정리하면 

  • 상수 객체: 상태 자체가 식별자이며, 완전히 불변함. 의미도, 내용도 변하지 않음. (상수 객체 ⊂ 불변 객체)
  • 충성스러운 객체: 객체는 불변이지만, 대표하는 외부 대상의 변화를 정직하게 반영함. (충성스러운 객체 ⊂ 불변 객체)
  • 가변 객체: 내부 상태가 언제든지 바뀌고, 동일성을 보장하기 위해 식별자가 반드시 필요함.

4. 객체들은 서로 협력하고, 이들에겐 계약서가 필요합니다: interface

객체지향의 키워드는 협력이다.

4.1. 객체는 관계 안에서 존재합니다. 

코드를 짜다 보면 "이 객체가 혼자 할 수 있는 일이 아닌데.. 다른 객체의 도움이 필요하네?" 하는 순간이 있다. 
이때 객체는 다른 객체를 사용하거나, 호출하거나, 의존하게 된다.

  • 서로 모르는데 일을 할 수는 없다 → 결국 의존하게 된다
  • 그런데 의존에도 레벨이 있다
    • 어떤 경우는 너무 직접적으로 붙어 있다 → 강한 결합(tight coupling) (상속, 조합도 여기에 해당)
    • 어떤 경우는 느슨하게 연결되어 있다 → 느슨한 결합(loose coupling)

처음에는 아무 문제가 없어 보인다. 하지만 프로젝트가 커지기 시작하면, 강하게 연결된 객체들의 구조가 발목을 잡기 시작한다.
 

4.2. 객체가 사는 세상은 "사회"에 가깝습니다.

객체는 항상 어떤 환경 속에서 맡은 임무를 수행한다. 이 환경은 아주 사회적이고, 유대감이 있으며, 협력이 필수적인 공간이다.
그래서 우리는 자연스럽게 "이 객체는 누구와 협력하고 있고, 그 협력을 어떻게 설계할 수 있을까?" 하는 질문을 던지게 된다. 
 
객체들이 서로 협력한다는 사실 자체는 피할 수 없다. 문제는 “얼마나 강하게 결합되어 있는가"인데, 결합이 강해질수록:

  • 테스트가 어려워지고
  • 리팩토링이 힘들어지고
  • 변경이 도미노처럼 퍼진다

그래서 우리는 최대한 객체들을 분리(decouple)하려고 노력한다.
하지만... 협력을 끊을 수는 없으니까, 이 사이에 완충지대가 필요하다.
 

4.3. 그 완충지대가 바로 인터페이스

인터페이스란, 객체가 다른 객체와 협력하기 위해 따라야 하는 ‘계약서’다.

 
객체는 인터페이스만 알면 된다. 상대가 어떤 방식으로 구현되었는지는 알 필요가 없다.

// 1. 인터페이스 정의: 계약 선언
interface Cash {
  multiply(factor: number): Cash;
}

위 코드는 “어떤 Cash 타입이든 multiply(factor: number): Cash 메서드를 반드시 가져야 한다”는 계약서이다. 이걸 따르지 않으면 TypeScript가 컴파일 에러를 발생시킨다. 

// 2. 계약을 따르는 실제 구현체
class DefaultCash implements Cash {
  private dollars: number;

  constructor(dollars: number) {
    this.dollars = dollars;
  }

  multiply(factor: number): Cash {
    return new DefaultCash(this.dollars * factor);
  }
}

 
여기서 DefaultCash는 Cash 인터페이스를 implements함으로써 계약을 정식으로 따르겠다고 선언한다.
즉, "나를 Cash 타입으로 취급해도 된다. 왜냐하면 약속된 규칙(multiply 함수)을 다 갖추었기 때문이다."라고 말하는 것과 같다. 

// 3. 사용하는 측에서는 Cash 인터페이스만 알면 됨
class Employee {
  private salary: Cash;

  constructor(salary: Cash) {
    this.salary = salary;
  }

  doubleSalary(): Cash {
    return this.salary.multiply(2);
  }
}
  • Employee 클래스는 DefaultCash라는 구체적인 구현을 몰라도 된다(느슨한 결합).
  • 오직 "multiply 메서드가 있는 Cash 타입만 있으면 돼" 라는 계약만 보고 신뢰하는 것이다. 

4.4. 여러 계약서

참고로 TypeScript는 다중 인터페이스를 지원한다. 클래스는 여러 역할을 동시에 수행할 수 있다는 뜻이다. 

interface Shape { getArea(): number; }
interface Printable { print(): void; }

class PrintableCircle implements Shape, Printable {
  constructor(private radius: number) {}

  getArea(): number {
    return Math.PI * this.radius * this.radius;
  }

  print(): void {
    console.log(`Area: ${this.getArea()}`);
  }
}

 

4.5. 정리

Q. 퍼블릭 메서드는 인터페이스 없이 존재하면 왜 문제가 될까?
[정보 은닉 원칙 위반]
퍼블릭 메서드는 외부 객체와의 접점이다. 그런데 아무 계약 없이 마구 만들어지면 의도되지 않은 사용이 가능해져 코드가 망가질 수 있다
 
[유지보수성 악화]
어떤 메서드를 써도 되는지, 어떤 건 내부 용도인지 구분이 되지 않아 팀원 간 협업과 유지보수가 어려워진다.
 
[사용자 오용 가능성]
외부 사용자가 내부 로직에 접근하게 되면, 내부 구현에 의존하는 취약한 구조가 된다.
 
[일관성 상실]
인터페이스 기반 설계가 아니라면, 각 객체의 사용법이 제각각이 되어 설계 통일성이 무너진다.


5. 이외에도 많지만.. 일단 마치며

아직 못다룬 내용이 많다. 
 
Q. 상태를 바꾸지 않고 새로운 객체를 리턴하는 식으로 불변 객체를 고집한다면, 메모리와 성능 면에서 너무 비효율적인 건 아닐까? 객체가 계속 늘어나는 게 진짜 좋은 설계일까?
: 개발자인 나 입장에서는 어떤게 유지보수하기가 더 쉬운지 고민해볼 필요가 있음
 
Q. 왜 UI 스레드는 단일 스레드인가? 왜 멀티 스레드가 아닐까? 멀티스레드면 더 빠를텐데, 왜 오히려 그걸 제한하고 있을까?
: 서로 다른 스레드가 동시에 화면을 바꾸려는 경쟁 발생, 사용자 인터랙션 중간에 UI가 꼬이는 현상 발생, ... 이러한 동기화 문제를 매번 락, 큐, 트랜잭션으로 해결해야 하며 프론트엔드 개발자가 UI 논리보다 동기화 문제로 더 많은 비용을 치르게 됨. UI는 '일관성'이 더 중요하다. 멀티스레드는 이런 일관성을 깨트릴 위험이 너무 큼. 
+ React에서 virtual dom으로 비교하는 것 자체도 비용이다. 
 
Q. 선언형으로 써봤자, 결국엔 안쪽 어딘가에선 명령형 코드가 실행되고 있을 텐데… 이걸 ‘선언형으로 감쌌다’고 보는 게 의미 있는가?
: 결국은 어떻게 디자인할건지가 중요하다. 
 
Q. OOP, FP… 언뜻 보면 정반대처럼 보이지만, 공부할수록 결국 같은 문제를 다른 방식으로 푸는 것 같다는 생각이 든다. 언어는 달라도 목적은 닮아 있다.
 
의문이 풀리지 않은 질문도 많다. 
 
Q. getter와 setter의 기준은 뭘까? 
Q. 객체를 정말 ‘협력하는 주체’로 다루려면, “시간”과 “이벤트”를 어떻게 객체로 모델링해야 할까?
Q. 프론트엔드에서 유저 인터랙션은 “명령”인가, “메시지”인가?
 
하나씩 정리되면 차근차근 올려야겠다