Blog iconJaeho Yi
JetBrains IDE용 GDShader 플러그인

개요

notion image
💡
기간: 2025. 07. ~ 2026. 04. (이후 오픈소스 유지보수)
사용 기술: Kotlin, IntelliJ Platform, Grammar Kit, JFlex
결과: JetBrains Marketplace 배포, 누적 다운로드 18,000회 이상
Godot 엔진의 셰이딩 언어인 GDShader를 IntelliJ 기반 IDE에서 쉽게 편집할 수 있도록 지원하는 플러그인입니다. 문법 토큰화 및 파싱부터 시작하여 코드 하이라이팅, 자동완성, 포매팅, 심볼 찾기, 리팩터링 등의 IDE 기능을 지원하도록 구현했습니다. 개발에는 IntelliJ Platform SDK와 Kotlin이 사용됐습니다.

프로젝트 목표

GDShader는 Godot 엔진에서 사용되는 셰이딩 언어입니다. Rider IDE에서는 Godot 엔진 개발 환경을 공식적으로 제공하고 있지만, GDShader에 대한 지원은 없는 상황이기 때문에 셰이더 작업을 할 때는 일관적인 개발 환경을 경험하기 어려웠습니다. Rider에서 GDShader를 지원한다면 개발 환경의 일관성을 높일 수 있을 뿐만 아니라, Godot 엔진 내의 에디터에 비해서 하이라이팅, 자동완성, 포매팅 등의 더 많은 에디팅 기능을 적용할 수 있을 것이라고 생각해 개발을 시작했습니다.
IntelliJ 기반 IDE는 커스텀 언어를 지원하는 플러그인 SDK와 공식 가이드를 제공하고 있습니다. SDK를 활용하여 GDShader를 하나의 커스텀 언어로서 IDE의 개발 환경에 통합하는 것을 목표로 했습니다.

기능

코드를 파싱하여 생성된 PSI 트리를 기반으로 아래와 같은 핵심 기능을 구현했습니다.
  • 문법 하이라이팅
  • 코드 자동완성
  • 코드 포매팅
  • 심볼의 정의 위치 이동 / 사용 위치 찾기
  • 심볼 이름 변경
  • 문법, 의미적 오류 감지
  • 빌트인 함수, 변수 등의 문서화 설명 제공
  • 색상 선택기

구조

구현

Lexer/Parser

GDShader 코드를 해석하는 첫번째 단계로, Lexer를 거쳐 토큰 단위로 분리하는 단계와 Parser를 거쳐 PSI 트리로 변환하는 단계로 나뉘어 있습니다.
Lexer와 Parser 구현에는 JetBrains의 Grammar-Kit을 사용했습니다. 토큰을 JFlex 문법으로 정의하고, 각 토큰을 기반으로 문법을 BNF로 작성한 뒤에 Grammar-Kit로 Lexer와 Parser 코드를 생성했습니다.
이 과정에서 목표는 단순히 코드가 문법적으로 유효한지만 판별하는 것이 아니라, 이후 자동완성, 참조 탐색, 리팩터링, 포매팅과 같은 에디터 기능에 활용할 수 있는 PSI 구조를 만드는 것이었습니다. 예를 들어 변수 선언, 함수 선언, 표현식, 타입 정보가 PSI 트리에서 의미 있는 단위로 구분되어야 이후 기능을 안정적으로 구현할 수 있었습니다.
또한 IDE에서는 사용자가 코드를 작성하는 도중 일시적으로 문법적으로 완성되지 않은 상태가 자주 발생합니다. 이때 Parser가 지나치게 엄격하게 동작하면 PSI 트리가 깨지고, 따라서 자동완성이나 하이라이팅 같은 기능도 함께 실패할 수 있습니다. 따라서 Parser는 최대한 편집 중인 불완전한 코드도 PSI로 표현할 수 있도록 구성하고, 문법적, 의미적 오류는 별도의 에디터 기능에서 검사하도록 분리했습니다.

PSI 기반 IDE 기능 구현

