Trait Views: 상속 없이 JavaScript에서 동작 노출하기

발행: (2026년 1월 15일 오전 03:06 GMT+9)
17 min read
원문: Dev.to

Source: Dev.to

위에 제공된 소스 링크 외에 번역할 텍스트를 알려주시면, 해당 내용을 한국어로 번역해 드리겠습니다.

Introduction

JavaScript는 행동을 공유하는 다양한 방법을 제공합니다: 상속, 믹스인, 컴포지션, 인터페이스(TypeScript를 통한).

하지만 규모가 큰 시스템에서는 이들 모두가 어느 정도 한계에 부딪히게 됩니다.

  • Inheritance는 경직됩니다.
  • Mixins는 상태가 새어나가는 경향이 있습니다.
  • 원시 객체를 전달하면 노출되는 표면 영역이 너무 넓어집니다.

JavaScript에 부족한 것은 다음과 같은 방식을 말할 수 있는 방법입니다:

“이 객체에 대한 를 제공하고, 특정 기능을 노출하며, 기본 동작을 포함하되—객체 자체는 변경하지 않음.”

이 글에서는 Trait Views를 소개합니다: Rust 트레이트에서 영감을 받아 JavaScript 객체 모델에 맞게 조정한 런타임 패턴.

이는 언어 제안이 아니며 기존 패턴을 대체하려는 것도 아닙니다. 누락된 추상화를 탐구하는 시도입니다.

문제

간단한 상황을 생각해 보세요: 객체가 하나 있는데, 이를 observable(관찰 가능)하게 취급하고 싶습니다.

  • Observable 기본 클래스를 상속받는 방식이 아닙니다.
  • 메서드를 섞어 넣는 방식이 아닙니다.
  • 객체 자체를 전달하고 모두가 “제대로” 사용하도록 믿는 방식도 아닙니다.

다음과 같이 말하고 싶습니다:

“시스템의 이 부분에서는 이 객체를 오직 observable 로만 봐야 합니다.”

JavaScript는 이를 표현할 수 있는 기본적인 방법을 제공하지 않습니다.

Trait Views — 아이디어

Trait View는 다른 객체의 특정 동작을 노출하는 파생 객체이다. 그것은:

  • 원본 객체가 아니다
  • 그 복사본도 아니다
  • 그에 적용된 믹스인도 아니다

이는 프로젝션이다.

객체에 trait추가하는 것이 아니다.
그 객체의 view를 파생한다.

최소 예시: Observable

class Observable {
  static from = trait(Observable)

  observe(): number {
    return 0
  }

  // Note this one is optional
  observeTwice?(): number {
    return this.observe() * 2
  }
}

이 트레이트는 다음을 정의합니다:

  • 하나의 핵심 동작: observe
  • 그 위에 구축된 하나의 기본 동작: observeTwice

이제 완전히 무관한 객체가 있습니다:

class Sensor implements Observable {
  value = 21

  observe(): number {
    return this.value
  }
}

implements Observable 절은 중요합니다. Observable이 절대로 확장되지 않더라도, TypeScript는 다음을 강제합니다:

  • 모든 필수 메서드가 존재함
  • 메서드 시그니처가 호환됨
  • 리팩터링이 타입‑안전하게 유지됨

이는 Trait Views가 “어두운 곳에서의 덕 타이핑”이 아니라 구조적으로 타입이 지정되고 컴파일 시점에 검사된다는 의미입니다.

const sensor = new Sensor()

Observable.from(sensor).observeTwice() // 42

여기서 일어난 일은 미묘하지만 중요합니다.

  • Sensor는 그대로 유지됩니다.
  • 메서드가 복사되지 않았습니다.
  • 상속이 도입되지 않았습니다.

대신 Observable.from(sensor)강하게 타입이 지정된 뷰를 생성하여 sensor에 관찰 가능한 동작을 노출합니다. 여기에는 원래 객체에는 없던 기본 로직도 포함됩니다. 원본 객체는 관찰 가능하지 않으며, 가 관찰 가능하게 됩니다.

Stateless vs Stateful Trait Views

Trait Views는 두 가지 모드로 존재할 수 있습니다.

Stateless views Observable.from(...) → Stateless

무상태(stateless) 트레이트 뷰:

  • 상태를 소유하지 않음
  • 자체 생성자는 의도적으로 사용되지 않으며 호출되지 않음
  • 인스턴스당 캐시되며 안정성을 위해 고정됨
  • 모든 동작을 원본 객체에 위임함

