낯선 단어, 익숙한 행동 — 우리는 이미 polyfill과 ponyfill을 쓰고 있다
polyfill과 ponyfill이라는 단어는 처음 들으면 다소 낯설게 느껴진다. 이름만 보면 마치 특정 라이브러리나 패턴의 한 종류처럼 보이기도 하고, 어떤 사람에게는 그저 커뮤니티에서 만들어낸 비공식 용어처럼 느껴질 수도 있다. 그러나 이 두 개념은 생각보다 훨씬 더 깊숙하게 JavaScript 개발자의 일상에 들어와 있다. 우리는 의식하지 못할 뿐, 이미 매일같이 이 개념을 사용하고 있다. 단지 그것을 이름으로 인식하지 않을 뿐이다.
개발자는 보통 새로운 프로젝트를 시작할 때, 혹은 기존 프로젝트에 기능을 추가할 때 자연스럽게 패키지를 설치한다. 이 과정에서 우리는 특정 기능을 직접 구현하기보다, 이미 누군가 만들어 놓은 코드를 가져다 사용하는 것을 당연하게 받아들인다. 문제는 이 패키지들이 단순한 기능 구현 코드만을 포함하고 있지 않다는 점이다. 그 안에는 특정 환경에서 동작하지 않을 가능성을 고려한 코드, 예상치 못한 실행 조건을 방어하기 위한 코드, 그리고 다양한 런타임 차이를 흡수하기 위한 코드가 함께 들어 있다. 다시 말해, 우리는 단순히 기능을 사용하는 것이 아니라, 환경을 보정하는 코드까지 함께 소비하고 있다.
이 지점에서 중요한 사실이 드러난다. 대부분의 개발자는 자신이 사용하는 코드가 어떤 환경을 전제로 만들어졌는지, 그리고 어떤 문제를 해결하기 위해 포함되었는지를 깊이 들여다보지 않는다. 코드는 잘 동작하고, 에러가 발생하지 않으며, 기능이 정상적으로 구현된다면 그것으로 충분하다고 판단한다. 그러나 이 “잘 동작한다”는 결과 뒤에는 수많은 전제와 가정이 숨어 있다. polyfill과 ponyfill은 바로 그 전제를 구성하는 코드들이다. 이들은 단순한 유틸리티가 아니라, 환경의 불확실성을 감추기 위한 장치다.
결국 우리는 하나의 질문에 도달하게 된다. 왜 JavaScript에서는 이런 코드가 필요하게 되었을까. 왜 다른 언어에서는 비교적 덜 보이는 이런 개념들이, JavaScript에서는 이렇게까지 자연스럽게 자리 잡게 되었을까. 이 질문에 답하기 위해서는 현재를 기준으로 판단하는 것이 아니라, JavaScript가 처음 등장했던 환경을 다시 살펴볼 필요가 있다. 이 글은 그 지점에서부터 출발한다. 우리가 지금 당연하게 사용하는 이 코드들이, 어떤 문제를 해결하기 위해 만들어졌는지를 이해하는 것에서 시작해야 한다.
왜 이런 코드가 필요했는가 — JavaScript는 처음부터 불완전한 환경이었다
지금의 JavaScript 환경은 상당히 안정적이다. 대부분의 브라우저는 표준을 잘 따르고 있으며, Node.js 역시 명확한 LTS 정책 아래에서 일관된 런타임을 제공한다. 개발자는 특정 기능이 대부분의 환경에서 동일하게 동작할 것이라는 기대를 어느 정도 가질 수 있다. 그러나 이 상태는 처음부터 주어진 것이 아니다. 오히려 JavaScript의 초기 환경은 그 반대에 가까웠다. 동일한 코드가 동일하게 동작한다는 보장이 거의 존재하지 않는 세계였다.
초기의 웹 환경에서는 브라우저마다 JavaScript 엔진의 구현이 달랐고, 표준 역시 느리게 발전했다. 어떤 브라우저에서는 존재하는 기능이 다른 브라우저에서는 아예 없거나, 동일한 API라도 동작 방식이 미묘하게 다른 경우가 흔했다. 배열 메서드나 객체 조작 함수처럼 지금은 당연하게 사용하는 기능들도 당시에는 지원되지 않는 경우가 많았다. 개발자는 특정 기능을 사용하기 위해 매번 환경을 확인해야 했고, 동일한 로직을 여러 방식으로 구현해야 하는 상황에 놓였다. 이 과정에서 “표준을 믿는다”는 개념 자체가 성립하기 어려웠다.
이러한 환경에서는 두 가지 선택지가 존재했다. 하나는 모든 기능을 직접 구현하는 것이고, 다른 하나는 특정 기능을 재사용 가능한 형태로 분리하여 공유하는 것이었다. 후자가 더 효율적인 선택이었고, 자연스럽게 작은 단위의 유틸리티 코드들이 등장하게 되었다. 이 코드들은 단순히 기능을 제공하는 것이 아니라, 특정 환경에서도 동일하게 동작하도록 보정하는 역할을 함께 수행했다. 즉, 코드 자체가 기능과 환경 대응을 동시에 담당하게 된 것이다.
이 지점에서 중요한 변화가 발생한다. 개발자는 더 이상 환경을 신뢰하지 않고, 코드 내부에서 환경을 처리하기 시작한다. 특정 기능이 존재하는지 확인하고, 존재하지 않을 경우 대체 구현을 실행하는 방식이 일반화된다. 이러한 패턴이 반복되면서, 환경 차이를 흡수하는 코드가 하나의 구조로 자리 잡게 된다. 그리고 바로 이 구조 위에서 polyfill이라는 개념이 등장하게 된다. polyfill은 단순한 도구가 아니라, 환경을 신뢰할 수 없었던 시대가 만들어낸 가장 직접적인 대응 방식이었다.
첫 번째 대응 — polyfill, 환경을 수정하는 가장 직접적인 방식
polyfill은 JavaScript 생태계에서 가장 직관적인 해결 방식이다. 특정 기능이 존재하지 않는 환경에서, 그 기능을 직접 구현하여 “있는 것처럼” 만들어 주는 코드다. 이 접근 방식의 핵심은 단순하다. 코드를 바꾸는 것이 아니라, 환경을 바꾼다. 개발자는 표준 API를 그대로 사용할 수 있고, 기존 코드의 형태를 유지할 수 있으며, 환경 차이를 의식하지 않아도 된다. 이는 당시의 상황에서 매우 강력한 장점이었다.
예를 들어, 어떤 브라우저에서 Array.prototype.includes가 지원되지 않는다면, polyfill은 해당 메서드를 직접 정의하여 모든 코드가 동일한 방식으로 동작하도록 만든다. 개발자는 더 이상 조건문을 통해 환경을 분기할 필요가 없고, 동일한 코드를 모든 환경에서 사용할 수 있다. 이 방식은 개발 생산성을 크게 향상시켰고, 코드의 가독성 역시 유지할 수 있었다. 즉, polyfill은 단순한 편의 기능이 아니라, 코드의 일관성을 유지하기 위한 전략적 선택이었다.