Lexer와 Parser를 거쳐 생성된 PSI 트리는 이후 IDE 기능을 구현하는 기반이 됩니다. GDShader 코드는 변수 선언, 함수 선언, 구조체 선언, 표현식 등의 규칙 단위로 트리를 구성하고, 플러그인은 이 구조를 기반으로 자동완성, 정의 위치 이동, 사용 위치 찾기, 이름 변경 리팩터링 등의 기능을 제공합니다.
예를 들어 표현식에 등장하는 심볼이 어떤 선언을 참조하는지 파악하기 위해서는 현재 위치의 PSI context를 기준으로 접근 가능한 선언을 탐색해야 합니다. 변수의 특성이나 선언된 위치에 따라서 사용 가능한 범위가 다르기 때문에, 스코프 규칙에 따라 적절한 정의를 찾아 연결했습니다.
이렇게 구성한 참조 해석 로직은 Go to Definition, Find Usages, 이름변경 리팩터링처럼 심볼 간 연결 관계가 필요한 기능에서 사용합니다. 또한 코드 하이라이팅, 포매팅, 자동완성도 현재 작성 위치의 PSI context를 기반으로 동작할 수 있도록 했습니다.

타입 추론과 함수 호출 검사

파싱만으로는 함수 호출이 올바른지, 표현식이 어떤 타입을 갖는지 판단할 수 없습니다. 이를 위해서 플러그인 내부에 GDShader의 타입 시스템을 표현하는 도메인 모델을 구현했습니다. 기본 타입, 벡터 타입, 행렬 타입, 샘플러 타입, 배열 타입 등을 모델링하고, 변수 선언이나 함수 선언을 이 모델로 변환해 사용했습니다. 이때 모든 타입을 enum이나 문자열로 처리하지 않고 지원 동작을 인터페이스로 분리했습니다. 이 덕분에 기능의 구현을 수정하지 않고도 새로운 타입을 추가할 수 있는 등의 이점이 있었습니다.
빌트인 함수, 변수 등도 이 타입 모델으로 저장되어 있습니다. 코드가 이 타입 모델로 변환되기 때문에 함수 호출에 전달된 파라미터의 타입이 유효한지, 표현식의 타입이 유효한지 등을 검사할 수 있습니다. 또한 자동완성에도 키워드의 타입 정보가 표시되도록 하기에도 용이했습니다.
타입 추론의 경우 GDShader의 규칙과 GLSL의 규칙에 조금씩 차이가 있었습니다. 이를 해결하기 위해서 여러 예시 코드를 찾아서 엣지 케이스를 확인했고, Godot 엔진의 셰이더 컴파일러 코드를 확인해서 최대한 실제 컴파일러 규칙과 일치시켰습니다.

에디터 기능

언어 지원 플러그인은 코드 분석 기능뿐만 아니라 코드를 작성할 때의 경험도 중요하다고 생각했습니다. 따라서 코드 포매팅, 코드 스타일 설정, 코드 접기, Structure View, 색상 선택기 등의 자주 사용되는 기능을 구현했습니다.
셰이더 코드에는 vec3 또는 vec4 타입의 색상 값이 들어가는 경우가 많습니다. 따라서 이 색상을 에디터에서 편하게 확인하고 조절할 수 있도록 색상 선택기를 구현했습니다. 플러그인이 타입과 변수명을 통해서 색상 값으로 추정되는 위치를 식별하고, 이러한 값을 색상 선택기로 쉽게 변경할 수 있도록 했습니다.

테스트

플러그인의 기능들은 대부분 Lexer와 Parser를 거쳐 생성된 PSI 트리를 기반으로 동작합니다. 따라서 새로운 문법을 추가하거나 기존 문법을 수정하면 PSI 트리에 의존하는 기존의 기능들이 깨질 가능성이 높았습니다.
따라서 수정 사항의 영향 범위를 파악하고 회귀 버그를 방지하기 위해서 Parser, 자동완성, 참조 해석, 포매팅 등 대부분의 기능에 대한 테스트를 만들었습니다. 기능을 추가하거나 Godot 엔진의 업데이트를 반영할 때 기존 기능이 유지되는지 빠르게 파악할 수 있다는 점에서 큰 도움이 됐습니다.

배포/유지보수

notion image
JetBrains Marketplace에 이 플러그인을 배포했습니다. 개인적으로만 사용하는 도구가 아니라 실제 사용자가 설치하고 사용했다는 것을 확인하고 리뷰를 받을 수 있다는 점에서 의미있는 경험이었습니다.
배포 이후에는 GitHub에 프로젝트를 오픈소스화했습니다. 빌드와 기여 절차를 문서화했고, Pull Request가 생성되면 GitHub Actions로 테스트가 자동 실행되도록 설정했습니다. 이를 통해 GitHub 내에서 외부 기여를 쉽게 1차적으로 테스트할 수 있었습니다.
많은 에디터 기능들이 제공되고 있지만, 일부 엣지 케이스나 개선할 부분도 남아 있습니다. Godot의 버전에 따른 문법 차이 대응, 전처리기 지원 확대, 문법 검사 정확도 개선을 중심으로 확장할 계획입니다.