C++ 패키지 관리 실무: vcpkg와 Conan으로 외부 라이브러리 의존성 지옥 탈출

들어가며: “이 라이브러리 어떻게 넣지?”

CMake와 패키지 매니저 개요를 다뤘다면, 40번 시리즈는 프로젝트 전체 생명 주기를 자동화합니다. 그 첫 단계가 외부 라이브러리입니다. 라이브러리를 수동으로 내려받아 빌드하고 include 경로를 맞추다 보면 팀원마다 환경이 달라지고 버전이 꼬입니다.

vcpkg와 Conan은 필요한 패키지와 버전을 선언하면 빌드 설정에 맞게 가져와서 경로를 잡아 주는 도구입니다. vcpkg는 Microsoft가 주도하며 CMake와 긴밀하게 통합되고, Conan은 프로필 기반으로 여러 컴파일러와 플랫폼 조합을 다루는 데 강점이 있습니다. 둘 다 같은 소스와 설정이면 어디서나 같은 결과를 얻는 재현 가능한 빌드의 기반이 됩니다.

같은 문제를 다른 생태계에서는 Python pip·uv·Poetry·npm·Go 모듈·Rust Cargo가 풀고, C++에서는 빌드 생성기로 CMake가 자주 쓰입니다. 전체 그림은 빌드 시스템 비교, 개별 심화는 vcpkg·Conan 글을 이어서 보세요. 이 글에서는 vcpkg의 Manifest 모드·triplet·CMake 연동과 Conan의 conanfile·프로필을 같은 프로젝트 기준으로 비교하고, 자주 만나는 에러와 CI 구성 패턴을 살펴봅니다.


의존성 지옥: 설치 경로·버전·플랫폼 불일치

라이브러리를 수동으로 관리할 때 생기는 문제는 대체로 네 가지로 나뉩니다. 팀원마다 설치 경로가 달라(/usr/local과 /opt 등) find_package(Boost)가 어떤 PC에서만 실패합니다. 개발·CI·운영 환경의 라이브러리 버전이 달라, 로컬에서는 spdlog 새 버전으로 빌드되던 코드가 배포 서버의 옛 버전에서 깨집니다. Windows·Linux·macOS마다 빌드 설정이 달라 한 플랫폼에서만 빌드가 됩니다. 그리고 A가 B를, B가 C를 요구하는 의존성 트리를 손으로 맞추기가 어렵습니다.

프로젝트 → A → C
         → D → C

위처럼 A와 D가 각각 C를 요구하는데 버전이 다르면 어느 C를 쓸지 정해야 합니다. 패키지 매니저는 이 해결을 규칙에 따라 자동으로 수행하고, 규칙으로 풀 수 없으면 에러로 알려 줍니다.

이 문제들은 새 팀원 온보딩과 CI에서 가장 먼저 드러납니다. 로컬에만 설치된 라이브러리에 의존하던 프로젝트는 새 PC나 깨끗한 CI 러너에서 바로 실패하고, 무엇을 어디서 받아야 하는지를 사람에게 물어봐야 합니다. vcpkg.json이나 conanfile.txt에 필요한 패키지와 버전만 적어 두면, 도구가 다운로드·빌드·경로 설정을 대신하고 이 파일이 코드와 함께 버전 관리됩니다.

flowchart LR
  subgraph before[수동 관리]
    B1[개발자A] --> B2[경로A]
    B3[개발자B] --> B4[경로B]
    B5[CI] --> B6[경로C]
    B2 -.->|불일치| B4
    B4 -.->|불일치| B6
  end
  subgraph after[패키지 매니저]
    A1[vcpkg.json] --> A2[동일 의존성]
    A2 --> A3[개발자A]
    A2 --> A4[개발자B]
    A2 --> A5[CI]
  end

다만 패키지 매니저를 써도 triplet이나 프로필(OS, 아키텍처, 컴파일러) 차이는 남으므로, CI가 쓰는 triplet과 프로필은 문서로 남겨 두어야 합니다.


vcpkg 사용하기

설치

# Linux/macOS: vcpkg 클론 후 bootstrap
git clone https://github.com/microsoft/vcpkg.git
cd vcpkg
./bootstrap-vcpkg.sh
# Windows (PowerShell)
git clone https://github.com/microsoft/vcpkg.git
cd vcpkg
.\bootstrap-vcpkg.bat

bootstrap이 끝나면 vcpkg 실행 파일이 ./vcpkg(Unix) 또는 vcpkg.exe(Windows)로 생성됩니다. PATH에 추가하거나 절대 경로로 사용합니다. Linux에서는 bootstrap에 curl, zip, unzip, tar가 필요하고, 많은 포트가 pkg-config도 요구합니다.

Manifest 모드 (권장)

Manifest 모드에서는 프로젝트 루트에 vcpkg.json을 두고 의존성을 선언합니다. CMake configure 단계에서 vcpkg가 해당 패키지들을 빌드·설치하며, 프로젝트가 필요로 하는 패키지 목록이 코드와 함께 버전 관리됩니다.