그러나 이 접근 방식에는 중요한 전제가 존재한다. polyfill은 환경을 “수정”한다는 점에서, 실행 환경 자체에 영향을 미친다. 이는 단순히 기능을 추가하는 것을 넘어, 기존 환경의 동작 방식을 변경할 수 있다는 의미다. 특정 객체의 프로토타입을 수정하거나, 전역 객체에 새로운 속성을 추가하는 과정에서 예상치 못한 충돌이 발생할 수 있다. 또한 여러 라이브러리가 동일한 기능을 서로 다른 방식으로 polyfill할 경우, 그 결과는 예측하기 어려워진다.
그럼에도 불구하고 polyfill은 오랜 시간 동안 JavaScript 생태계의 핵심 도구로 자리 잡았다. 그 이유는 명확하다. 당시의 환경에서는 이것이 가장 현실적인 해결책이었기 때문이다. 환경을 통제할 수 없는 상황에서, 코드를 수정하는 것보다 환경을 보정하는 것이 훨씬 효율적이었다. 그러나 이 선택은 또 다른 문제를 남긴다. 환경을 수정하는 방식이 가진 구조적 한계는 시간이 지날수록 점점 더 명확해지기 시작한다. 그리고 이 한계는 결국 새로운 접근 방식을 필요로 하게 만든다.
편리함의 대가 — polyfill이 만들어낸 구조적 문제
polyfill은 분명히 강력한 해결 방식이었다. 환경을 수정함으로써 코드의 일관성을 유지하고, 개발자가 환경 차이를 신경 쓰지 않아도 되게 만들었다. 그러나 이 편리함은 결코 공짜가 아니었다. polyfill은 문제를 “숨기는” 방식으로 해결했기 때문에, 그 대가는 점점 더 복잡한 형태로 드러나기 시작했다. 처음에는 단순한 보완처럼 보였던 코드가, 시간이 지나면서 환경 자체의 예측 가능성을 무너뜨리는 요소로 변해 갔다.
가장 대표적인 문제는 global namespace 오염이다. polyfill은 전역 객체나 내장 프로토타입을 수정하는 방식으로 동작한다. 이는 특정 기능을 추가하는 데는 효과적이지만, 동시에 기존 환경의 상태를 변경한다는 의미이기도 하다. 문제는 이 변경이 항상 단일한 방식으로 이루어지지 않는다는 점이다. 서로 다른 라이브러리가 동일한 기능을 서로 다른 방식으로 polyfill할 경우, 실행 환경은 더 이상 일관된 상태를 유지하지 못한다. 개발자는 코드만 보고는 실제 환경이 어떤 상태인지 파악하기 어려워지고, 이는 디버깅 난이도를 급격히 증가시킨다.
또 다른 문제는 충돌과 우선순위의 문제다. polyfill은 “없는 기능을 채운다”는 전제를 가지고 있지만, 실제로는 이미 존재하는 기능을 덮어쓰거나 확장하는 경우도 발생한다. 이 과정에서 기존 동작과 polyfill의 구현이 미묘하게 다른 경우, 예상치 못한 버그가 발생할 수 있다. 특히 브라우저가 점점 더 많은 표준을 지원하게 되면서, polyfill과 native 구현이 공존하는 상황이 만들어지기 시작했다. 이때 어떤 구현이 실제로 사용되는지는 코드만으로는 명확하게 드러나지 않는다. 결과적으로 우리는 코드가 아니라 환경 상태에 의존하는 시스템을 만들게 된다.

