나는 모듈 시스템이 없는 언어를 위해 모듈 시스템을 만들었다

발행: (2025년 12월 28일 오전 06:26 GMT+9)
20 min read
원문: Dev.to

Claudia Nadalin

Source: Dev.to – “I built a module system for a language that doesn’t have one”

PineScript에 대해 웃긴 점이 뭔지 아세요? 2025년인데도 우리는 1995년처럼 트레이딩 인디케이터를 작성하고 있습니다—파일 하나에 모든 것이 전역으로 선언돼 있죠. 모듈도 없고, 임포트도 없습니다. 오직 당신과 당신의 코드, 그리고 서서히 미쳐가는 과정뿐입니다.

저는 이 상황을 한동안 견뎌왔습니다. 여러 TradingView 라이브러리로 나눠서 사용하던 꽤 복잡한 인디케이터가 있었거든요. 그때는 그게 옳은 선택인 것처럼 보였습니다: 관심사를 분리하고, 코드를 깔끔하게 유지하고, 아주 전문적으로 보이게 말이죠.

그런데 어느 순간, 한 함수만 바꿔야 할 상황이 찾아왔습니다.

지옥 같은 워크플로우

라이브러리 함수를 업데이트하는 과정은 다음과 같았습니다:

  1. 로컬에서 코드를 편집한다.
  2. Git에 푸시한다.
  3. TradingView 편집기에 복사‑붙여넣기한다.
  4. 라이브러리를 다시 퍼블리시한다.
  5. 새로운 버전 번호를 확인한다.
  6. 인디케이터 스크립트를 연다.
  7. 새로운 버전으로 import 문을 업데이트한다.
  8. 스크립트를 저장한다.
  9. 변경 사항을 Git에 푸시한다.
  10. 업데이트된 스크립트를 다시 TradingView 편집기에 복사‑붙여넣기한다.
  11. 다시 저장한다.

그리고 이것은 하나의 라이브러리만을 위한 것이었다—나는 다섯 개를 가지고 있었다.

“조지가 자신의 모든 본능과 반대로 행동하려는 Seinfeld 에피소드를 본 적 있나요?”
이 워크플로우가 바로 그 느낌이었지만, 끝에 야구팀 잡힌 게 아니라—그냥 고쳐야 할 버그가 있었을 뿐이다.

더 나은 방법이 있어야 했다.

누군가 이미 해결했을 거라 생각합니다

조사를 시작했습니다. “PineScript bundler”, “PineScript multiple files”, “PineScript module system” 같은 것을 구글링했습니다.

찾은 것들:

  • VS Code 확장 프로그램으로 구문 강조 (멋지지만 내가 필요한 것은 아님)
  • PyneCore/PyneSys, PineScript를 Python으로 변환해 로컬에서 백테스트를 실행할 수 있게 해줍니다 (흥미롭지만 다른 문제)
  • 같은 좌절감을 가진 사람들의 포럼 게시글이 많이 있었지만 해결책은 없었습니다

내가 찾지 못한 것은 번들러였습니다. 아무도 내가 원하는 것을 만들지 않았습니다.

하지만 검색하기도 전에, 왜 이것이 어려운지 알았습니다. 어떤 해결책도 필요로 하는 것이 무엇인지 알았고, 그것은 결코 깔끔하지 않았습니다.

Why This Is Actually Hard

JavaScript에는 스코프 라는 개념이 있습니다. Webpack이 코드를 번들링할 때 각 모듈을 함수로 감싸서 격리시킬 수 있습니다:

var __module_1__ = (function () {
  function double(x) {
    return x * 2;
  }
  return { double };
})();

var __module_2__ = (function () {
  function double(x) {
    return x + 100;
  } // No collision – different scope
  return { double };
})();

double이라는 이름을 가진 두 함수가 있어도 문제되지 않습니다. 서로 다른 스코프에 존재하므로 서로를 볼 수 없습니다. Webpack은 이 점을 활용해 모듈을 격리합니다.

PineScript에는 이러한 개념이 없습니다.
클로저도 없고, 격리된 스코프를 만들 방법도 없습니다. 모든 것이 기본적으로 전역입니다. 한 파일에서 double을 정의하고 다른 파일에서도 double을 정의한 뒤 두 파일을 합치면 충돌이 발생합니다—하나가 다른 것을 덮어써서 코드가 깨집니다.