TypeScript 관점에서:

  • 모든 트레이트 메서드가 존재함이 보장됩니다
  • 트레이트가 소유한 속성은 옵셔널 상태를 유지합니다

무상태 모드에서는 트레이트가 소유한 속성이 의도적으로 옵셔널로 타입 지정됩니다. 이는 구현 객체가 동일한 이름의 getter를 제공할 경우, 트레이트 상태를 구체화하는 것이 잘못될 수 있음을 대비한 것입니다.

개념적으로 이는 Rust의 빌린 트레이트 객체(&dyn Trait)와 가깝습니다: 기존 상태 위에 안정적인 인터페이스를 제공합니다.

Stateful views Observable.from(...) → Stateful

유상태(stateful) 트레이트 뷰:

  • 자신만의 내부 상태를 소유
  • 자체 생성자와 매개변수를 가짐
  • Observable.from(object, param1, param2, …)를 통해 명시적으로 생성됨
  • 캐시되지 않음

타이핑 관점에서:

  • 모든 트레이트 메서드가 존재함
  • 모든 트레이트 속성이 존재함이 보장됨

이를 통해 트레이트가 원본 객체를 오염시키지 않으면서 상태를 보유할 수 있습니다.

두 모드가 존재하는 이유는 서로 다른 문제를 해결하기 위해서입니다. 옵션을 사용하여 특정 트레이트에 대해 모드를 선택할 수 있습니다:

class MyTrait {
  static from = trait(MyTrait, { stateful: true });

  public myState?: number = 42;

  constructor(myParameter: number) {}
}

트레이트 뷰는 능력 경계로서

지금까지 트레이트 뷰는 행동을 공유하는 방법처럼 보였습니다.
하지만 트레이트 뷰는 표면 영역을 줄여줍니다.

class Disposable {
  static from = trait(Disposable)

  dispose(): void {
    console.log("default dispose")
  }
}
class Resource implements Disposable {
  secret = "do not touch"
  disposed = false

  dispose() {
    this.disposed = true
  }

  dangerousOperation() {
    console.log(this.secret)
  }
}
const resource = new Resource()
const disposable = Disposable.from(resource)

disposable.dispose()            // OK
disposable.dangerousOperation() // ❌ 접근 불가

트레이트 뷰는 오직 하나의 능력만 노출합니다: 폐기(disposal).

원본 객체는 많은 메서드와 상태, 불변 조건을 가질 수 있지만, 뷰는 의도적으로 보이는 것을 제한합니다. 객체 자체를 전달하는 대신 그 객체가 할 수 있는 일만을 전달합니다.

트레이트 뷰는 단순히 재사용을 위한 것이 아니라 투영에 의한 캡슐화를 위한 것입니다.

Source:

작동 방식 (개념적으로)

높은 수준에서 trait view는:

  • 메서드 조회를 위해 trait 프로토타입을 사용합니다.
  • 호출을 기본 대상 객체(상태가 없는 경우) 또는 자체 내부 상태(상태가 있는 경우)로 전달합니다.
  • trait(...)에 의해 생성된 정적 from 헬퍼를 통해 생성됩니다.
  • 상태가 없는 경우 대상당 캐시될 수 있어, 동일성 보장을 합니다.

(구현 세부 사항은 생략했으며, 관찰 가능한 API와 타입‑레벨 보장에 초점을 맞추었습니다.)

기본 동작

  • 재정의된 메서드를 원본 객체에 바인딩합니다.
  • 선택적으로 접근자를 바인딩합니다.
  • 상태가 없는 view를 캐시합니다(가비지 컬렉션을 방해하지 않도록 약하게).
  • 안정성을 위해 상태가 없는 view를 고정합니다.

원본 객체는 절대 변경되지 않습니다.
trait view는 명확히 정의된 인터페이스를 가진 별도의 객체입니다.

이것이 Rust 트레이트와 비교했을 때

Trait Views는 Rust 트레이트에서 영감을 받았지만, 동일하지는 않습니다.

유사점

  • 행동 지향 추상화
  • 기본 메서드
  • 안정적인 인터페이스를 통한 동적 디스패치

차이점

  • 해결이 컴파일 타임이 아니라 런타임에 이루어집니다
  • 일관성(coherence)이나 고아(orphan) 규칙이 없습니다
  • 오버라이드는 이름 기반입니다
  • TypeScript는 모든 보장을 표현할 수 없습니다

이것은 패턴의 결함이 아니라 JavaScript의 동적 특성에 기인한 결과입니다.

Trait Views는 유사한 사용성을 목표로 하며, 동일한 의미론을 추구하지는 않습니다.

왜 함수만 바인딩하지 않을까?