이러한 문제는 단순히 기술적인 이슈에 그치지 않는다. polyfill이 많아질수록, 시스템은 점점 더 “보이지 않는 전제” 위에서 동작하게 된다. 개발자는 더 이상 환경을 신뢰할 수 없게 되고, 코드의 동작을 이해하기 위해서는 실제 실행 환경을 직접 확인해야 하는 상황에 놓인다. 이는 개발 경험을 복잡하게 만들고, 장기적으로는 시스템 전체의 유지보수 비용을 증가시킨다. 결국 polyfill은 문제를 해결했지만, 동시에 새로운 문제를 만들어냈다. 그리고 이 문제는 자연스럽게 다음 질문으로 이어진다. 환경을 수정하지 않고도 같은 문제를 해결할 수는 없을까.
두 번째 대응 — ponyfill, 환경을 건드리지 않는 대안
polyfill이 가진 구조적 한계가 드러나면서, 개발자들은 다른 방향의 해결 방식을 고민하기 시작했다. 그 결과 등장한 것이 바로 ponyfill이다. ponyfill은 polyfill과 동일한 기능을 제공하지만, 접근 방식에서 근본적인 차이를 가진다. polyfill이 환경을 수정하여 문제를 해결한다면, ponyfill은 환경을 수정하지 않고 대체 구현을 명시적으로 사용하는 방식을 선택한다. 즉, 동일한 기능을 제공하되, 환경에 개입하지 않는 것이다.
이 방식의 가장 큰 특징은 명시성이다. ponyfill은 import를 통해 필요한 기능을 가져오고, 개발자는 이를 직접 호출한다. 이는 전역 객체나 프로토타입을 수정하지 않기 때문에, 실행 환경의 상태를 예측 가능하게 유지할 수 있다. 코드의 동작은 더 이상 “환경에 따라 달라지는 것”이 아니라, 코드 자체에 의해 결정되는 것이 된다. 이는 polyfill이 가진 가장 큰 문제를 정면으로 회피하는 접근이다.
또한 ponyfill은 충돌 가능성을 크게 줄인다. 동일한 기능을 여러 라이브러리가 각각 구현하더라도, 이들은 서로의 영역을 침범하지 않는다. 각 구현은 독립적으로 존재하며, 개발자가 어떤 구현을 사용할지를 선택할 수 있다. 이는 시스템의 복잡도를 줄이고, 디버깅을 보다 단순하게 만든다. 개발자는 더 이상 “누가 환경을 수정했는가”를 추적할 필요 없이, 자신이 사용하는 코드만을 기준으로 동작을 이해할 수 있다.
그러나 ponyfill 역시 완벽한 해결책은 아니다. 이 방식은 환경을 수정하지 않는 대신, 코드의 양을 증가시키는 방향으로 문제를 해결한다. 동일한 기능이 여러 형태로 존재하게 되고, 각 프로젝트는 자신만의 구현을 선택하게 된다. 이 과정에서 코드의 중복이 발생하며, 의존성 구조는 여전히 복잡하게 유지된다. 또한 개발자는 이제 단순히 “기능을 사용”하는 것이 아니라, 어떤 구현을 선택할 것인지 판단해야 하는 책임을 가지게 된다.
결국 ponyfill은 polyfill이 가진 문제를 해결하기 위한 대안이었지만, 문제의 본질을 제거하지는 못했다. 환경을 수정하는 대신, 환경을 우회하는 방식으로 접근했을 뿐이다. 이 선택은 분명히 더 안전한 방향이었지만, 동시에 새로운 형태의 복잡성을 만들어낸다. 그리고 이 복잡성은 다시 한 번 다른 질문을 만들어낸다. 우리가 해결하려 했던 문제는 정말 환경 수정의 문제였을까, 아니면 그보다 더 근본적인 구조에 있는 것일까.
해결된 것처럼 보였던 문제 — 그러나 구조는 그대로 남았다
ponyfill의 등장은 많은 개발자들에게 하나의 전환점처럼 보였다. 환경을 수정하지 않으면서도 동일한 기능을 제공할 수 있다는 점은, polyfill의 위험성을 피할 수 있는 합리적인 대안으로 받아들여졌다. 실제로 많은 라이브러리들이 ponyfill 방식으로 전환되었고, “global을 건드리지 않는다”는 원칙은 하나의 좋은 설계 기준으로 자리 잡기 시작했다. 겉으로 보면 문제는 해결된 것처럼 보였다. 그러나 조금 더 깊이 들여다보면, 상황은 그렇게 단순하지 않다.
가장 중요한 사실은, 문제의 핵심이 사라지지 않았다는 점이다. polyfill과 ponyfill은 모두 동일한 출발점을 가지고 있다. 그것은 바로 환경을 신뢰할 수 없다는 전제다. polyfill은 이 문제를 환경을 수정하는 방식으로 해결하려 했고, ponyfill은 환경을 우회하는 방식으로 접근했다. 그러나 두 방식 모두, 결국 환경의 불확실성을 코드로 흡수한다는 점에서는 동일하다. 즉, 해결 방식이 달라졌을 뿐, 문제의 구조는 그대로 유지된 것이다.
이로 인해 발생하는 현상은 명확하다. 코드의 양은 줄어들지 않고, 오히려 다른 형태로 계속 증가한다. polyfill이 전역을 수정하는 코드였다면, ponyfill은 별도의 모듈로 존재하는 코드가 된다. 환경 대응을 위한 로직은 여전히 필요하며, 단지 그 위치와 형태만 바뀌었을 뿐이다. 결과적으로 시스템에는 여전히 “환경을 보정하기 위한 코드”가 포함되며, 이는 의존성 구조의 일부로 계속 유지된다. 우리는 문제를 제거한 것이 아니라, 문제의 표현 방식을 바꾼 것에 불과하다.

