
자바스크립트에서 obj[key]는 가장 기본적인 문법이다. 우리는 이를 단순한 프로퍼티 조회로 받아들인다. 특정 key를 넣으면 그에 해당하는 값을 반환하는, 가장 직관적인 접근 방식이다. 그러나 Proxy를 사용하면 이 동작을 가로채고 전혀 다른 의미로 재정의할 수 있다. 문법은 그대로 두되, 그 해석을 바꾸는 것이다.
이 글의 목적은 브라켓 문법을 활용해 객체를 “조건 기반으로 조회”하는 DSL(Domain Specific Language)을 설계하는 것이다. 우리가 만들고자 하는 형태는 다음과 같다.
users
[WHERE](u => u.age >= 20)
[VALUES]()이 표현은 단순한 프로퍼티 접근이 아니다. 객체 내부를 순회하며 조건을 만족하는 값만 추려내고, 마지막에 이를 배열로 변환한다. 겉으로는 obj[key]이지만, 실제로는 작은 쿼리 문법처럼 동작한다.
이 설계를 성립시키기 위해서는 한 가지 전제가 필요하다. 브라켓 문법을 유지하면서도, 우리가 정의한 “메타 명령”을 손실 없이 전달할 수 있어야 한다는 점이다. 문제는 브라켓 안에 들어간 값이 내부적으로 ToPropertyKey 과정을 거친다는 것이다. 이 과정에서 객체는 문자열로 변환된다. 따라서 일반 객체나 함수는 Proxy의 get 트랩까지 원형 그대로 도달할 수 없다.
여기서 중요한 단서가 나온다. ToPropertyKey 과정에서도 변환되지 않고 그대로 유지되는 타입이 있다. 바로 Symbol이다. Symbol은 문자열로 강제 변환되지 않으며, Proxy의 get 트랩에서 정확히 구분할 수 있다. 이 특성을 이용하면 브라켓 문법을 유지한 채, DSL 명령을 안전하게 전달할 수 있다.
이제 DSL에서 사용할 메타 키와 객체를 감싸는 createQueryable 함수를 정의한다. 우선 메타 키의 모습은 아래와 같다.
const WHERE = Symbol('where');
const VALUES = Symbol('values');이 Symbol들은 실제 데이터에 대응하는 키가 아니다. 객체의 속성을 가리키기 위한 식별자가 아니라, DSL에서 특정 연산을 의미하는 명령어다. 다시 말해, 값에 접근하기 위한 key가 아니라 동작을 트리거하기 위한 신호에 가깝다.
function createQueryable(obj) {
return new Proxy(obj, {
get(target, prop, receiver) {
if (prop === WHERE) {
return (predicate) => {
const filtered = Object.entries(target)
.filter(([key, value]) => predicate(value, key))
.reduce((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {});
return createQueryable(filtered);
};
}
if (prop === VALUES) {
return () => Object.values(target);
}
return Reflect.get(target, prop, receiver);
}
});
}이 구조의 동작은 명확하다. WHERE가 들어오면 predicate를 인자로 받는 함수를 반환한다. 그 함수는 객체를 순회하면서 조건을 만족하는 항목만 남긴 새로운 객체를 생성하고, 그 결과를 다시 createQueryable로 감싼다. 이렇게 반환된 값은 다시 Queryable이 되므로 연산을 이어갈 수 있다. VALUES가 들어오면 현재 상태의 값들을 배열로 반환한다. 이 시점에서 쿼리 체인은 종료되고, 최종 결과가 배열 형태로 확정된다.
그 외의 문자열 키나 일반적인 프로퍼티 접근은 Reflect.get을 통해 그대로 위임된다. 즉, DSL 명령을 제외하면 기존 객체와 동일하게 동작한다.
이제 이 구조를 실제 객체에 적용해보면 다음과 같은 형태가 된다.
const users = createQueryable({
a: { age: 10, name: "A" },
b: { age: 20, name: "B" },
c: { age: 30, name: "C" }
});
const result =
users
[WHERE](u => u.age >= 20)
[VALUES]();
console.log(result); // [
// { age: 20, name: "B" },
// { age: 30, name: "C" }
// ]문법은 obj[key]지만, 이 코드에서 그 의미는 단순한 프로퍼티 조회가 아니다. 실제로 수행되는 것은 조건 기반 필터 연산이다. 이 지점이 Proxy 기반 메타프로그래밍의 핵심이다. WHERE가 반환하는 값은 일반 객체가 아니라 다시 Proxy로 감싼 Queryable이며, 그 덕분에 연산을 자연스럽게 이어갈 수 있다.
각 단계는 이전 상태를 기반으로 새로운 객체를 만들고, 그 결과를 다시 Queryable로 감싼다. 이 흐름이 반복되면서 구조는 점차 축소되고, 전체 코드는 작은 쿼리 엔진처럼 동작한다.
const result =
users
[WHERE](u => u.age >= 10)
[WHERE](u => u.age < 30)
[VALUES]();이 패턴은 세 가지 개념이 결합된 결과다. 첫째는 Proxy를 이용해 연산의 의미를 동적으로 재정의한 것이다. 둘째는 Symbol을 통해 메타 명령을 안전하게 전달한 설계다. 셋째는 브라켓 문법을 새로운 관점에서 해석해 DSL로 확장한 발상이다. 중요한 점은 문법 자체를 바꾸지 않았다는 것이다. 자바스크립트가 허용하는 범위 안에서, 단지 해석 방식만 재정의했을 뿐이다.
물론 이 방식에는 비용이 따른다. 모든 접근이 Proxy를 거치기 때문에 런타임 오버헤드가 발생한다. 내부 동작이 감춰져 있어 디버깅 난이도도 올라간다. 타입스크립트 환경에서는 원하는 수준의 타입 안전성을 확보하려면 추가적인 제네릭 설계가 필요하다. 이러한 이유로 이 패턴은 일반적인 비즈니스 로직보다는 내부 DSL이나 라이브러리 수준의 추상화에 더 적합하다.
그럼에도 이 실험은 의미가 있다. 자바스크립트의 브라켓 문법은 단순한 프로퍼티 접근처럼 보이지만, Proxy를 통해 그 의미를 재정의할 수 있다. 다만 스펙의 제약을 정확히 이해해야 한다. 객체는 브라켓을 통해 그대로 전달되지 않으며, Symbol만이 안전한 메타 채널이라는 점이 설계의 핵심이다.
결국 이 코드는 작은 장난감 예제처럼 보일 수 있다. 그러나 런타임 메타프로그래밍이 어디까지 확장될 수 있는지를 보여주는 사례이기도 하다. 문법은 고정되어 있지만, 그 해석은 바꿀 수 있다. 그리고 바로 그 지점에서 DSL이 만들어진다.
더 읽어보기
2026.04.11
Trie 자료구조
문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다. Trie는 무…
2026.03.19
Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까
느린 네트워크에서 큰 이미지를 열면 가끔 화면이 위에서 아래로 채워진다. 마치 누군가 아주 성실하게 롤러로 이미지를 칠하는 것처럼 보인다. 물론 브라우저 안에 그런 직원은 없다. 있다면 우리보다 야근을 더 많이 하고 있을 것이다. 이 현상은 단순한 시각 효과가 아니라 네트워크 전송, 브…
2026.03.19
Streams API 부록 1. HTTP 다운로드 진행률은 어떻게 계산될까
다운로드 화면에서 진행 막대가 조금씩 차오르면 이상하게 안심된다. 반대로 스피너만 계속 돌면 파일이 오는 중인지, 서버가 고민 중인지, 내 인생이 잠깐 멈춘 건지 알 수 없다. 사용자는 둘 다 “다운로드 중”이라고 느끼지만, 내부적으로는 꽤 다른 상황일 수 있다. 진행률 계산 자체는 복…
2026.03.13
Streams API 4. 왜 모든 언어에는 Stream API가 존재할까
Streams API를 공부하다 보면 묘한 기시감이 온다. JavaScript에서 ReadableStream, WritableStream, TransformStream을 보고 있는데, Java의 InputStream, Go의 io.Reader, Rust의 Read와 Write가 멀리서…
2026.06.01
React Server Components를 위한 컴포넌트 아키텍처
이 포스트는 Vercel의 Next.js 팀 소속 개발자 Aurora Scharff가 자신의 블로그에 올린 Component Architecture for React Server Components 게시글을 번역한 것이다. 번역하는 과정에서 다소 의역이 있을 수 있으며, 일부 번역에는…
2026.05.26
차트는 멈췄는데 윈도우가 움직인다
상황 어느날 서비스를 살펴보시던 팀장님께서 이런 말씀을 slack에 남기셨다. 진호님, 예측 차트에서 zoom을 계속하면 어느 순간 라인 차트가 아니라 단일 스캐터 차트처럼 보이는 데, 이거 수정하면 좋을 거 같아요. 어느정도 zoom을 하면 그 이후로는 zoom이 안 되도록 할 수 없…
2026.05.21
피자가게로 이해하는 디자인 패턴
에이든 피자는 처음부터 복잡한 시스템을 만들 생각이 없었다. 처음에는 메뉴 몇 개만 만들면 됐다. 그런데 손님은 커스텀 주문을 넣기 시작했고, 주방은 상태를 나눠야 했고, 결제와 배달앱과 알림이 하나씩 붙었다. 코드도 가게를 닮는다. 장사가 잘될수록 이상하게 더 쉽게 망가진다. 디자인…
2026.05.21
7. Decorator — 토핑 추가할 때마다 클래스를 새로 만들 수 없다
에이든 피자에서 주문서를 객체로 만들자 취소와 재주문은 한결 편해졌다. 그런데 주문이 편해지자 손님들도 한결 편해졌다. 편해진 손님은 더 많은 요구를 한다. "치즈 추가요", "올리브도 추가요", "소스 많이요", "조금 더 바삭하게 구워주세요" 같은 요청이 주문대 위로 쌓이기 시작했다…
댓글
댓글을 불러오는 중...