프로젝트 구조 예시

my-app/
├── CMakeLists.txt
├── vcpkg.json          # 의존성 선언
└── src/
    └── main.cpp

vcpkg.json

{
  "name": "my-app",
  "version": "1.0.0",
  "description": "vcpkg Manifest 모드 예제",
  "dependencies": [
    "fmt",
    "spdlog",
    {
      "name": "openssl",
      "version>=": "3.0.0"
    }
  ],
  "builtin-baseline": "<vcpkg 저장소의 40자리 커밋 SHA>"
}

dependencies의 문자열 항목은 baseline이 가리키는 버전을 사용하고, version>=는 최소 버전을 지정합니다. builtin-baseline은 vcpkg 저장소의 특정 커밋 해시(40자리 전체)이며, vcpkg x-update-baseline --add-initial-baseline으로 현재 커밋을 채울 수 있습니다.

baseline의 역할을 이해하면 vcpkg의 버전 선택이 예측 가능해집니다. vcpkg는 “최신 버전”을 가져오는 것이 아니라, baseline 커밋 시점에 각 포트가 가리키던 버전을 기준으로 삼고, version>= 제약이 그보다 높으면 그 제약을 만족하는 최소 버전으로 올립니다. 그래서 같은 vcpkg.json이라면 언제 어디서 빌드해도 같은 버전 조합이 나옵니다. 반대로 baseline 없이 manifest를 쓰면 로컬에 클론한 vcpkg 저장소의 현재 상태에 따라 버전이 달라지고, version>=를 쓰는 순간 baseline이 필요하다는 에러가 납니다.

CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
# vcpkg 툴체인: 반드시 project() 이전에 설정해야 적용됨
set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake"
    CACHE STRING "Vcpkg toolchain file")