이 지점에서 우리는 중요한 통찰에 도달하게 된다. JavaScript의 문제는 단순히 “어떤 방식으로 구현하느냐”의 문제가 아니다. 더 근본적으로는, 왜 이런 코드가 계속 필요하게 되는가에 대한 문제다. polyfill이든 ponyfill이든, 그 존재 자체는 환경의 불확실성을 전제로 한다. 그리고 이 전제가 유지되는 한, 어떤 형태로든 동일한 종류의 코드는 계속해서 생성될 수밖에 없다. 결국 우리는 다시 원점으로 돌아온다. polyfill과 ponyfill의 차이를 이해하는 것은 중요하지만, 그보다 더 중요한 것은 이들이 공통적으로 해결하려 했던 문제의 본질을 이해하는 것이다.
이름 없는 코드들 — polyfill도 ponyfill도 아닌 compatibility code의 영역
앞선 논의를 통해 우리는 polyfill과 ponyfill이라는 두 가지 주요한 대응 방식을 살펴보았다. 그러나 실제 JavaScript 생태계를 조금만 더 깊이 들여다보면, 이 두 개념만으로 설명되지 않는 훨씬 더 넓은 영역이 존재한다는 사실을 발견하게 된다. 그것은 바로 polyfill도 아니고 ponyfill도 아닌, 그러나 훨씬 더 광범위하게 사용되는 코드들이다. 이들은 특정 표준 기능을 구현하기 위한 것이 아니라, 환경 자체의 불확실성을 흡수하기 위해 존재하는 코드다.
이 영역의 특징은 명확하다. 이름이 없다. polyfill처럼 정의된 개념도 아니고, ponyfill처럼 철학적으로 구분된 방식도 아니다. 대신 이 코드는 다양한 형태로 흩어져 있다. 어떤 경우에는 cross-realm 문제를 해결하기 위한 타입 체크 로직으로 등장하고, 어떤 경우에는 global 객체가 오염되었을 가능성을 방어하기 위한 wrapper로 존재한다. 또 어떤 경우에는 브라우저별 미묘한 동작 차이를 흡수하기 위한 분기문으로 구현된다. 이들은 모두 동일한 목적을 가진다. 환경을 믿지 않고, 코드 내부에서 모든 가능성을 처리한다는 목적이다.
이러한 코드의 가장 큰 특징은 “필요한 경우에만 실행된다”는 점이다. 대부분의 경우, 이 코드들은 실제로 실행되지 않는다. 특정 조건이 만족될 때만 동작하도록 설계되어 있으며, 정상적인 환경에서는 아무런 역할을 하지 않는다. 그러나 그럼에도 불구하고 이 코드는 항상 포함되어 있다. 왜냐하면 “혹시 모를 상황”을 대비하기 위해서다. 이 지점에서 우리는 중요한 구조를 발견하게 된다. JavaScript 코드의 상당 부분은 실제 기능을 위해 존재하는 것이 아니라, 발생하지 않을 가능성을 대비하기 위해 존재한다.