언뜻 보면 Trait View는 사소해 보일 수 있습니다. 결국 새 객체를 만들고 몇몇 메서드를 수동으로 바인딩하면 되니까요.

차이는 런타임에 일어나는 일이 아니라 무엇을 모델링하고 있는가에 있습니다.

Trait View가 제공하는 것:

  • 일관된 추상화
  • 객체 상태와 트레이트 행동 사이의 명확한 경계
  • 안정적인 타입 표면
  • 선택적인 캐싱 및 동결 보장
  • 코드베이스 전체에 공유되는 정신 모델

함수를 수동으로 바인딩하는 것은 국부적인 문제를 해결합니다.
Trait View는 시스템적인 문제를 해결하는 것을 목표로 합니다.

편리함보다는 의도를 명확히 표현하는 데 더 중점을 둡니다.

기존 라이브러리와의 비교

여러 라이브러리가 이미 JavaScript와 TypeScript에서 트레이트를 탐구하고 있습니다. Trait Views는 이를 대체하기 위한 것이 아니라 다른 초점을 목표로 합니다.

@traits-ts/core

  • 클래스에 여러 기본 기능을 확장할 수 있는 트레이트(또는 믹스인) 기능을 제공합니다.
  • TypeScript의 타입 시스템과 일반 class extends 메커니즘을 활용합니다.
  • 트레이트 동작을 새로운 클래스 계층으로 결합하며 컴파일 시 타입 안전성을 보장합니다.

사용 사례: 트레이트가 타입 정의 시점에 알려지고 적용되는 정적 컴포지션.

Trait Views는 정적 컴포지션이 아니라 기존 객체에 대한 런타임 적응인스턴스별 뷰에 초점을 맞춥니다.

traits.js

  • 트레이트 컴포지션을 위한 전용 라이브러리로, 고전적인 재사용 가능한 행동 단위를 기반으로 합니다.
  • 하나 이상의 트레이트를 단일 복합 트레이트로 구성하고, 결합된 행동을 가진 객체를 생성할 수 있습니다.

사용 사례: 생성 시점에 결합된 행동을 가진 새로운 객체를 만드는 경우.

Trait Views는 반대 관점을 취합니다: 트레이트에서 객체를 파생시켜 기존 객체에 대한 뷰를 변형 없이 생성합니다.

트레이드오프 및 제한 사항

  • Trait Views는 런타임 리플렉션에 의존합니다.
  • getter가 부작용이 없다고 가정합니다.
  • 모든 형태의 몽키패치를 방지할 수 없습니다.
  • API 설계에 있어 규율이 필요합니다.

이 패턴은 모든 상황에 적합한 것은 아닙니다. Trait Views는 특히 다음에 잘 맞습니다:

  • 엔진 및 시뮬레이션
  • ECS 스타일 아키텍처
  • 표면 제어가 중요한 능력 기반 API 또는 시스템

이들은 단순 CRUD 애플리케이션이나 UI 중심 코드베이스에는 적합하지 않습니다.

결론 — 그리고 열린 질문

Trait Views는 새로운 언어 기능이 아닙니다. 이것은 패턴이며 — JavaScript에서 동작을 다르게 생각하는 방법입니다.

이들은 다음 사이 어딘가에 위치합니다:

  • Rust 스타일 트레이트
  • 역량 기반 설계
  • 런타임 객체 뷰

현재 단계에서 Trait Views는 실험입니다. 아직 공개 라이브러리는 없으며 — 아이디어와 구현, 그리고 일련의 트레이드오프만 존재합니다.

이것이 여러분에게 공감된다면

  • 사용자로서
  • 라이브러리 작성자로서
  • 언어 설계에 관심이 있는 사람으로서

그럼 피드백이 중요합니다.

아마도 이미 다른 방식으로 유사한 문제를 해결했을 수 있으며, 그 접근 방식을 공유하고 싶을 수도 있습니다.

  • 이것을 패턴으로 유지해야 할까요?
  • 작은 실험적 라이브러리로 만들어야 할까요?
  • 아니면 특수 시스템을 위한 내부 도구로 남겨야 할까요?

저는 커뮤니티가 어떻게 생각하는지 진심으로 듣고 싶습니다.

Back to Blog

관련 글

더 보기 »

JavaScript의 비밀스러운 삶: 청사진

ES6 클래스는 프로토타입에 대한 단순한 “syntactic sugar”에 불과합니다. 티모시가 칠판 앞에 서서 자신의 작품을 감탄했습니다. 그는 완벽한 직사각형 상자를 그렸습니다. > “마침내,”...