간단한 경우에는 C++ Modules를 꽤 쉽게 쓸 수 있지만, IntelliSense 문제, `#include`와 `import`의 혼용 제한, 모듈의 전염성, 이중 빌드 지원의 복잡성 때문에 2026년에도 여전히 도입은 까다롭다.
13 Apr 2026 on C++
그럴지도? 아마도? 복잡합니다.
저는 6개월에서 12개월마다 한 번씩 C++ 모듈을 써보려다가, 장애물을 만나고, 아마 소셜 미디어에서 한바탕 투덜거리고, 그러고는 다른 일로 넘어가곤 합니다. 이 주제에 관한 여러발표를 봤는데도, 항상 뭔가가 발목을 잡습니다. 지금까지 가장 큰 성공 사례는 제 렌더러 라이브러리에서 VulkanHpp 모듈을 사용하게 만든 것이었고, 그 뒤로는 상황이 무너지기 시작했습니다. 하지만 지난주에 다시 약간의 진전을 이루었고, 동시에 새로운 장애물도 만난 끝에, 이제는 제대로 된 요약을 쓸 만큼은 되었다고 느꼈습니다.
면책 차원에서 말씀드리자면, 저는 모듈의 현재 상태에 대한 제 결론 일부를 다른 C++ 프로그래머들과 공유해봤고, 모두가 제 결론에 동의한 것은 아니었습니다. 하지만 저는 모듈이 강한 “전문가 편향” 문제를 안고 있다고 생각합니다. 그래서 많은 반론이, 저처럼 이 주제에 깊이 노출되지 않았고 표준화 과정을 면밀히 따라가지도 않은 사람에게는 “내 컴퓨터에서는 되는데?”처럼 들립니다. 저는 이 주제의 전문가라고 자처하지는 않습니다. 하지만 저는 빌드 시스템은 잘 알고, 평균적인 C++ 프로그래머보다 제 프로젝트에서 모듈을 가지고 씨름하는 데 훨씬 더 많은 시간을 썼다고 믿습니다. 그래서 이 글은 평균적인 열성 사용자, 혹은 좀 더 정확히는 잠재적 사용자 입장을 대변할 수 있다고 생각합니다.
아, 그리고 저는 주로 MSVC에 초점을 맞춥니다. Clang이나 GCC를 잠깐 언급할 수는 있겠지만, 제 경험은 대부분 Windows에서 나온 것입니다.
들어보신 이야기와는 달리, 단순한 사용 사례는 꽤 엄격한 제한 집합 안에 머무르기만 한다면 비교적 쉽게 동작하게 만들 수 있습니다. 예를 들어, 앞서 말했듯이 저는 제 렌더링 라이브러리에서 VulkanHpp가 제공하는 모듈을 사용했고, 아주 잘 동작했습니다. 더 정확히 말하면, 상류에서 뭔가가 바뀌어 제가 암시한 제한 집합에 걸리기 전까지는 잘 동작했습니다. 자세한 이야기는 뒤에서 다시 하겠습니다. 일단은 제 CMake에서 어떻게 생겼는지 보시죠:
add_library( VulkanHppModule )
target_sources( VulkanHppModule PRIVATE
FILE_SET CXX_MODULES
BASE_DIRS ${Vulkan_INCLUDE_DIR}
FILES ${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm
)
target_compile_definitions( VulkanHppModule PUBLIC
VULKAN_HPP_NO_SETTERS
VULKAN_HPP_NO_CONSTRUCTORS
)
target_link_libraries( VulkanHppModule PUBLIC Vulkan::Vulkan )
저는 이 줄들을 직접 생각해낼 필요조차 없었습니다. 프로젝트 문서에서 그대로 제공해줬거든요. 제가 실제로 맞춤 설정할 필요가 있었던 건, 필요하다면 컴파일 정의 정도였습니다. 이 경우에는 setter와 constructor를 비활성화해서 대신 C++ 20 지정 초기화자에 의존하도록 했습니다.
그리고 그렇게 해서 동작했습니다. 제 렌더러 라이브러리에서 그냥 import vulkan_hpp를 하고 Vulkan의 C++ 바인딩을 쓸 수 있었습니다. 이걸 동작시키는 데 실패했더라면, 아마 저는 Vulkan의 표준 #include 사용 시 컴파일 시간이 너무 끔찍했기 때문에, 다시 Vulkan의 C API로 돌아가 제 나름의 RAII 래퍼를 만들었을 겁니다. 이것은 재귀적으로도 동작했습니다. 다시 말해, 제 렌더러 라이브러리가 공개 헤더 안에 import로 VulkanHpp를 포함하고 있어도, #include <renderer/renderer.h>를 하는 제 프로젝트들로 잘 전달되었습니다. 물론 이것도 뒤에서 설명할 제한이 있기는 합니다.
CMake로 모듈을 쓰려면 약간의 해킹이 필요하고, CMAKE_CXX_SCAN_FOR_MODULES, CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP, CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API 같은 난해한 플래그를 써야 한다는 이야기를 보셨을지도 모르지만, 적어도 지금은 그런 것들이 필요하지 않습니다. 최근 버전의 CMake를 쓰기만 하면 됩니다. 이상적으로는 4.x이고, 기본값도 3.28부터는 켜져 있을 겁니다.
그렇게 해서, 적은 노력으로 저는 VulkanHpp를 포함하는 데 걸리던 고통스러운 9초를 무시해도 될 정도의 몇 밀리초로 바꿔놓았습니다. 저는 이것을 확실한 승리라고 봅니다. 이제 문제의 시작입니다…
재미있는 사실 하나 말씀드리죠. 2019년 SG15 회의록을 찾아보면 Microsoft가 Edge 팀 내부에서는 모듈이 아주 잘 돌아가고 있다고 주장한 내용을 볼 수 있습니다. 그런데 Visual Studio 2026에서 모듈을 사용하는 프로젝트를 열면 이런 놀라운 메시지가 여러분을 맞이합니다:
C++ IntelliSense support for C++20 Modules is currently experimental.
맞습니다. 7년이 지났는데도 여전히 IntelliSense는 import 지시문을 제대로 파싱하지 못합니다. 언어 서버가 VC++가 아니라 EDG 기반이라는 건 알고 있습니다. 하지만 솔직히 저는 신경 쓰지 않습니다. 글을 쓰는 시점 기준으로 거의 3 조 달러의 가치가 있는 회사가, 자사 내부 성공 사례를 바탕으로 모듈 표준화를 밀어붙인 지 거의 10년이 지난 시점에 기능 하나를 제대로 작동시킬 수 없다고 말하고 있는 겁니다. 당시 주장을 과장했는지, 이후 Visual Studio 팀에 충분한 투자를 하지 않았는지, 혹은 다른 이유가 있는지는 모르겠습니다. 하지만 8년이 지났는데도 모듈에서 문법 강조가 안 된다는 건 납득하기 어렵습니다. 만약 정말 그 정도로 어려운 일이라면, 애초에 제안 자체에 뭔가 깊은 문제가 있었던 것이고, 위원회는 찬성표를 던지기 전에 증빙부터 봤어야 했습니다.
어쨌든, 해결 방법은 이렇습니다:
#if defined( __INTELLISENSE__ )
#include <vulkan/vulkan.hpp>
#include <vulkan/vulkan_raii.hpp>
#else
import vulkan_hpp;
#endif
이렇게 하면 컴파일러와 반복 작업 시간은 모듈의 빠른 경로를 유지하고, IntelliSense는 백그라운드에서 헤더 파일을 꾸역꾸역 파싱하면서 강조와 자동 완성을 제공할 수 있습니다. 해킹이냐고요? 물론입니다. 하지만 저는 지난 6개월 동안 이 해킹을 써왔고, 덕분에 다른 일에 집중할 수 있었습니다.
이제 이걸 치워놓았으니, 진짜 문제를 이야기할 수 있습니다.
앞선 부분들에서 저는, 몇 가지 엄격한 제한을 지키면 모듈이 동작한다고 암시했습니다. 문제는 그 제한이 결코 작지 않다는 점입니다. 핵심적으로, 모듈은 거의 올-오어-낫싱에 가깝습니다. import 지시문을 통해 어떤 라이브러리를 쓰기 시작하면, 같은 번역 단위에서 그것을 #include로 다시 끌어올 수 없습니다. 그리고 이것은 순식간에 문제가 됩니다.
가장 단순한 예시는 이렇습니다:
// 당연히 동작함
#include <array>
// <array>가 먼저 포함되었고 std 모듈의 일부여도 동작함
import std;
// 오류. "xxx already declared"류의 실패가 백만 개 쏟아짐
#include <utility>
간단히 말해, 어떤 라이브러리는 #include가 먼저 오고 import가 나중에 오는 한 import도 되고 include도 됩니다. 이것이 표준에 의해 강제되는 건지, 아니면 구현상의 제한인지는 아직도 잘 모르겠습니다. 하지만 적어도 MSVC에서 제가 직접 관찰했고, 다른 사람들도 언급하는 것을 들었습니다.
이전의 제 사용 사례에서는 괜찮았습니다. VulkanHpp는 제 렌더러 라이브러리에서만 import되고, 자기 자신은 아무것도 import하지 않으며, 제 빌드 트리의 다른 곳에서는 사용되지 않았기 때문입니다. 안타깝게도 최근 릴리스에서 import std를 통해 표준 라이브러리를 끌어오기 시작하면서 상황이 급격히 나빠졌습니다. 갑자기 아주 흔한 라이브러리를 import하는 전이적 의존성이 생겨버린 겁니다. 이제 저는 import vulkan_hpp 지시문이 표준 라이브러리의 다른 모든 #include 뒤에 오도록 보장해야 합니다. 그리고 vulkan_hpp가 제 렌더러 라이브러리에서 공개적으로 사용되기 때문에, 이제 제 렌더러 라이브러리 자체도 모든 번역 단위에서 항상 마지막에 import되어야 합니다. 그렇지 않으면 재선언/재정의 컴파일 오류가 수없이 쏟아집니다.
제가 듣기로는 선호되는 해결책은 모든 것을 모듈로 옮기는 것입니다. 혹은 적어도 하나의 라이브러리가 import std를 시작했다면, 제가 쓰는 다른 모든 라이브러리도 import std만 사용하도록 패치하는 것이죠. 제 작은 프로젝트의 경우라면 적어도 TBB와 fastgltf 정도는 그렇게 해야 합니다. 아이러니하게도 C 표준 라이브러리에만 의존하는 C++ 라이브러리들은 영향이 없는 것처럼 보입니다. 아마 제가 import std.compat를 썼다면 영향이 있었겠죠? STL 사용을 거부하는 라이브러리 작성자들을 정당화해주는 슬픈 상황입니다.
여기서 제가 “스위치만 켜면 된다”고 하지 않고 “패치해야 한다”고 말한 점에 주목하세요. C++20이 나온 지 6년이 지났는데도, 모듈 정의를 함께 제공하는 C++ 라이브러리는 거의 없습니다. Boost도 일부 선택된 라이브러리에만 모듈을 제공합니다. Catch2가 모듈을 제공한다는 제가 읽은 주장들은 AI 환각이었던 것 같습니다. 제가 찾을 수 있었던 사실상 유일한 큰 사례는 fmt였는데, 좋은 라이브러리이긴 하지만 솔직히 C++20을 지원한다면 어차피 <format>을 이미 쓸 수 있습니다.
물론 모듈을 지원하기로 한 각 라이브러리는, 아직 모든 클라이언트가 모듈을 쓰는 것은 아니기 때문에 어떤 형태로든 이중 빌드를 제공해야 합니다. 그리고 각자의 의존성마다, 그것을 #include로 가져올지 import로 가져올지, 아니면 사용자가 설정할 수 있게 둘지를 결정해야 합니다. 제 현재 생각으로는 모듈 버전은 항상 import를 사용해야 하고, 조합 폭발을 피하기 위해 스위치는 제공하지 않는 편이 낫습니다.
그다음으로 저는 제 렌더러 라이브러리에서 이중 빌드를 지원해보려 했는데, 결코 사소한 일이 아니었습니다.
먼저, 앞서 말했듯이 모듈 모드에서 빌드/파싱할 때는 include를 import로 전환해야 합니다. 보통은 define 하나를 추가하고 각 #include 지시문 주변에서 약간의 춤을 춰야 한다는 뜻입니다:
#ifndef RENDERER_MODULE
#include <array>
#include <utility>
#include <vector>
#else
import std;
#endif
헤더 하나로만 이루어진 라이브러리라면 아주 최악은 아닙니다. 하지만 여러 .cpp와 .h 파일로 이루어진 더 복잡한 라이브러리라면, 조금 보물찾기 같은 일이 됩니다. 제 현재 POC 브랜치에서는 결국 모든 #include 지시문을 뜯어내서, 모듈 경로와 비모듈 경로 사이에서 켜고 끌 수 있는 하나의 파일에 몰아넣었습니다. 그 결과 모듈을 쓰지 않을 때 빌드가 더 느려졌습니다. 이제 모든 번역 단위가 자기에게는 필요도 없는 헤더들을 한 무더기씩 끌어오고 있기 때문입니다. 특히 <filesystem>, 너 말입니다 😠.
그리고 또 하나 처리해야 할 사실이 있습니다. module 지시문은 #ifdef로 제거할 수 없습니다. 설계상 그렇습니다. 왜 그런지는 저는 확실히 모르겠지만, 표준상 분명한 하드 에러입니다. 즉, .cpp 구현 파일이 있다면, #ifdef 같은 것으로 그것을 조건부로 모듈의 일부로 선언할 수 없습니다. 그러면 선택지는 세 가지뿐입니다. 해킹 하나, 또 다른 해킹 하나, 아니면 라이브러리를 항상 모듈로 빌드하는 것입니다.
첫 번째 해킹부터 봅시다. 저는 마음에 들지 않지만, 사양에서 #ifdef를 제한하려는 시도의 허망함을 꽤 잘 보여줍니다. 왜냐하면 그 제한은 #include에는 적용되지 않기 때문입니다. 그래서 그냥 구현 파일을 복제해서 우회할 수 있습니다:
// device_module.cpp
module renderer;
#define RENDERER_MODULE
#include <device.cpp>
이건 모듈로 빌드하느냐 아니냐에 따라 다른 .cpp 파일 집합을 사용해야 함을 의미하고, 각 구현 파일마다 추가적인 글루 파일도 필요하지만, 어쨌든 동작은 합니다. 대안으로 Daniela Engert의 제안은, 모든 .cpp 파일의 개별 컴파일을 아예 포기하고 모듈 정의의 module :private; 섹션 안으로 #include 지시문을 통해 전부 끌어들이는 방식이었습니다:
export module renderer;
export {
#include <renderer/renderer.h>
}
module :private;
#include <renderer/bindless.cpp>
#include <renderer/buffer.cpp>
#include <renderer/command_buffer.cpp>
#include <renderer/device.cpp>
// ...
여기서 어떤 분들은 “하지만 그러면 모든 구현이 unity build처럼 하나의 번역 단위에 들어가잖아”라고 اعتراض할지도 모릅니다. 맞는 말입니다. 그래서 저도 그 해법은 되도록 쓰고 싶지 않습니다. 저는 과거에 unity build를 다뤄본 적이 있고, 여전히 그것을 static과 namespace {}에 대한 전통적인 기대를 깨뜨리는 해킹이라고 생각합니다.
그래서 저는 대신 제 라이브러리를 항상 모듈로 빌드하기로 했습니다. 그러면 .cpp 파일 안에 module 선언을 문제 없이 둘 수 있습니다. 여기서 비결은 C++20의 extern "C++"를 사용하는 것입니다. extern "C"로 선언된 이름이 하위 호환 가능한 C 링크 규약과 이름 맹글링을 사용하는 것과 같은 방식으로, export {} 선언을 extern "C++"로 감싸면 #include 선언과 ABI 호환되는 심볼을 생성합니다. 모듈의 기본 동작은 모든 심볼에 모듈 이름을 장식해서 붙이는 것인데, 그러면 비모듈 문맥에서는 링커가 그 심볼을 찾을 수 없게 됩니다.
export module renderer;
// 비모듈 include와의 하위 호환성을 위해 모듈로 맹글링하지 않음
extern "C++"
{
export {
#include "renderer/renderer.h"
}
}
이렇게 하면 라이브러리는 import를 쓰는 소비자와 #include를 쓰는 소비자에 따라 다르게 빌드할 필요가 없습니다. 물론 이 문제는 내보내는 심볼을 생산하는 라이브러리에만 해당합니다. 헤더 전용 라이브러리라면 이 점을 신경 쓸 필요가 없습니다.
빌드가 하나뿐이면, 라이브러리 자체는 더 이상 자기 자신의 #include 변형 경로를 실행해보지 않게 됩니다. 따라서 두 경로를 모두 지원하는 동안에는, 라이브러리를 import와 #include 양쪽 방식으로 사용하는 테스트를 몇 개 유지하는 것이 좋습니다. 모듈 채택률을 생각하면, 그 기간은 꽤 길어질 것 같습니다.
모듈로 전환하는 데는 초기에 큰 비용이 듭니다. 모든 의존성을 모듈로 바꿔야 하는데, 그건 적잖은 작업이고, 안타깝게도 현재로서는 라이브러리 유지보수자들의 지원도 거의 없습니다. 심지어 모듈을 사용한다고 보고하는 사람들조차, 지금 시점에서는 서드파티 라이브러리의 포크를 사용하고 있는 것처럼 보입니다. 왜 패치를 기여하거나 유지할 생각이 없었던 건지, 아니면 패치를 제출했는데 거절당한 건지는 모르겠지만, 그다지 고무적인 상황은 아닙니다. Meeting C++의 설문조사도 6년 된 기능치고는 높은 채택률을 보여주지 않습니다. 닭이 먼저냐 달걀이 먼저냐 같은 문제일 수도 있습니다. 라이브러리 지원이 없어서 아무도 모듈로 옮기지 않고, 모듈 사용자가 없어서 라이브러리 유지보수자도 신경 쓰지 않는 것이죠.
저는 제가 쓰는 라이브러리들에 패치를 기여하는 것을 고려 중이지만, 솔직히 이 글을 쓰고 난 지금도 여전히 약간의 사기꾼 증후군을 느끼고, 제 기여가 과연 괜찮을지 스스로 의문이 듭니다. 모듈에 관한 전문성, 경험, 문헌이 너무 부족해서, 무엇이 관행이고 무엇이 아닌지가 분명하지 않습니다. 저는 새 키워드들의 요점을 대부분 시행착오로 파악했습니다. 그래서 대부분의 프로젝트에는, 제안된 패치가 좋은지 판단할 수 있는 자격 있는 리뷰어가 없을 것 같다는 생각이 듭니다.
그동안의 손쉬운 탈출구는, 처음 제가 VulkanHpp에 대해 했던 것처럼, 파싱 비용이 크지만 #include/import 경로에서 마지막에 두기 쉬운 라이브러리에만 모듈 사용을 제한해서 빠른 이득만 취하는 것입니다. 하지만 안타깝게도 규모가 커지면 전염성 문제 때문에 금방 한계에 부딪힙니다.
추신: Jens Weller가 Are We Modules Yet?라는 사이트의 존재를 알려주었는데, 어떤 프로젝트가 모듈을 제공하는지 목록으로 보여줍니다. 재미있게도 fastgltf는 모듈을 제공합니다. 다만 vckpg가 그것을 빌드하거나 설치하지 않기 때문에 제가 몰랐던 것뿐입니다. 제 생각에는 라이브러리들은 모듈 정의를 빌드 설정 뒤에 숨기기보다는 항상 설치 목록에 포함해야 합니다. 그래야 이게 패키지 관리자 문제가 되지 않습니다.