이러한 compatibility code는 polyfill이나 ponyfill보다 훨씬 더 은밀하게 시스템에 스며든다. 개발자는 이를 명시적으로 인식하지 못한 채 사용하며, 대부분의 경우 이 코드의 존재조차 인지하지 못한다. 그러나 이 코드들은 시스템의 복잡도를 증가시키고, 실행 경로를 확장시키며, 유지보수 비용을 높이는 중요한 요소로 작용한다. 결국 우리는 하나의 결론에 도달하게 된다. JavaScript의 복잡성은 단순히 polyfill이나 ponyfill의 문제만이 아니라, 이름 없는 수많은 compatibility code가 누적된 결과라는 점이다.
세 가지 구조의 차이 — “채운다 vs 피한다 vs 방어한다”
이제 우리는 세 가지 서로 다른 코드 구조를 모두 확인했다. polyfill, ponyfill, 그리고 compatibility code. 이들은 각각 다른 방식으로 문제를 해결하지만, 그 출발점은 동일하다. 환경을 신뢰할 수 없다는 전제다. 그러나 이 전제를 처리하는 방식은 분명히 다르며, 이 차이는 코드의 구조와 시스템의 복잡성에 직접적인 영향을 미친다. 이 지점에서 우리는 이 세 가지 접근 방식을 보다 구조적으로 정리할 필요가 있다.
polyfill은 가장 직접적인 방식이다. 환경에 없는 기능을 채워 넣는다. 이는 환경을 수정하는 접근이며, 결과적으로 코드가 환경 위에 얹히는 것이 아니라, 환경 자체가 코드에 맞게 변형된다. ponyfill은 이와 다른 방향을 선택한다. 환경을 수정하지 않고, 별도의 구현을 사용하여 동일한 기능을 제공한다. 이는 환경을 우회하는 방식이며, 코드가 환경과 독립적으로 동작하도록 만든다. 그리고 마지막으로 compatibility code는 이 둘과 또 다른 차원을 가진다. 이 코드는 환경을 수정하지도, 완전히 우회하지도 않는다. 대신 환경의 다양한 가능성을 모두 고려하여, 환경의 변동성을 코드 내부에서 흡수한다.
이 세 가지 방식은 단순한 구현 차이가 아니라, 문제를 바라보는 관점의 차이를 보여준다. polyfill은 “환경을 바꿀 수 있다”는 전제를 가지고 있고, ponyfill은 “환경을 바꾸지 않아야 한다”는 전제를 따른다. 반면 compatibility code는 “환경은 바뀔 수 있고, 예측할 수 없다”는 전제를 받아들인다. 이 차이는 매우 중요하다. 왜냐하면 이 세 가지 접근 방식이 서로 대체 관계에 있는 것이 아니라, 동시에 공존하며 시스템을 구성하기 때문이다.
실제 애플리케이션에서는 이 세 가지 방식이 모두 혼합되어 사용된다. 일부 기능은 polyfill로 제공되고, 일부는 ponyfill로 구현되며, 그 위에 다양한 compatibility code가 덧붙여진다. 이 구조는 단순한 레이어가 아니라, 서로 얽힌 형태로 존재한다. 결과적으로 개발자는 하나의 기능을 사용하기 위해, 세 가지 서로 다른 철학이 결합된 코드를 함께 사용하게 된다. 이때 중요한 것은, 개발자가 이 구조를 명확히 인식하지 못한다는 점이다. 시스템은 점점 더 복잡해지지만, 그 복잡성은 명시적으로 드러나지 않는다.