project(my-app VERSION 1.0.0 LANGUAGES CXX)
add_executable(my-app src/main.cpp)
find_package(fmt CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
target_link_libraries(my-app PRIVATE
    fmt::fmt
    spdlog::spdlog
)
target_compile_features(my-app PRIVATE cxx_std_17)

CMAKE_TOOLCHAIN_FILE을 vcpkg의 vcpkg.cmake로 지정하면, find_package가 vcpkg가 설치한 경로를 자동으로 검색합니다.

툴체인 파일은 첫 project() 호출이 컴파일러를 감지할 때 읽히므로, project() 뒤에서 set(CMAKE_TOOLCHAIN_FILE ...)을 하면 아무 효과가 없고 find_package(fmt)가 “Could not find a package configuration file”로 실패합니다. 이미 한 번 configure한 빌드 디렉터리에서는 툴체인을 나중에 바꿔도 캐시 때문에 반영되지 않으므로, 설정을 바꾼 뒤에는 build/ 디렉터리를 지우고 다시 configure하는 것이 안전합니다. 명령줄의 -DCMAKE_TOOLCHAIN_FILE이나 CMakePresets.json에 두는 방식이 이런 순서 문제를 피하기 쉽습니다.

main.cpp

#include <spdlog/spdlog.h>
#include <fmt/core.h>
int main() {
    spdlog::info("vcpkg Manifest 모드로 빌드됨");
    spdlog::info("fmt 예제: {}", fmt::format("Hello, {}!", "vcpkg"));
    return 0;
}

빌드 절차

# 1. vcpkg를 프로젝트에 서브모듈로 추가한 경우
git submodule add https://github.com/microsoft/vcpkg.git vcpkg
./vcpkg/bootstrap-vcpkg.sh
# 2. CMake 설정 (이 단계에서 vcpkg가 의존성을 설치)
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE="$(pwd)/vcpkg/scripts/buildsystems/vcpkg.cmake"
# 3. 빌드
cmake --build build
./build/my-app

실행하면 [info] vcpkg Manifest 모드로 빌드됨 같은 로그가 출력됩니다. 첫 configure는 의존성을 소스에서 빌드하므로 오래 걸리고, 이후에는 바이너리 캐시(Linux 기준 ~/.cache/vcpkg/archives)에서 가져옵니다.

Triplet

triplet은 대상 아키텍처, OS, 링크 방식을 묶은 이름입니다.

Triplet설명
x64-windowsWindows 64비트, DLL
x64-windows-staticWindows 64비트, 정적 라이브러리와 정적 CRT(/MT)
x64-linuxLinux 64비트, 정적 라이브러리
arm64-osxmacOS Apple Silicon
x64-osxmacOS Intel
# 특정 triplet 사용
cmake -B build -S . \
  -DCMAKE_TOOLCHAIN_FILE="$(pwd)/vcpkg/scripts/buildsystems/vcpkg.cmake" \
  -DVCPKG_TARGET_TRIPLET=x64-windows-static

x64-windows-static은 CRT까지 정적으로 링크하므로, 이 triplet을 쓸 때는 프로젝트도 CMAKE_MSVC_RUNTIME_LIBRARY를 MultiThreaded$<$<CONFIG:Debug>:Debug>로 맞춰야 링크 에러가 나지 않습니다.

Classic 모드

Classic 모드에서는 vcpkg install로 vcpkg 디렉터리에 전역 설치합니다. 프로젝트마다 버전을 고정할 수 없어 재현성이 떨어지므로, 신규 프로젝트는 Manifest 모드를 권장합니다.

vcpkg install fmt spdlog
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake

vcpkg 오버레이 (사내 포트)

vcpkg 공식 레지스트리에 없는 라이브러리나 사내에서 수정한 포트를 쓸 때 오버레이 포트를 지정합니다.

cmake -B build -S . \
  -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake \
  -DVCPKG_OVERLAY_PORTS=/path/to/my-ports

오버레이 디렉터리 구조:

my-ports/
└── my-internal-lib/
    ├── portfile.cmake
    └── vcpkg.json

오버레이 포트는 같은 이름의 공식 포트보다 우선하며 버전 제약과 baseline의 영향을 받지 않습니다. 사내 포트가 많아지면 vcpkg-configuration.json에 git 레지스트리를 등록하는 방식이 버전 관리 면에서 더 낫습니다.

vcpkg 버전 제약 문법

{
  "dependencies": [
    "fmt",
    {
      "name": "spdlog",
      "version>=": "1.11.0"
    },
    {
      "name": "openssl",
      "version>=": "3.0.0",
      "platform": "!windows"
    }
  ],
  "overrides": [
    { "name": "fmt", "version": "10.1.1" }
  ]
}

version>=는 최소 버전이고, overrides는 최소 버전 규칙을 무시하고 해당 버전으로 정확히 고정합니다. platform은 특정 플랫폼에서만 의존성을 추가하며 !windows는 Windows를 제외한다는 뜻입니다.

vcpkg에는 version< 같은 상한 제약이 없습니다. npm이나 Cargo의 범위 지정에 익숙하면 “2.0 미만”을 쓰고 싶어지지만, vcpkg는 최소 버전(version>=)과 baseline, 그리고 정확한 고정(overrides)만으로 버전을 정합니다. 그래서 “이 라이브러리가 새 메이저 버전으로 올라가면 곤란하다”는 요구는 baseline을 올릴 때 변경 내역을 확인하거나 overrides로 고정하는 방식으로 풉니다. 규칙이 단순한 대신, 의존성 트리 전체에서 같은 라이브러리는 항상 하나의 버전만 쓰입니다. overrides로 fmt를 낮췄는데 baseline의 spdlog가 더 새 fmt를 요구한다면 spdlog 빌드가 실패할 수 있으므로, 고정한 조합은 실제로 빌드해 확인해야 합니다.


Conan 사용하기

설치

# Conan 2.x (가상환경이나 pipx 권장)
pip install conan
conan --version
# 처음 한 번: 현재 컴파일러를 감지해 기본 프로필 생성
conan profile detect

conanfile.txt (간단한 프로젝트)

conanfile.txt는 패키지 이름과 버전, 생성기만 나열하는 단순한 형식입니다.

프로젝트 구조

my-conan-app/
├── CMakeLists.txt
├── conanfile.txt
└── src/
    └── main.cpp

conanfile.txt

[requires]
fmt/10.1.1
spdlog/1.12.0
[generators]
CMakeDeps
CMakeToolchain

[requires]에는 이름/버전 형식으로 패키지를 적고, [generators]의 CMakeDeps는 find_package용 설정 파일을, CMakeToolchain은 conan_toolchain.cmake를 생성합니다. spdlog 레시피도 fmt를 요구하므로, 직접 적은 fmt 버전이 spdlog 레시피가 요구하는 버전과 다르면 Conan 2는 버전 충돌 에러를 냅니다(아래 에러 절 참고). conan graph info .로 실제 해결된 버전을 확인하고 맞춥니다.

의존성 설치 및 빌드

# 1. Conan 의존성 설치 (build/에 CMake 파일 생성)
conan install . --output-folder=build --build=missing
# 2. CMake 설정 (Conan 툴체인 사용, build_type은 프로필과 일치시킴)
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release
# 3. 빌드
cmake --build build
./build/my-conan-app

--build=missing은 현재 설정에 맞는 바이너리가 원격에 없을 때 소스에서 빌드하라는 옵션입니다.

-DCMAKE_BUILD_TYPE=Release를 붙인 이유가 있습니다. Conan은 프로필의 build_type(기본 Release)에 맞는 바이너리만 설치하고, CMakeDeps가 만든 설정 파일도 그 구성 전용입니다. Makefile·Ninja 같은 단일 구성 생성기에서 CMake의 빌드 타입을 비워 두거나 Debug로 두면 구성이 어긋나, find_package는 성공해도 타깃에 라이브러리 경로가 비어 링크 단계에서 undefined reference가 나는 식의 헷갈리는 증상이 생깁니다. Debug로 빌드하려면 conan install . -s build_type=Debug ...로 Debug 바이너리를 따로 받아야 합니다.

CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(my-conan-app VERSION 1.0.0 LANGUAGES CXX)
add_executable(my-conan-app src/main.cpp)
find_package(fmt REQUIRED)
find_package(spdlog REQUIRED)
target_link_libraries(my-conan-app PRIVATE
    fmt::fmt
    spdlog::spdlog
)
target_compile_features(my-conan-app PRIVATE cxx_std_17)

conan install 후 CMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake를 지정해야 find_package가 Conan이 생성한 설정 파일을 찾습니다.

main.cpp

#include <spdlog/spdlog.h>
#include <fmt/core.h>
int main() {
    spdlog::info("Conan으로 빌드됨");
    spdlog::info("fmt: {}", fmt::format("Hello, {}!", "Conan"));
    return 0;
}

conanfile.py

conanfile.py는 빌드 스크립트·옵션·요구사항을 세밀히 제어할 때 사용합니다.

from conan import ConanFile
from conan.tools.cmake import CMakeToolchain, CMakeDeps, cmake_layout
from conan.tools.build import check_min_cppstd
class MyAppConan(ConanFile):
    settings = "os", "compiler", "build_type", "arch"
    # generate()에서 직접 만들므로 generators 속성은 두지 않음 (둘 다 쓰면 Conan 2가 에러)
    default_options = {"spdlog/*:shared": False}
    def layout(self):
        cmake_layout(self)
    def requirements(self):
        self.requires("fmt/10.1.1")
        self.requires("spdlog/1.12.0")
    def validate(self):
        check_min_cppstd(self, "17")
    def generate(self):
        deps = CMakeDeps(self)
        deps.generate()
        tc = CMakeToolchain(self)
        tc.generate()

이 레시피에서 몇 가지는 Conan 1에서 넘어온 예제와 다르게 써야 합니다. 첫째, generators = "CMakeDeps", "CMakeToolchain" 속성과 generate() 메서드 안에서 같은 생성기를 직접 만드는 코드를 동시에 두면 Conan 2는 같은 생성기가 두 번 정의되었다는 에러를 냅니다. 생성기에 옵션을 줄 필요가 없으면 속성만, 세부 설정이 필요하면 generate()만 쓰면 됩니다. 둘째, check_min_cppstd처럼 “이 설정 조합으로는 빌드할 수 없다”를 판단하는 코드는 configure()가 아니라 validate()에 둡니다. validate()에서 발생한 오류는 Conan이 “이 구성은 유효하지 않음”으로 처리해 의존성 그래프 계산과 명확히 분리됩니다. 셋째, 의존성의 옵션은 "패키지/*:옵션" 패턴으로 지정합니다. 또 cmake_layout을 쓰면 생성 파일이 build/Release/generators 같은 하위 폴더에 놓이므로, 위 conanfile.txt 예제의 build/conan_toolchain.cmake 경로가 그대로 맞지 않는다는 점도 주의합니다. conan install이 생성하는 CMakeUserPresets.json을 쓰면 cmake --preset conan-release로 경로를 신경 쓰지 않고 configure할 수 있습니다.

프로필

프로필은 컴파일러, 빌드 타입(Release/Debug), 아키텍처, C++ 표준 같은 설정을 정의합니다.

# 기본 프로필 확인
conan profile show
# 프로필 경로
# Linux: ~/.conan2/profiles/default
# Windows: C:\Users\<user>\.conan2\profiles\default

프로필 예시(~/.conan2/profiles/default):

[settings]
os=Linux
arch=x86_64
compiler=gcc
compiler.version=12
compiler.libcxx=libstdc++11
compiler.cppstd=17
build_type=Release

Conan 1 시절 프로필에 있던 os_build, arch_build 같은 설정은 Conan 2에서 제거되어, 옛 프로필을 그대로 쓰면 해당 설정이 존재하지 않는다는 에러가 납니다. 빌드 머신과 대상 머신이 다른 크로스 컴파일은 Conan 2에서 -pr:h(host 프로필)와 -pr:b(build 프로필)를 나눠 지정하는 방식으로 바뀌었습니다. 새 환경에서는 conan profile detect로 현재 컴파일러를 감지한 기본 프로필을 만든 뒤 고쳐 쓰는 것이 가장 빠릅니다.

팀과 CI가 같은 프로필 파일을 저장소에 두고 공유하면 설정 차이가 줄어듭니다.

conan install . --output-folder=build --build=missing -pr=./conanprofile

Conan 옵션

일부 패키지는 옵션으로 빌드 방식을 바꿀 수 있습니다.

# conanfile.txt
[requires]
spdlog/1.12.0
[options]
spdlog/*:shared=False
spdlog/*:header_only=False

conanfile.py에서는 default_options = {"spdlog/*:shared": False}처럼 지정합니다. 옵션 앞의 spdlog/*: 패턴이 Conan 2 문법이며, Conan 1의 spdlog:shared=False 형식은 Conan 2에서 폐기 예정 문법으로 경고가 나므로 새 프로젝트에서는 쓰지 않습니다. 옵션을 바꾸면 패키지 ID가 달라져 캐시에 있던 바이너리를 쓸 수 없으므로, --build=missing으로 다시 빌드되는 것이 정상입니다. 레시피마다 지원하는 옵션이 다르므로 conan inspect 또는 Conan Center의 레시피 페이지에서 확인합니다.

Conan 레시피 검색

# Conan Center에서 패키지와 사용 가능한 버전 검색 (Conan 2)
conan search "spdlog" -r=conancenter
# 원격 저장소의 레시피·리비전 목록 확인
conan list "spdlog/*" -r=conancenter

Conan 빌드 흐름

sequenceDiagram
    participant Dev as 개발자
    participant Conan as Conan
    participant Cache as 캐시
    participant CMake as CMake
    Dev->>Conan: install
    Conan->>Cache: 패키지?
    alt 있음
        Cache-->>Conan: 제공
    else 없음
        Conan->>Conan: 원격에서 받거나 빌드
        Conan->>Cache: 저장
    end
    Conan->>Dev: toolchain, CMakeDeps 파일 생성
    Dev->>CMake: configure
    CMake->>CMake: 생성된 파일로 find_package

conanfile.py, 옵션, 프로파일

conanfile.py

conanfile.py는 Python으로 의존성·빌드·패키징을 제어하는 방식입니다. requirements()에서 self.requires()로 일반 의존성을, build_requirements()에서 self.test_requires()로 테스트 전용 의존성(GTest 등)을 선언합니다. layout()으로 디렉터리 구조를 정하고, generate()에서 CMake용 파일을 만들며, build()에서 실제 빌드를 수행합니다. 조건부 의존성이나 옵션이 필요할 때 텍스트 파일보다 유리합니다.

from conan import ConanFile
from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout

class MyProjectConan(ConanFile):
    name = "myproject"
    version = "1.0.0"
    settings = "os", "compiler", "build_type", "arch"

    def requirements(self):
        self.requires("fmt/10.2.1")
        self.requires("spdlog/1.13.0")

    def build_requirements(self):
        self.test_requires("gtest/1.14.0")

    def layout(self):
        cmake_layout(self)

    def generate(self):
        CMakeDeps(self).generate()
        CMakeToolchain(self).generate()

    def build(self):
        cmake = CMake(self)
        cmake.configure()
        cmake.build()

CMakeDeps를 생성하지 않으면 find_package(fmt)가 쓸 config 파일이 만들어지지 않으므로, generate()를 직접 쓸 때는 CMakeToolchain과 함께 생성해야 합니다(또는 클래스에 generators = "CMakeDeps", "CMakeToolchain"을 지정합니다).

옵션

options로 이 레시피의 선택 사항을 정의하고 default_options로 기본값을 둡니다. requirements() 안에서 옵션에 따라 의존성을 다르게 줄 수 있습니다.

from conan import ConanFile

class MyProjectConan(ConanFile):
    settings = "os", "compiler", "build_type", "arch"
    options = {"with_tests": [True, False]}
    default_options = {"with_tests": True}

    def requirements(self):
        self.requires("fmt/10.2.1")
        if self.options.with_tests:
            self.test_requires("gtest/1.14.0")

명령줄에서 현재 레시피(consumer)의 옵션을 바꿀 때는 &: 접두사를 씁니다. 예를 들어 conan install . -o "&:with_tests=False"로 테스트 의존성을 빼고 설치할 수 있고, 의존 패키지의 옵션은 -o "fmt/*:shared=True"처럼 패턴으로 지정합니다.

프로파일

프로파일 파일의 [settings]로 OS, 아키텍처, 컴파일러와 버전, 표준 라이브러리, C++ 표준, 빌드 타입을 고정할 수 있고, [conf]로 CMake 제너레이터 같은 도구 설정도 넣을 수 있습니다. 팀 표준 환경(예: Ubuntu 22.04 + GCC 11 + Release)을 프로파일 하나로 정의해 두면 누구의 PC에서든 같은 설정으로 의존성이 빌드됩니다.

# conan/profiles/linux-gcc11
[settings]
os=Linux
arch=x86_64
compiler=gcc
compiler.version=11
compiler.libcxx=libstdc++11
compiler.cppstd=17
build_type=Release

[conf]
tools.cmake.cmaketoolchain:generator=Ninja
conan install . --profile=conan/profiles/linux-gcc11 --build=missing

프로파일 파일을 ~/.conan2/profiles/ 대신 저장소 안에 두고 경로로 지정하면, 프로파일 자체도 코드 리뷰와 버전 관리의 대상이 됩니다.

vcpkg 예제 프로젝트: vcpkg.json부터 빌드까지

myapp-vcpkg/
├── CMakeLists.txt
├── vcpkg.json
└── src/
    └── main.cpp

vcpkg.json

{
  "name": "myapp-vcpkg",
  "version": "1.0.0",
  "dependencies": [
    "fmt",
    "spdlog",
    "nlohmann-json",
    "boost-asio"
  ],
  "builtin-baseline": "<vcpkg 저장소의 40자리 커밋 SHA>"
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.21)
project(MyAppVcpkg LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

find_package(fmt CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
find_package(boost_asio CONFIG REQUIRED)   # 최근 vcpkg의 Boost 포트별 CMake 패키지

add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE
    fmt::fmt
    spdlog::spdlog
    nlohmann_json::nlohmann_json
    Boost::asio
)

Boost의 CMake 패키지 이름과 타깃 이름은 vcpkg와 Boost 버전에 따라 바뀌어 왔습니다(예전에는 find_package(Boost REQUIRED COMPONENTS system) + Boost::system 형태). 설치할 때 vcpkg가 출력하는 usage 안내에 맞추세요.

src/main.cpp

#include <iostream>
#include <fmt/core.h>
#include <spdlog/spdlog.h>
#include <nlohmann/json.hpp>
#include <boost/asio.hpp>

int main() {
    spdlog::info("vcpkg 예제 시작");

    std::cout << fmt::format("Hello, {}!\n", "vcpkg");

    auto j = nlohmann::json::parse(R"({"name": "myapp", "version": 1})");
    std::cout << "JSON: " << j.dump() << "\n";

    // Boost.Asio: 1초 타이머
    boost::asio::io_context io;
    boost::asio::steady_timer t(io, std::chrono::seconds(1));
    t.async_wait([](const boost::system::error_code& ec) {
        if (!ec) spdlog::info("타이머 완료");
    });
    io.run();

    return 0;
}

빌드

export VCPKG_ROOT=/path/to/vcpkg

cmake -B build -S . \
  -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
cmake --build build
./build/myapp

첫 configure는 모든 의존성을 소스에서 빌드하므로 오래 걸립니다. vcpkg는 빌드한 패키지를 기본적으로 사용자 디렉터리의 바이너리 캐시(Linux는 ~/.cache/vcpkg/archives, Windows는 %LOCALAPPDATA%\vcpkg\archives)에 저장하므로, 같은 버전·트리플렛·컴파일러 조합이면 두 번째부터는 캐시에서 바로 복원합니다.

vcpkg vs Conan 비교

항목vcpkgConan
통합CMake 툴체인 지정으로 configure 시 자동 설치conan install 후 CMakeToolchain
패키지 수공식 레지스트리 2,000개 이상Conan Center 1,000개 이상
커스텀 빌드포트 수정·오버레이conanfile.py로 빌드 과정 전체 제어
플랫폼·컴파일러 조합triplet프로필
사내 패키지오버레이 포트, git 레지스트리사설 원격 저장소(Artifactory 등)
바이너리 캐시로컬 디렉터리 또는 원격 바이너리 소스Conan 캐시(~/.conan2)와 원격 저장소

Windows·Visual Studio·CMake 중심이고 공개 라이브러리 위주로 쓴다면 vcpkg가 설정이 가장 간단합니다. 여러 컴파일러와 크로스 컴파일 조합을 관리해야 하거나, 사내 라이브러리를 미리 빌드한 바이너리로 배포하는 구조라면 Conan의 프로필과 원격 저장소가 유리합니다. 이미 한쪽으로 많은 라이브러리를 쓰고 있다면 그대로 가져가는 편이 낫고, 어느 쪽이든 팀 규약과 CI 파이프라인에 맞춰 하나로 정해 두면 됩니다. 두 도구를 섞으면 같은 라이브러리가 두 벌 빌드되는 문제가 생기기 쉽습니다.


자주 만나는 에러

vcpkg 관련

”Could not find a package configuration file”

CMake Error: Could not find a package configuration file provided by "fmt"

대부분 CMake가 vcpkg의 툴체인 파일을 모른 채로 configure되었기 때문입니다. CMAKE_TOOLCHAIN_FILE은 첫 configure 시점에 들어가야 하므로, 이미 만든 빌드 디렉터리가 있다면 지우고 다시 configure합니다. 설치한 triplet(예: x64-windows)과 현재 빌드 대상이 다를 때도 패키지를 찾지 못합니다.

rm -rf build
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake

vcpkg가 쓰는 CMake·Ninja 문제

vcpkg는 기본적으로 자신이 쓸 CMake와 Ninja를 직접 내려받습니다. 내려받은 도구를 쓸 수 없는 환경(일부 ARM Linux, 오프라인 빌드 서버 등)에서는 VCPKG_FORCE_SYSTEM_BINARIES=1을 설정하고 시스템에 설치된 CMake와 Ninja를 사용하게 합니다. 이 경우 시스템 CMake 버전이 vcpkg가 요구하는 버전보다 낮으면 실패하므로 cmake --version으로 확인합니다.

baseline에 해당 버전이 없음

version>=로 요구한 버전이 baseline 커밋 시점의 버전 데이터베이스에 없거나 baseline이 너무 오래된 경우입니다. baseline을 최신 커밋으로 올립니다.

cd vcpkg && git pull && cd ..
./vcpkg/vcpkg x-update-baseline   # vcpkg.json의 builtin-baseline 갱신

baseline을 올리면 다른 포트 버전도 함께 바뀌므로, 별도 브랜치에서 전체 빌드와 테스트를 돌린 뒤 합치는 편이 안전합니다.

포트 빌드 실패

error: building xxx:x64-linux failed with: BUILD_FAILED

포트 소스 컴파일 중 에러가 났거나 시스템 의존성(예: pkg-config, autoconf)이 빠진 경우입니다. 에러 메시지에 buildtrees/<포트>/ 아래 로그 파일 경로가 출력되므로 그 로그를 먼저 봅니다. Manifest 모드에서 더 자세한 출력이 필요하면 -DVCPKG_INSTALL_OPTIONS=--debug를 configure에 넘깁니다.

Conan 관련

패키지를 찾지 못함

ERROR: Package 'fmt/10.1.1' not resolved: Unable to find 'fmt/10.1.1' in remotes

Conan Center에 해당 버전이 없거나 remote 설정이 빠진 경우입니다.

conan search fmt -r=conancenter   # 사용 가능한 버전 확인
conan remote list                 # conancenter가 등록되어 있는지 확인

find_package(fmt) 실패

CMAKE_TOOLCHAIN_FILE을 Conan이 생성한 conan_toolchain.cmake로 지정하지 않았거나, --output-folder와 CMake의 -B 경로가 달라 conan_toolchain.cmake를 찾지 못하는 경우입니다.

conan install . --output-folder=build --build=missing
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=$(pwd)/build/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release

미리 빌드된 바이너리가 없음

ERROR: Missing prebuilt package for 'fmt/10.1.1'

현재 프로필(컴파일러 버전, 아키텍처, 옵션 등)에 맞는 바이너리가 원격에 없습니다. 소스에서 빌드하도록 허용합니다.

conan install . --output-folder=build --build=missing
# 특정 패키지만 소스에서 다시 빌드
conan install . --output-folder=build --build="fmt/*"

버전 충돌

ERROR: Version conflict: Conflict between fmt/10.2.1 and fmt/10.1.1 in the graph.

spdlog 레시피가 요구하는 fmt 버전과 프로젝트가 직접 요구한 fmt 버전이 다를 때 나는 에러입니다. Conan 2는 이런 충돌을 자동으로 한쪽으로 정하지 않습니다. conan graph info . --format=html > graph.html로 그래프를 보고, 직접 요구 버전을 spdlog가 쓰는 버전에 맞추거나, 의도적으로 한 버전을 강제할 때는 conanfile.py에서 self.requires("fmt/10.1.1", override=True)처럼 지정합니다. 강제한 버전으로 spdlog가 빌드되는지는 직접 확인해야 합니다.

공통

C++ 표준 불일치

의존성의 공개 헤더가 C++17 기능(std::optional, std::string_view 등)을 쓰는데 프로젝트가 C++14로 빌드되면, 의존성 헤더를 include하는 순간 컴파일 에러가 납니다. 프로젝트 쪽 표준을 올립니다.

target_compile_features(myapp PRIVATE cxx_std_17)
# 또는
set(CMAKE_CXX_STANDARD 17)

Conan에서는 프로필의 compiler.cppstd가 의존성 빌드에 쓰이므로, 프로젝트와 같은 값으로 맞춰 두는 편이 안전합니다.

중복 정의·링크 에러

같은 라이브러리를 정적과 동적으로 섞어 링크하거나, 시스템 패키지와 패키지 매니저의 사본이 동시에 링크되면 중복 심볼 또는 미해결 심볼 에러가 납니다. MSVC에서는 정적 CRT(/MT)로 빌드한 라이브러리와 동적 CRT(/MD)로 빌드한 코드를 섞으면 LNK2038: mismatch detected for 'RuntimeLibrary'가 대표적입니다. vcpkg는 triplet 하나로 통일하고, Conan은 프로필의 compiler.runtime과 shared 옵션을 프로젝트 설정과 맞춥니다.


버전 고정과 CMake 연동 세부

vcpkg baseline 업데이트 주기

builtin-baseline은 vcpkg 포트 저장소의 특정 커밋을 가리킵니다. 주기적으로 올리면 보안 패치와 버그 수정을 받을 수 있지만, 여러 라이브러리 버전이 한꺼번에 바뀌므로 별도 브랜치에서 빌드와 테스트를 돌린 뒤 합치는 것이 좋습니다. 개발 브랜치에서 baseline이나 Conan 버전을 올리고, 스테이징에서 전체 테스트를 통과한 조합만 lockfile과 baseline에 고정해 배포하는 흐름이 일반적입니다.

Conan lockfile

# conan.lock 생성 (해결된 의존성 버전과 리비전 고정)
conan lock create .
# lockfile을 사용해 설치: CI와 로컬이 같은 버전 조합을 사용
conan install . --output-folder=build --lockfile=conan.lock --build=missing

conan.lock을 Git에 커밋하면, 레시피에 새 리비전이 올라와도 팀 전체가 같은 의존성 조합으로 빌드합니다.

find_package의 CONFIG 모드

vcpkg와 Conan(CMakeDeps)이 제공하는 패키지는 대부분 Config 모드 파일(<Pkg>Config.cmake)입니다. find_package(fmt CONFIG REQUIRED)처럼 CONFIG를 명시하면 CMake가 Find 모듈을 먼저 찾지 않고 패키지 매니저가 만든 설정 파일을 바로 사용합니다.

find_package(fmt CONFIG REQUIRED)
target_link_libraries(myapp PRIVATE fmt::fmt)
# Boost도 1.70부터 Config 파일을 제공하며, CMake 3.30부터 FindBoost 모듈은 폐기 예정
find_package(Boost CONFIG REQUIRED COMPONENTS filesystem)

CI, 사내 저장소, Docker 패턴

CI에서 vcpkg Manifest 모드

# .github/workflows/build.yml (예시)
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive  # vcpkg 서브모듈 포함
      - name: Bootstrap vcpkg
        run: ./vcpkg/bootstrap-vcpkg.sh
      - name: Configure CMake
        run: |
          cmake -B build -S . \
            -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake \
            -DCMAKE_BUILD_TYPE=Release
      - name: Build
        run: cmake --build build --config Release

멀티 플랫폼 빌드 (vcpkg)

triplet은 러너 OS와 맞아야 하므로 matrix에 OS와 triplet을 함께 둡니다.

strategy:
  matrix:
    include:
      - os: ubuntu-latest
        triplet: x64-linux
      - os: windows-latest
        triplet: x64-windows
      - os: macos-latest
        triplet: arm64-osx
runs-on: ${{ matrix.os }}
steps:
  - run: >
      cmake -B build -S .
      -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake
      -DVCPKG_TARGET_TRIPLET=${{ matrix.triplet }}

CI 캐시

vcpkg에서 캐시할 대상은 buildtrees(빌드 중간 산출물)가 아니라 바이너리 캐시입니다. 위치를 VCPKG_DEFAULT_BINARY_CACHE로 고정해 두면 OS와 상관없이 같은 경로를 캐시할 수 있습니다.

env:
  VCPKG_DEFAULT_BINARY_CACHE: ${{ github.workspace }}/vcpkg-bincache
steps:
  - run: mkdir -p vcpkg-bincache
  - name: Cache vcpkg binaries
    uses: actions/cache@v4
    with:
      path: vcpkg-bincache
      key: vcpkg-${{ runner.os }}-${{ hashFiles('vcpkg.json') }}
  # Conan 캐시
  - name: Cache Conan
    uses: actions/cache@v4
    with:
      path: ~/.conan2/p
      key: conan-${{ runner.os }}-${{ hashFiles('conan.lock') }}

builtin-baseline이 vcpkg.json 안에 있으므로 baseline을 올리면 캐시 키도 바뀝니다. Conan은 lockfile을 키로 쓰면 의존성 조합이 바뀔 때만 캐시가 갱신됩니다.

사내 Conan 저장소

# 사설 remote 추가
conan remote add company https://artifactory.company.com/artifactory/api/conan/conan-local
conan remote login company <사용자>
# conanfile.txt
[requires]
company-internal-lib/1.0.0
fmt/10.1.1

Docker와 vcpkg

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
    git cmake g++ ninja-build curl zip unzip tar pkg-config
RUN git clone https://github.com/microsoft/vcpkg.git /vcpkg && \
    /vcpkg/bootstrap-vcpkg.sh
# CMake 3.21부터 CMAKE_TOOLCHAIN_FILE 환경 변수를 인식
ENV CMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake
WORKDIR /app
COPY . .
RUN cmake -B build -S . && cmake --build build

Docker와 Conan

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3-pip cmake g++
RUN pip install conan && conan profile detect
WORKDIR /app
# 의존성 정의만 먼저 복사해, 소스만 바뀔 때는 이 레이어 캐시를 재사용
COPY conanfile.txt .
RUN conan install . --output-folder=build --build=missing
COPY . .
RUN cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release && \
    cmake --build build

Ubuntu 24.04 이후 이미지에서는 시스템 Python에 pip install이 막혀 있으므로 가상환경이나 pipx를 사용합니다.

vcpkg와 CMake Presets

CMakePresets.json으로 팀 전체가 같은 configure 설정을 씁니다.

{
  "version": 3,
  "configurePresets": [
    {
      "name": "vcpkg-default",
      "binaryDir": "${sourceDir}/build",
      "cacheVariables": {
        "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake"
      }
    }
  ]
}
cmake --preset vcpkg-default
cmake --build build

참고 자료


같이 보면 좋은 글

다음 글: [DevOps for C++ #40-2] CI/CD 파이프라인: GitHub Actions를 이용한 멀티 OS 자동 빌드 및 테스트 이전 글: [고성능 C++ #39-3] SIMD와 병렬화: std::execution과 인트린직(Intrinsics)을 이용한 연산 가속