따라서 PineScript 번들러는 이름을 바꾸는(rename) 작업이 필요합니다. 예를 들어 utils/math.pine에 있는 double 함수를 __utils_math__double 같은 이름으로 바꾸는 식입니다. 모든 파일의 모든 export에 대해 동일하게 이름을 바꾸고, 모든 참조도 업데이트하면 충돌이 사라집니다.

코드를 한 줄도 작성하기 전에 이 길이 정답이라는 것을 알았습니다. 문제는 *코드에서 이름을 신뢰성 있게 바꾸려면 어떻게 해야 할까?*였습니다.

찾기/바꾸기 함정

내 첫 번째 직감은 간단했습니다: find / replace만 사용하는 것이죠. double을 찾아 __utils_math__double로 바꾸면 됩니다. 끝.

왜 이것이 실패하는가

double(x) =>
    x * 2

myLabel = "Call double() for twice the value"
doubleCheck = true
plot(double(close), title="double")

double 함수를 __utils_math__double로 이름을 바꾸고 싶습니다. 순진한 찾기 / 바꾸기를 하면 다음과 같이 됩니다:

__utils_math__double(x) =>
    x * 2

myLabel = "Call __utils_math__double() for twice the value"  // 문자열 내용이 바뀌어 깨짐
__utils_math__doubleCheck = true                              // 잘못된 변수 이름이 바뀌어 깨짐
plot(__utils_math__double(close), title="__utils_math__double") // 문자열 내용이 바뀌어 깨짐
  • 문자열 리터럴 "double"이 변경되었습니다.
  • 관련 없는 변수 doubleCheck가 이름이 바뀌었습니다.

찾기 / 바꾸기는 소스를 단순히 문자들의 평면 문자열로 취급하기 때문에 다음을 구분할 수 없습니다:

  • 함수 정의(double)
  • 함수 호출(double)
  • 문자열 리터럴("double")

Pine Script를 올바르게 파싱하려면 들여쓰기, 라인 연속, 타입 추론 등 특이한 문법을 모두 처리할 수 있는 완전한 파서가 필요합니다.

그래서 저는 pynescript를 사용하게 되었습니다.

The Missing Piece

pynescript는 PineScript를 추상 구문 트리 (AST) 로 파싱하고 다시 PineScript로 언파싱할 수 있는 파이썬 라이브러리입니다.

“추상 구문 트리”가 겁나는 용어처럼 들릴 수 있지만, 실제로는 아주 간단한 개념입니다. AST는 기본적으로 HTML의 DOM과 같은 역할을 하는데, HTML이 아니라 코드에 적용됩니다.

브라우저가 HTML을 받으면 원시 문자열 그대로 보관하지 않습니다. 마크업을 트리 구조로 파싱해서 각 노드가 <script> 태그, <div> 태그, 텍스트 노드 등 자신이 무엇인지 알게 됩니다. 이렇게 파싱된 노드들을 프로그래밍적으로 조작해 클래스를 추가하거나, 텍스트를 바꾸거나, 요소를 이동시킬 수 있으며, 원시 HTML 문자열을 직접 다루지 않아도 됩니다.

AST도 코드에 대해 같은 일을 합니다. 다음 PineScript 코드를 파싱하면:

double(x) =>
    x * 2

myLabel = "Call double() for twice the value"
doubleCheck = true
plot(double(close), title="double")

다음과 같은 트리를 얻습니다:

  • 함수 정의 double는 별개의 노드입니다.
  • 문자열 리터럴 "Call double() for twice the value"는 또 다른 노드입니다.
  • 변수 doubleCheck는 자체 식별자 노드입니다.
  • 함수 호출 double(close) 역시 또 다른 노드입니다.

트리가 각 토큰의 유형을 알기 때문에, 함수 double을 나타내는 식별자 노드만 안전하게 이름을 바꿀 수 있고, 문자열이나 관련 없는 식별자는 그대로 두게 됩니다.

이러한 방식으로 번들러는 다음을 수행할 수 있습니다:

  1. Parse 모든 소스 파일을 AST로 변환합니다.
  2. Walk 트리를 순회하면서 모든 내보낸 식별자를 수집합니다.
  3. Generate 고유하고 네임스페이스가 포함된 식별자(예: __utils_math__double)를 생성합니다.
  4. Replace 이름을 바꿔야 하는 식별자 노드만 교체합니다.
  5. Emit 변환된 코드를 각 파일에 대해 생성하고 결과를 연결합니다.