이 지점에서 우리는 중요한 질문에 도달하게 된다. 이렇게 다양한 방식으로 문제를 해결하고 있음에도 불구하고, 왜 시스템은 여전히 복잡해지는 것일까. 왜 이러한 코드들은 시간이 지나도 줄어들지 않고, 오히려 계속해서 쌓여가는 것일까. 이 질문은 자연스럽게 다음 섹션으로 이어진다.
더 큰 문제 — 이 코드들은 왜 사라지지 않는가
지금까지의 논의를 통해 우리는 polyfill, ponyfill, 그리고 compatibility code가 어떻게 등장했고, 어떤 방식으로 문제를 해결하는지를 살펴보았다. 그러나 이 모든 설명은 하나의 더 근본적인 질문을 남긴다. 왜 이 코드들은 사라지지 않는가. 환경이 개선되고, 표준이 안정화되었으며, 대부분의 브라우저가 동일한 기능을 지원하게 되었음에도 불구하고, 왜 우리는 여전히 이러한 코드를 계속해서 사용하고 있는가.
이 질문에 대한 답은 기술적인 문제라기보다, 구조적인 문제에 가깝다. 코드의 추가는 쉽다. 새로운 기능이 필요할 때, 우리는 패키지를 하나 설치하거나, 몇 줄의 코드를 추가하면 된다. 그러나 코드를 제거하는 것은 전혀 다른 이야기다. 특정 코드가 더 이상 필요하지 않다는 것을 증명하기 위해서는, 모든 환경에서 해당 코드가 없어도 문제가 발생하지 않는다는 것을 확인해야 한다. 이는 단순한 테스트를 넘어, 환경에 대한 완전한 신뢰를 요구하는 작업이다. 그리고 대부분의 경우, 우리는 그 신뢰를 확보하지 못한다.
또한 오픈소스 생태계의 특성도 중요한 역할을 한다. 많은 패키지는 개인이나 소규모 팀에 의해 유지되며, 이들은 안정성을 가장 중요한 기준으로 삼는다. 이미 잘 동작하는 코드를 굳이 제거하여 리스크를 만들 이유가 없다. 특히 polyfill이나 ponyfill과 같은 코드는 “있어도 문제 없고, 없어도 되는 경우가 많다”는 특성을 가진다. 이 때문에 제거의 우선순위는 항상 낮아지고, 결국 코드베이스에 계속 남게 된다. 이러한 선택은 개별적으로는 합리적이지만, 전체 생태계에서는 기술 부채의 누적이라는 결과를 만든다.
더 나아가, 의존성 구조 역시 이 문제를 강화한다. 하나의 패키지가 다른 패키지에 의존하고, 그 패키지가 또 다른 패키지에 의존하는 구조에서는, 특정 코드를 제거하는 것이 전체 시스템에 어떤 영향을 미칠지 예측하기 어렵다. 개발자는 자신의 코드뿐만 아니라, 수많은 간접 의존성을 고려해야 한다. 이 과정에서 가장 안전한 선택은 아무것도 바꾸지 않는 것이다. 결국 우리는 “문제가 없으면 건드리지 않는다”는 전략을 반복하게 되고, 이 전략은 코드의 영구화를 가속화한다.

