C++ 기존 프로젝트를 Module로 전환 | 단계별 마이그레이션 [#24-2]

들어가며: 헤더 기반 프로젝트를 모듈로 옮기려는 이유

헤더 기반 프로젝트에서 utils.h 하나를 고치면, 그 헤더를 직접 또는 간접으로 include하는 모든 .cpp가 다시 컴파일됩니다. 각 .cpp는 <vector>, <string> 같은 표준 헤더까지 포함해 수만 줄을 매번 처음부터 파싱하고, 헤더가 정의한 매크로는 include하는 모든 파일로 새어 나가 include 순서에 따라 동작이 달라지기도 합니다.

C++20 모듈은 인터페이스를 한 번 컴파일해 BMI(Binary Module Interface)로 저장하고, import하는 쪽이 그것을 읽게 해 이 문제를 줄입니다. 하지만 기존 프로젝트를 한 번에 모듈로 바꾸는 것은 현실적이지 않습니다. 외부 라이브러리는 대부분 헤더로만 제공되고, 컴파일러와 빌드 도구의 지원도 아직 고르지 않기 때문입니다. 이 글은 의존성이 적은 코드부터 한 레이어씩 옮기면서 헤더와 모듈을 공존시키는 절차를 다룹니다. export module, import의 기본 문법은 C++20 모듈 기초(#24-1)에서 다뤘습니다.

CMake로 모듈을 빌드하려면 CMake 3.28 이상과, CMake가 모듈 의존성 스캔을 지원하는 컴파일러(GCC 14 이상, Clang 16 이상, MSVC 2022 17.4 이상)가 필요합니다. 컴파일러 자체는 그 이전 버전부터 모듈을 부분적으로 지원했지만, 빌드 시스템과 함께 쓰기에는 위 버전부터가 실용적입니다.


헤더 기반 프로젝트의 한계와 전환 전략

flowchart TB
  subgraph header[헤더 기반 빌드]
    H1[utils.h 수정] --> H2[include 1]
    H1 --> H3[include 2]
    H1 --> H4[include N]
    H2 --> H5[각 TU가 헤더를 다시 파싱]
    H3 --> H5
    H4 --> H5
  end
  subgraph module[모듈 기반 빌드]
    M1[utils 인터페이스 수정] --> M2[BMI 한 번 재생성]
    M2 --> M3[import하는 TU 재컴파일, 텍스트 파싱 없음]
  end
항목헤더 (#include)모듈 (import)
파싱TU마다 헤더 텍스트를 다시 파싱인터페이스를 한 번 컴파일해 BMI 재사용
매크로include하는 쪽으로 전파모듈 밖으로 전파되지 않음
순서 의존include 순서에 따라 결과가 달라질 수 있음import 순서와 무관
캡슐화헤더에 있는 것은 모두 보임export한 것만 보임

전환 방향은 두 가지입니다. 다른 코드에 의존하지 않는 유틸리티와 기본 타입부터 모듈로 만들고 그것을 쓰는 상위 레이어로 올라가는 “바닥부터” 방식과, 기존 라이브러리는 그대로 두고 새로 작성하는 코드만 모듈로 쓰는 “새 코드부터” 방식입니다. 대부분의 프로젝트는 두 방식을 섞습니다. 어느 쪽이든 한 디렉터리나 한 라이브러리를 하나의 모듈(필요하면 여러 파티션)로 묶고, 그 모듈을 쓰는 쪽을 import로 바꾸는 것을 한 단위로 진행합니다.

모듈이 해결하는 것과 해결하지 않는 것

여러 파일이 공통으로 include하는 common.h를 모듈로 옮기면, 그 인터페이스를 고쳤을 때 import하는 TU들은 여전히 다시 컴파일되지만 각 TU가 헤더 텍스트를 파싱하는 비용은 사라집니다. 인터페이스는 그대로 두고 구현 유닛만 고쳤다면 import하는 쪽은 아예 다시 컴파일되지 않습니다.

매크로 오염도 줄어듭니다. 모듈 안에서 정의한 매크로는 import하는 쪽으로 전파되지 않으므로, min/max 같은 매크로가 다른 파일의 코드를 깨뜨리는 일이 없습니다. 반대로 말하면, 설정 매크로를 공유하던 코드는 매크로를 constexpr 변수 등으로 바꿔야 합니다.

반면 순환 의존은 해결되지 않습니다. 헤더에서는 전방 선언으로 a.h와 b.h의 순환을 우회할 수 있지만, 모듈 사이의 순환 import는 아예 허용되지 않습니다. 모듈 A가 B를 import하고 B가 A를 import하면 컴파일 에러입니다. 그래서 순환 include가 있는 코드는 모듈로 옮기기 전에 공통 부분을 별도 모듈로 분리하는 리팩터링이 먼저 필요합니다. 같은 모듈 안의 파일끼리라면 파티션 사이에서 서로 다른 파티션의 선언을 쓸 수 있으므로, 강하게 얽힌 두 헤더는 한 모듈의 파티션으로 묶는 방법도 있습니다.


단계별 마이그레이션

Step 1: 의존성이 적은 유틸부터 모듈로 작성

Before (헤더):

// mylib/utils.h
#ifndef MYLIB_UTILS_H
#define MYLIB_UTILS_H
int add(int a, int b);
double clamp(double v, double lo, double hi);
#endif
// mylib/utils.cpp
#include "utils.h"
int add(int a, int b) { return a + b; }
double clamp(double v, double lo, double hi) {
    if (v < lo) return lo;
    if (v > hi) return hi;
    return v;
}

After (모듈):

// mylib/utils.cppm (모듈 인터페이스 유닛)
export module mylib.utils;
export int add(int a, int b);
export double clamp(double v, double lo, double hi);
// mylib/utils_impl.cpp (모듈 구현 유닛)
module mylib.utils;
int add(int a, int b) { return a + b; }
double clamp(double v, double lo, double hi) {
    if (v < lo) return lo;
    if (v > hi) return hi;
    return v;
}

모듈 이름의 점(.)은 계층을 나타내는 관례일 뿐 언어상 의미는 없습니다. mylib.utils와 mylib는 서로 무관한 별개의 모듈입니다.

Step 2: 사용하는 쪽을 import로 전환

// app/main.cpp (Before)
#include "mylib/utils.h"
int main() { return add(3, 5) == 8 ? 0 : 1; }
// app/main.cpp (After)
import mylib.utils;
int main() { return add(3, 5) == 8 ? 0 : 1; }

Step 3: 헤더를 include해야 하는 모듈 코드 (global module fragment)

모듈 코드 안에서 <cmath> 같은 헤더가 필요할 때 가장 많이 하는 실수는 module 선언 뒤에 #include를 두는 것입니다.

// 잘못된 예
module engine.math.vector3;
#include <cmath>   // <cmath>의 모든 선언이 이 모듈에 소속(attach)되어 버림

module 선언 뒤에 include한 헤더의 선언들은 모두 그 모듈에 속한 것으로 취급됩니다. 그러면 같은 std::sqrt가 다른 TU에서는 전역 모듈 소속, 여기서는 engine.math.vector3 소속이 되어 재정의 에러나 링크 에러가 나고, 컴파일러에 따라서는 아예 거부합니다. 헤더는 반드시 모듈 선언 앞의 global module fragment에 둡니다.

// 올바른 예
module;                      // global module fragment 시작
#include <cmath>             // 여기서 include한 것은 전역 모듈 소속으로 남음
module engine.math.vector3;  // 이 줄부터 모듈 본문

global module fragment에는 전처리 지시문만 올 수 있습니다. 인터페이스 유닛도 마찬가지로 module; → #include → export module 이름; 순서로 씁니다.

Step 4: 여러 헤더를 하나의 모듈로 묶기 (파티션)

a.h, b.h를 각각 별도 모듈로 만들지 않고 mylib 하나로 묶으려면 파티션을 씁니다.

// mylib.cppm (주 인터페이스 유닛)
export module mylib;
export import :part_a;   // 파티션을 다시 export
export import :part_b;
// part_a.cppm (인터페이스 파티션)
export module mylib:part_a;
export class PartA {};
// part_b.cppm
export module mylib:part_b;
export class PartB {};

export 선언을 담는 파티션은 export module mylib:part_a;로 선언하는 인터페이스 파티션이어야 합니다. module mylib:part_a;처럼 export 없이 선언하면 구현 파티션이 되어 그 안에서 export를 쓸 수 없습니다. 또 주 인터페이스 유닛은 모든 인터페이스 파티션을 직접 또는 간접으로 export import해야 합니다. 파티션은 같은 모듈 안에서만 import할 수 있고, 외부에서는 import mylib; 한 번으로 PartA와 PartB를 모두 씁니다.

Step 5: export 범위 정하기

기존에 공개 헤더로 제공하던 것만 export하고, 내부용 함수와 타입은 export하지 않습니다.

// mylib.cppm
export module mylib;

export {
    int public_api();
    class PublicClass {};
}

namespace detail {
    int internal_helper() { return 42; }   // export 없음: 모듈 내부에서만 사용
}

export하지 않은 이름은 import하는 쪽에서 이름으로 찾을 수 없습니다. 다만 export한 인라인 함수나 템플릿이 내부 함수를 호출하는 것은 문제없이 동작합니다.

Step 6: include와 import 공존

같은 파일에서 헤더와 모듈을 함께 쓴다면 #include를 먼저, import를 나중에 두는 편이 안전합니다.

#include <iostream>
#include "legacy_config.h"
import mylib.utils;

int main() {
    std::cout << add(1, 2) << "\n";
}

모듈이 global module fragment에서 <string> 등을 include해 두었는데, import한 뒤에 같은 헤더를 다시 include하면 컴파일러가 이미 BMI에서 읽어 온 정의와 텍스트로 다시 들어온 정의를 병합해야 합니다. 이 병합은 컴파일러마다 지원 수준이 달라, 일부 버전에서는 재정의 에러가 납니다. MSVC 문서도 표준 라이브러리 헤더와 모듈을 섞을 때 include를 먼저 두도록 권장합니다.


math·utils 모듈로 옮긴 예제 프로젝트

game_engine/
├── modules/
│   ├── math/
│   │   ├── vector3.cppm       # export module engine.math.vector3;
│   │   └── vector3_impl.cpp
│   └── utils/
│       ├── logger.cppm
│       └── logger_impl.cpp
├── src/
│   └── main.cpp
└── CMakeLists.txt
// modules/math/vector3.cppm
export module engine.math.vector3;

export struct Vector3 {
    double x, y, z;
    Vector3(double x = 0, double y = 0, double z = 0);
    Vector3 operator+(const Vector3& other) const;
    double length() const;
};
// modules/math/vector3_impl.cpp
module;
#include <cmath>
module engine.math.vector3;

Vector3::Vector3(double x, double y, double z) : x(x), y(y), z(z) {}

Vector3 Vector3::operator+(const Vector3& other) const {
    return Vector3(x + other.x, y + other.y, z + other.z);
}

double Vector3::length() const {
    return std::sqrt(x * x + y * y + z * z);
}
// modules/utils/logger.cppm
export module engine.utils.logger;
export void log_info(const char* msg);
export void log_error(const char* msg);
// modules/utils/logger_impl.cpp
module;
#include <iostream>
module engine.utils.logger;

void log_info(const char* msg) { std::cout << "[INFO] " << msg << "\n"; }
void log_error(const char* msg) { std::cerr << "[ERROR] " << msg << "\n"; }
// src/main.cpp
#include <iostream>
import engine.math.vector3;
import engine.utils.logger;

int main() {
    Vector3 v(1, 2, 3);
    Vector3 w = v + Vector3(0, 1, 0);
    log_info("Vector length");
    std::cout << "length = " << w.length() << "\n";
}

CMake로 모듈 프로젝트 빌드하기

cmake_minimum_required(VERSION 3.28)
project(GameEngine LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_library(engine_math STATIC modules/math/vector3_impl.cpp)
target_sources(engine_math PUBLIC
    FILE_SET CXX_MODULES FILES modules/math/vector3.cppm
)

add_library(engine_utils STATIC modules/utils/logger_impl.cpp)
target_sources(engine_utils PUBLIC
    FILE_SET CXX_MODULES FILES modules/utils/logger.cppm
)

add_executable(game_engine src/main.cpp)
target_link_libraries(game_engine PRIVATE engine_math engine_utils)

모듈 인터페이스 파일(.cppm, .ixx)은 FILE_SET CXX_MODULES에만 등록하고, 일반 소스 목록에는 구현 유닛만 둡니다. CMake는 빌드 전에 각 소스를 스캔해(P1689 형식) 어떤 파일이 어떤 모듈을 제공하고 import하는지 알아낸 뒤 컴파일 순서를 정합니다. 그래서 engine_math의 BMI가 먼저 만들어지고 main.cpp는 그 뒤에 컴파일됩니다. 모듈을 import하는 타겟은 그 모듈을 제공하는 타겟을 target_link_libraries로 연결해야 BMI를 찾을 수 있습니다.

모듈을 지원하는 생성기는 Ninja(1.11 이상), Ninja Multi-Config, Visual Studio 17 2022(17.4 이상)입니다. Makefile 생성기는 지원하지 않습니다. MSVC는 관례상 .ixx를 인터페이스 확장자로 쓰지만, CMake가 CXX_MODULES 파일 집합의 파일을 인터페이스로 넘겨 주므로 .cppm을 그대로 써도 됩니다.

# Linux/macOS
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
./build/game_engine

# Windows (Visual Studio)
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
.\build\Release\game_engine.exe

C++20을 쓰는 타겟은 CMake가 기본으로 모듈 스캔을 수행합니다. 아직 모듈을 쓰지 않는 타겟에서 스캔 비용을 없애려면 set(CMAKE_CXX_SCAN_FOR_MODULES OFF)나 타겟 속성 CXX_SCAN_FOR_MODULES를 끕니다.

표준 라이브러리 모듈 (import std)

import std;는 C++23 기능으로, 컴파일러와 표준 라이브러리, 빌드 도구가 모두 지원해야 합니다. GCC 15(libstdc++), Clang 18 이상(libc++), MSVC(도구 집합 14.36 이상)가 지원하며, CMake에서는 3.30 이상에서 실험 기능 플래그와 함께 CXX_MODULE_STD 속성으로 켭니다. 지원 조합이 아직 좁으므로, 전환 초기에는 표준 라이브러리는 global module fragment에서 include하고 자체 코드만 모듈화하는 편이 안전합니다.


모듈 인터페이스 유닛 vs 구현 유닛

구분선언역할변경 시 영향
주 인터페이스 유닛export module M;모듈의 공개 API, BMI 생성import하는 모든 TU 재컴파일
인터페이스 파티션export module M:part;인터페이스를 여러 파일로 분할주 인터페이스를 거쳐 import하는 TU 재컴파일
구현 파티션module M:part;모듈 내부 전용 선언 공유해당 파티션을 import한 모듈 내부 파일만
구현 유닛module M;export한 선언의 정의해당 파일만 재컴파일
flowchart LR
  subgraph interface[인터페이스 유닛]
    I1[math.cppm] --> I2[export 선언]
    I2 --> I3[BMI 생성]
  end
  subgraph impl[구현 유닛]
    J1[math_impl.cpp] --> J2[module math;]
    J2 --> J3[함수 정의]
  end
  I3 --> J1

구현 유닛은 자기 모듈의 인터페이스를 암묵적으로 import하므로, 인터페이스에서 선언한 모든 것(export 여부와 무관)을 볼 수 있습니다. 구현 유닛에서는 export를 쓸 수 없습니다. 증분 빌드 이득을 얻으려면 인라인이 필요 없는 함수 본문은 구현 유닛에 두어, 구현을 고쳐도 BMI가 바뀌지 않게 하는 것이 핵심입니다. 인터페이스 유닛 안에 함수 본문을 함께 쓰면 본문만 고쳐도 BMI가 바뀌어 import하는 TU가 모두 다시 컴파일될 수 있습니다.


빌드 시간은 어디서 달라지나

시나리오헤더 기반모듈 기반
클린 빌드모든 TU가 헤더를 각자 파싱BMI를 한 번 만들고 재사용하지만, BMI가 만들어질 때까지 import하는 TU가 기다려야 해서 병렬성이 줄 수 있음
구현 파일만 수정그 .cpp만 재컴파일그 구현 유닛만 재컴파일
인터페이스 수정include하는 모든 TU가 헤더를 다시 파싱하며 재컴파일BMI 재생성 후 import하는 모든 TU 재컴파일, 다만 TU마다 텍스트를 파싱하지 않음

이득이 가장 큰 곳은 무거운 헤더를 많은 TU가 공유하던 경우입니다. 클린 빌드는 의존 체인이 길면 오히려 느려질 수 있는데, 헤더 방식에서는 모든 .cpp를 동시에 컴파일할 수 있지만 모듈 방식에서는 BMI 생성 순서가 정해져 있어 병렬 실행이 제한되기 때문입니다. 템플릿 인스턴스화는 사용하는 TU에서 여전히 일어나므로, 템플릿이 많은 코드에서는 파싱 비용이 줄어든 만큼만 빨라집니다.

모듈 지원은 컴파일러마다 아직 성숙해 가는 중이라, 버전에 따라 BMI 읽기가 기대보다 느리거나 버그가 있을 수 있습니다. 전환 효과는 대표적인 타겟 하나를 옮겨 클린 빌드와 증분 빌드 시간을 직접 비교해 보고 판단하는 것이 정확합니다. 바이너리 크기는 거의 달라지지 않습니다. BMI는 빌드 중간 산출물일 뿐 실행 파일에 들어가지 않고, 인라인과 템플릿 인스턴스화 규칙도 그대로입니다.


마이그레이션 중 자주 만나는 오류

매크로가 모듈 밖에서 안 보임

모듈은 매크로를 export하지 않습니다. 설정값을 매크로로 공유하던 코드는 constexpr 변수로 바꾸고, 조건부 컴파일처럼 꼭 매크로가 필요한 부분은 작은 헤더로 남겨 두고 따로 include합니다.

// 매크로 대신 constexpr
export module config;
export constexpr int MAX_SIZE = 1024;
// 매크로가 꼭 필요하면 헤더로 분리해 import와 별도로 include
#include "config_macros.h"
import mylib;

내부 헤더가 매크로·include 순서에 의존함

내부 구현용 헤더가 매크로나 include 순서에 기대고 있다면 모듈로 옮기기 어렵습니다. 이런 헤더는 당분간 그대로 두고, 그것을 쓰는 모듈의 global module fragment에서 include합니다.

module;
#include "mylib/internal_detail.h"   // 전역 모듈 소속으로 남고, export되지 않음
export module mylib;
export int public_api();

import 후 선언을 찾을 수 없음

import mylib; 뒤에 add 같은 이름을 찾을 수 없다면, 해당 선언을 export하지 않았거나 다른 모듈 이름을 import한 경우가 대부분입니다. 빌드 단계에서 “module not found” 류의 에러가 난다면, 모듈을 제공하는 타겟을 target_link_libraries로 연결하지 않아 CMake가 BMI 경로를 넘겨주지 못한 것입니다.

구현 유닛에서 export 사용

module math; 파일에서 export를 쓰면 컴파일 에러입니다. export할 선언은 모두 인터페이스 유닛(또는 인터페이스 파티션)에 두고, 구현 유닛에는 정의만 둡니다.

같은 모듈의 인터페이스가 두 개

export module foo;를 가진 파일이 두 개이거나, 같은 인터페이스 파일이 두 타겟에 들어가면 모듈 재정의 에러가 납니다. 모듈당 주 인터페이스 유닛은 하나여야 하고, 인터페이스 파일은 한 타겟의 FILE_SET CXX_MODULES에만 등록합니다.

공개 모듈이 비공개 모듈을 import

FILE_SET CXX_MODULES를 PRIVATE으로 등록한 모듈은 다른 타겟에 BMI가 제공되지 않습니다. 그런데 PUBLIC 인터페이스 유닛이 그 모듈을 import하면, 이 인터페이스를 import하는 소비자 쪽에서 비공개 모듈의 BMI를 찾을 수 없어 빌드가 실패합니다. 내부용 모듈은 구현 유닛에서만 import합니다.

// mylib.cppm (PUBLIC 인터페이스)
export module mylib;
export int foo();

// mylib_impl.cpp (구현 유닛)
module mylib;
import mylib.internal;   // 내부 모듈은 구현 유닛에서만
int foo() { return internal_helper(); }

기존 사용자를 깨지 않고 옮기는 패턴

헤더와 모듈을 동시에 제공하기

모든 사용자를 한 번에 import로 바꿀 수 없다면, 당분간 헤더와 모듈을 둘 다 제공해야 합니다. 이때 같은 함수를 헤더와 모듈 인터페이스에 각각 따로 선언하면 안 됩니다. 모듈 안에서 선언한 이름은 그 모듈에 소속되어, 헤더의 전역 선언과는 다른 엔티티가 됩니다. 같은 프로그램에서 두 선언을 함께 쓰면 링크 에러나 정의되지 않은 동작으로 이어지고, MSVC처럼 모듈 소속을 이름에 반영하는 컴파일러에서는 링크 단계에서 바로 드러납니다.

안전한 방법은 헤더를 그대로 유지하고, 모듈 인터페이스가 그 헤더를 global module fragment에서 include한 뒤 using 선언으로 다시 export하는 것입니다.

// mylib/utils.cppm
module;
#include "mylib/utils.h"     // 기존 헤더: 선언은 전역 모듈 소속
export module mylib.utils;
export using ::add;          // 같은 엔티티를 모듈 이름으로도 노출
export using ::clamp;

이렇게 하면 #include "mylib/utils.h"를 쓰는 기존 코드와 import mylib.utils;를 쓰는 새 코드가 같은 함수를 가리킵니다. 구현은 기존 utils.cpp에 그대로 두면 됩니다. 사용자가 모두 import로 옮긴 뒤에 선언을 모듈 안으로 옮기고 헤더를 지웁니다.

외부 라이브러리 래퍼 모듈

nlohmann/json 같은 헤더 전용 라이브러리도 같은 방식으로 감싸 프로젝트 안에서는 import만 쓰게 할 수 있습니다.

// wrappers/json.cppm
module;
#include <nlohmann/json.hpp>
export module wrappers.json;

export namespace nlohmann {
    using nlohmann::json;
}

헤더를 여러 TU에서 반복 파싱하는 대신 래퍼 모듈에서 한 번만 파싱합니다. 다만 템플릿이 많은 라이브러리는 컴파일러에 따라 BMI로 옮겼을 때 에러가 나거나 기대만큼 빨라지지 않는 경우가 있어, 적용 전에 직접 확인해야 합니다. 일부 라이브러리는 이미 공식 모듈 인터페이스 파일을 함께 배포하기 시작했으므로 그것이 있는지 먼저 찾아봅니다.

PCH와의 병행

이미 PCH로 빌드 시간을 줄이고 있는 프로젝트라면, 모듈 전환 중에도 PCH를 유지할 수 있습니다. MSVC는 PCH와 모듈을 같은 TU에서 함께 쓰는 것을 지원하지만, GCC와 Clang에서는 조합에 제약이 있으므로 사용하는 컴파일러 문서를 확인합니다. 모듈화가 진행되어 PCH에 넣던 헤더들이 global module fragment로 옮겨 가면 PCH는 점차 필요 없어집니다.

테스트 전용 모듈

테스트 픽스처를 모듈로 만들어 테스트 실행 파일에만 링크하면, 프로덕션 빌드에는 포함되지 않습니다.

add_library(engine_test_helpers STATIC)
target_sources(engine_test_helpers PUBLIC
    FILE_SET CXX_MODULES FILES test/helpers.cppm
)
target_link_libraries(engine_test_helpers PUBLIC engine_math)

add_executable(engine_tests test/main.cpp)
target_link_libraries(engine_tests PRIVATE engine_math engine_test_helpers)

팀 단위 전환 순서

먼저 C++20을 켜고 CI의 컴파일러·CMake·Ninja 버전을 모듈 지원 버전 이상으로 고정합니다. 모듈 지원 상태가 컴파일러마다 다르므로, 팀이 쓰는 모든 컴파일러에서 같은 코드가 빌드되는지 CI로 확인해야 합니다. 다음으로 의존성 그래프에서 리프에 해당하는 유틸리티 한두 개를 골라 파일럿으로 옮기고, 빌드 시간과 IDE·디버거·정적 분석 도구가 모듈 코드를 제대로 다루는지 확인합니다. 도구 지원이 부족하다면 이 단계에서 드러나므로, 범위를 넓히기 전에 판단할 수 있습니다.

파일럿이 문제없으면 core → math, utils → app처럼 의존성 그래프의 아래쪽부터 위로 넓혀 갑니다. 진행하는 동안에는 어떤 라이브러리가 모듈로 제공되고 어떤 것은 아직 헤더인지, include와 import를 섞을 때의 순서 규칙을 팀 문서로 정리해 두면 혼란이 줄어듭니다. 새 코드는 기본으로 모듈을 쓰고, 레거시 헤더는 앞에서 본 using 재export 방식으로 유지하다가 사용처가 사라지면 제거합니다.


참고 자료

같이 보면 좋은 글