이것이 제가 최종적으로 만든 PineScript 번들러의 핵심 아이디어입니다.

TL;DR

  • PineScript는 전역 전용 특성 때문에 이름 충돌 없이 단순히 번들링하는 것이 불가능합니다.
  • 단순한 찾기/바꾸기는 코드 구조와 일반 텍스트를 구분하지 못해 깨집니다.
  • **pynescript**는 PineScript용 정식 AST를 제공하여 식별자를 안전하게 이름 바꾸기 할 수 있게 해줍니다.
  • AST 기반 접근법을 사용하면 모든 export에 네임스페이스를 적용하고 충돌을 없애는 신뢰할 수 있는 PineScript 번들러를 만들 수 있습니다.

같은 워크플로우 악몽에 시달리고 있다면 pynescript를 한 번 살펴보고 작은 번들러를 직접 만들어 보세요. 복사‑붙여넣기와 버전 탐색에 수많은 시간을 절약해 주었고, “1995‑스타일” PineScript 지옥에서 여러분을 구해줄 수도 있습니다.

Example AST

Script
├── FunctionDefinition
│   ├── name: "double"           ← I'm a function definition
│   ├── parameters: ["x"]
│   └── body: BinaryOp(x * 2)

└── Assignment
    ├── target: "myLabel"
    └── value: StringLiteral     ← I'm a string, leave me alone
        └── "Call double()"

이제 이름 바꾸기가 정밀하게 이루어집니다. 당신은 이렇게 말합니다:

FunctionDefinition 타입의 모든 노드 중 이름이 double인 것을 찾아서 이름을 바꾸세요. 그 노드들을 바꾸고, FunctionCall 타입의 모든 노드 중 함수가 double인 것을 찾아서 역시 이름을 바꾸세요. StringLiteral 노드는 그대로 두세요.”

문자열 내용은 StringLiteral 노드이기 때문에 손대지 않으며, FunctionCall 노드가 아니기 때문에 변경되지 않습니다. doubleCheck는 전혀 다른 식별자 노드이므로 그대로 남아 있습니다. 구조가 무엇인지 정확히 알려줍니다.

pynescript는 이미 PineScript를 올바르게 파싱하는 어려운 작업(ANTLR을 사용한 수개월 간의 작업, 진지한 파서 생성기)을 수행했습니다. 나는 단순히 pip install pynescript만 하면 그 트리에 접근할 수 있었습니다.

나는 전략(접두사로 이름 바꾸기)을 가지고 있었고, 이제 도구(pynescript를 통한 AST 조작)를 갖게 되었습니다. 실제로 작동하는지 확인할 시간입니다.

스파이크

전체 도구를 만들기 전에 개념을 증명하고 싶었습니다. 두 개의 PineScript 파일을 가져와 파싱하고, AST에서 이름을 바꾸고, 병합하고, 다시 파싱한 뒤 TradingView가 출력물을 받아들이는지 확인했습니다.

math_utils.pine

//@version=5
// @export double

double(x) =>
    x * 2

main.pine

//@version=5
// @import { double } from "./math_utils.pine"

indicator("Test", overlay=true)

result = double(close)
plot(result)

그 다음에 파이썬으로 다음 작업을 수행했습니다:

  1. pynescript를 사용해 두 파일을 파싱합니다.
  2. AST에서 double 함수 정의를 찾아 __math_utils__double 로 이름을 바꿉니다.
  3. main.pine의 AST에서 double에 대한 참조를 찾아 __math_utils__double 로 업데이트합니다.
  4. 병합하고 다시 파싱합니다.

그 결과:

//@version=5
indicator("Test", overlay=true)

__math_utils__double(x) =>
    x * 2

result = __math_utils__double(close)
plot(result)

이를 TradingView에 붙여넣었습니다. 정상적으로 작동했습니다.

저는 정말 놀랐습니다—마법이라기보다는, 어떤 이상한 엣지 케이스나 파서 제한 때문에 전체 아이디어가 무너질 거라 예상했기 때문이었습니다. 하지만 전혀 그렇지 않았습니다. 그냥… 작동했습니다.