이 지점에서 우리는 하나의 명확한 결론에 도달한다. polyfill, ponyfill, 그리고 compatibility code가 사라지지 않는 이유는, 그것이 필요해서가 아니라, 제거할 수 없기 때문이다. 그리고 이 구조는 단순한 기술 선택의 문제가 아니라, 생태계 전체가 만들어낸 관성의 결과다. 결국 우리는 다시 처음의 질문으로 돌아온다. 이 모든 코드들은 과연 지금도 필요한가, 아니면 단지 제거되지 않았기 때문에 남아 있는 것인가.
결론 — 우리는 기능이 아니라 ‘환경의 불확실성’을 처리하고 있다
지금까지의 흐름을 따라오면 하나의 일관된 패턴이 드러난다. polyfill은 환경을 수정하는 방식으로 문제를 해결하려 했고, ponyfill은 환경을 우회하는 방식으로 접근했으며, compatibility code는 환경의 다양한 가능성을 코드 내부에서 흡수하려 했다. 이 세 가지 방식은 서로 다른 철학을 가지고 있지만, 모두 동일한 출발점을 공유한다. 그것은 바로 JavaScript의 실행 환경은 신뢰할 수 없다는 전제다. 그리고 이 전제는 단순한 기술적 사실을 넘어, 생태계 전체의 구조를 형성하는 핵심 요소로 작용해 왔다.
이 지점에서 중요한 것은, 우리가 이 구조를 얼마나 당연하게 받아들이고 있는가다. 대부분의 개발자는 polyfill이나 ponyfill을 특별한 기술로 인식하지 않는다. 그것은 단지 “필요한 기능을 제공하는 코드”일 뿐이다. 그러나 조금 더 깊이 들여다보면, 이 코드들은 단순한 기능 구현이 아니라, 환경의 불확실성을 감추기 위한 레이어라는 사실을 알 수 있다. 우리는 코드를 작성하면서 기능을 구현한다고 생각하지만, 실제로는 그 기능이 어떤 환경에서도 동일하게 동작하도록 보정하는 작업을 함께 수행하고 있다.
이러한 구조는 시간이 지날수록 더욱 강화된다. 새로운 기능이 추가될 때마다, 우리는 그 기능이 다양한 환경에서 어떻게 동작할지를 고려해야 한다. 특정 환경에서 문제가 발생할 가능성을 배제할 수 없기 때문에, 우리는 다시 한 번 방어 코드를 추가하게 된다. 이 과정은 반복되고, 코드베이스에는 점점 더 많은 “환경 대응 코드”가 쌓이게 된다. 결국 시스템은 단순한 기능 집합이 아니라, 수많은 가정과 예외를 포함한 복합적인 구조로 변해 간다. 그리고 이 구조는 코드의 길이보다 훨씬 더 큰 복잡성을 만들어낸다.

이제 우리는 질문을 조금 바꿔야 한다. polyfill이냐 ponyfill이냐, 어떤 방식이 더 좋은가를 묻는 것은 더 이상 중요한 문제가 아니다. 그보다 중요한 것은, 왜 우리는 여전히 이러한 코드에 의존하고 있는가라는 질문이다. 환경이 점점 더 표준화되고, 대부분의 런타임이 동일한 기능을 지원하게 되었음에도 불구하고, 우리는 여전히 과거의 전제를 기반으로 코드를 작성하고 있다. 이는 단순한 습관의 문제가 아니라, 오랜 시간 동안 축적된 구조적 관성의 결과다.
결국 이 글이 말하고자 하는 핵심은 단순하다. polyfill과 ponyfill은 특정 기술의 이름이 아니라, 환경을 신뢰할 수 없었던 시대가 만들어낸 대응 방식의 흔적이다. 그리고 그 흔적은 지금도 여전히 코드 안에 남아 있다. 우리는 기능을 구현하기 위해 코드를 작성한다고 생각하지만, 실제로는 환경의 불확실성을 처리하기 위해 더 많은 코드를 추가하고 있다. 이 구조를 인식하는 순간, 우리가 작성하는 코드의 의미는 조금 다르게 보이기 시작한다.
마지막으로 남는 질문은 이것이다. 지금 우리의 환경은 과연 여전히 그렇게까지 불확실한가. 만약 그렇지 않다면, 우리는 왜 여전히 그 전제를 유지하고 있는가. 그리고 이 질문에 대한 답은, 단순히 기술적인 선택이 아니라, 앞으로 우리가 어떤 방식으로 코드를 작성할 것인가에 대한 방향을 결정하게 될 것이다.