실제 구현하기

스파이크가 작동하니 전체 도구를 구축했습니다:

  • CLI (pinecone build, pinecone build --watch).
  • 설정 파일 지원.
  • 올바른 의존성 그래프 구성.
  • 위상 정렬(의존성이 해당 코드를 사용하기 전에 오도록).
  • 번들된 출력이 아니라 원본 파일을 가리키는 오류 메시지.
  • --copy 플래그를 사용해 출력을 바로 클립보드에 복사.

모듈 구문은 주석을 사용합니다. 그래서 번들되지 않은 코드를 실수로 TradingView에 붙여넣어도 깨지지 않습니다:

// @import { customRsi } from "./indicators/rsi.pine"
// @export myFunction

TradingView 파서는 이를 단순히 주석으로 보고 무시합니다. PineCone은 이를 지시문으로 인식합니다.

프리픽스 전략은 파일 경로를 사용합니다. src/utils/math.pine__utils_math__가 됩니다. 해당 파일에서 내보낸 모든 함수와 변수는 이 프리픽스를 붙이고, 그 내보낸 항목에 대한 모든 참조도 업데이트됩니다.

아름답지는 않습니다. 번들된 출력에는 __indicators_rsi__customRsi와 같은 보기 안 좋은 이름이 생깁니다. 하지만 동작합니다. TradingView가 이를 받아들입니다. 충돌도 없고, 번들된 출력을 직접 볼 필요도 없습니다—그것은 컴파일된 코드와 같은 중간 산출물일 뿐입니다.

실제로 무엇을 만들었나요?

저는 이것을 “PineScript용 Webpack”이라고 부르고 있었지만, 정확히는 그렇지 않습니다. Webpack은 코드 스플리팅, 레이지 로딩, 트리 쉐이킹, 핫 모듈 교체 등 다양한 작업을 수행합니다.

PineCone은 하나의 일을 합니다: 모듈 시스템을 사용해 여러 파일에 걸쳐 PineScript를 작성하고, TradingView가 이해할 수 있는 단일 파일로 컴파일합니다.

  • 이것은 모듈 시스템이자 번들러이며, 해당 언어 자체에는 이러한 기능이 없습니다.
  • PineScript에 새로운 기능을 추가하거나, 인디케이터를 더 빠르게 만들거나, 브로커와 연결해 주지는 않습니다.
  • 단순히 2025년 현재의 일반적인 인간처럼 코드를 정리할 수 있게 해줄 뿐입니다.

솔직히 말해서? 그 정도면 충분합니다. 제가 필요했던 바로 그 것이죠.

요약

근본적인 무언가가 빠진 언어를 다루고 있다면, 반드시 막힌 것은 아닙니다. 파서를 찾아보세요. AST 도구를 찾아보세요. 누군가 이미 그 언어 구조를 이해하는 작업을 해두었다면, 그 위에 여러분이 구축하면 됩니다.

pynescript는 제 문제를 직접 해결해 주지는 않았습니다. 파서일 뿐, 번들러는 아니니까요. 하지만 어려운 부분—PineScript를 정확히 파싱하는 것—을 해결해 주었고, 제가 실제로 신경 써야 했던 모듈 시스템 부분에 집중할 수 있게 해 주었습니다.

PineCone은 오픈 소스입니다. 여러 라이브러리를 관리하면서 고통을 겪어본 PineScript 개발자라면 한번 사용해 보세요. 그리고 더 나은 아이디어가 있다면 언제든지 의견을 주세요.

Serenity now.

PineCone여기에서 사용할 수 있습니다. PineScript와 프로그래밍적으로 작업한다면 확인해 볼 만한 pynescript로 구축되었습니다.

Claudia는 프론트‑엔드 개발자입니다. 그녀의 다른 작업은 여기에서 확인할 수 있습니다.

다음은 올바른 링크 구문을 사용한 정리된 마크다운 구문입니다:

[Visit the website](https://www.claudianadalin.com/).
Back to Blog

관련 글

더 보기 »

Claw로 휴대폰에서 Claude Code 제어하기

문제: 당신은 Claude Code 세션에 깊이 몰두해 있습니다. 복잡한 작업을 진행 중입니다. 하지만 잠시 떠나야 합니다—커피를 마시거나, 전화를 받거나, 아이를 데려와야 합니다. 무엇을…