본문 바로가기
잡(job)기술/opengl

OpenGL #03 - GLFW, GLAD, CMake로 OpenGL 3.3 개발 환경 세팅

by 무니이구나 2026. 8. 22.

🎯 이 글에서 얻을 수 있는 것

  • Windows, macOS, Linux에서 OpenGL 3.3 Core Profile 개발 환경을 구성하는 방법
  • GLFW로 OS 창을 만들고 OpenGL 컨텍스트를 생성하는 방법
  • GLAD로 최신 OpenGL 함수 포인터를 로딩하는 이유와 설정 방법
  • CMake로 GLFW, GLAD, OpenGL을 링크하는 최소 프로젝트 구조
  • 빈 OpenGL 창을 띄우고, 창 크기가 바뀌었을 때 glViewport를 갱신하는 기본 루틴

📸 결과물 미리보기

 

이 글의 결과물은 OpenGL 3.3 Core Profile 컨텍스트를 가진 960x540 창입니다. 아직 삼각형을 그리지는 않지만, glClearColor로 지정한 색으로 화면을 채우고 glClearglfwSwapBuffers를 이용해 정상적인 렌더링 루프의 뼈대를 확인합니다. ESC 키를 누르면 창이 종료됩니다.

🧠 개념 설명

OpenGL을 처음 접하면 가장 먼저 막히는 부분이 "그래서 창은 누가 만들어주고, OpenGL 함수는 어디서 오는가?"입니다. OpenGL 자체는 그래픽 드라이버가 제공하는 API 규약에 가깝고, 창 생성과 입력 처리는 OS마다 방법이 다릅니다. 이 차이를 정리해주는 라이브러리가 필요합니다.

이 시리즈에서는 다음 조합을 사용합니다.

  • GLFW: OS별 창 생성, 키보드/마우스 입력, OpenGL 컨텍스트 생성을 담당하는 라이브러리입니다.
  • GLAD: OpenGL 함수 포인터를 실행 시점에 불러오는 로더입니다.
  • CMake: 플랫폼별로 빌드와 링크를 자동화하는 빌드 시스템입니다.

비유하자면 이런 구조입니다.

OpenGL은 상태 머신입니다. 예를 들어 glClearColor(0.12f, 0.18f, 0.28f, 1.0f)는 "앞으로 색상 버퍼를 지울 때 이 색을 사용해라"라는 상태를 설정합니다. 이후 glClear(GL_COLOR_BUFFER_BIT)를 호출하면 현재 설정된 clear color로 색상 버퍼를 지웁니다. 즉, OpenGL 함수들은 대부분 어떤 상태에 영향을 주거나 현재 상태를 사용해 작업을 수행합니다.

이번 편에서 다루는 내용은 전체 OpenGL 파이프라인 관점에서 보면 아직 버텍스 데이터를 넘겨주기 전 단계에 해당합니다. 정확히는 렌더링을 위한 컨텍스트와 프레임버퍼 준비 단계입니다. glClear는 버텍스 셰이더나 래스터라이저를 통과하는 도형은 없지만, 화면에 보여줄 색상 버퍼를 초기화하는 작업입니다. 이후 삼각형을 그릴 때는 버텍스 데이터 → 버텍스 셰이더 → 래스터라이저 → 프래그먼트 셰이더 → 프레임버퍼 순서로 진행됩니다.

이번 편에서 만들 코드는 다음 순서를 따릅니다.

  1. GLFW 초기화
  2. OpenGL 3.3 Core Profile 창 생성
  3. OpenGL 컨텍스트를 현재 스레드에 바인딩
  4. GLAD로 OpenGL 함수 포인터 로딩
  5. 창 크기 변경 콜백에서 glViewport 갱신
  6. 매 프레임 glClearglfwSwapBuffersglfwPollEvents

이 순서가 중요합니다. OpenGL 함수는 컨텍스트가 현재 상태로 지정되기 전에는 호출할 수 없고, GLAD 로딩도 컨텍스트 생성 이후에 해야 합니다.

🛠️ 구현 준비

필요한 준비물은 다음과 같습니다.

  • C++17 이상을 지원하는 컴파일러
  • CMake 3.16 이상
  • Git
  • OpenGL 3.3 이상을 지원하는 GPU 또는 드라이버
  • GLAD 로더 파일

플랫폼별 준비 사항은 다음과 같습니다.

Windows

  • Visual Studio Build Tools 또는 MSVC 컴파일러
  • CMake
  • Git

Windows에서는 일반적으로 추가 OpenGL 개발 패키지 설치 없이 opengl32.lib가 Windows SDK에 포함됩니다. CMake의 find_package(OpenGL REQUIRED)가 이를 찾아 링크합니다.

macOS

  • Xcode Command Line Tools
  • CMake

터미널에서 아래 명령으로 기본 개발 도구를 설치할 수 있습니다.

xcode-select --install
brew install cmake

macOS는 OpenGL이 deprecated 상태이지만, 학습 목적의 OpenGL 3.3 Core Profile 창은 여전히 만들 수 있습니다. macOS에서 OpenGL 3.2 이상 Core Profile을 사용하려면 GLFW_OPENGL_FORWARD_COMPAT 힌트가 필요합니다.

Linux

Debian/Ubuntu 계열 기준 아래 패키지를 설치합니다.

sudo apt update
sudo apt install build-essential cmake git libgl1-mesa-dev xorg-dev

 

다른 배포판에서는 mesa-libGL-devel, libX11-devel 등 OpenGL과 X11 개발 패키지가 필요합니다. Wayland 환경에서는 GLFW가 Wayland 백엔드로 빌드될 수도 있지만, 이 글에서는 기본적인 X11 개발 패키지를 기준으로 설명합니다.

프로젝트 구조

최종적으로 만들 프로젝트 구조는 다음과 같습니다.

modern-opengl-setup/
├── CMakeLists.txt
├── src/
│   └── main.cpp
└── third_party/
    └── glad/
        ├── include/
        │   ├── glad/
        │   │   └── glad.h
        │   └── KHR/
        │       └── khrplatform.h
        └── src/
            └── glad.c

여기서 third_party/glad는 GLAD 생성기에서 만든 로더 파일을 넣는 폴더입니다.

GLAD 로더 준비

GLAD는 OpenGL 함수 포인터를 실행 시점에 불러오는 로더입니다. Windows의 opengl32.dll은 오래된 OpenGL 1.1 수준의 함수만 직접 제공하고, 최신 OpenGL 함수는 드라이버로부터 함수 주소를 받아와야 합니다. GLAD가 이 작업을 대신해 줍니다.

GLAD 생성기에서 다음 조건으로 로더를 생성합니다.

  • Language: C/C++
  • Specification: OpenGL
  • gl: Version 3.3
  • Profile: Core

이 글에서는 아래 파일 구조가 나오는 GLAD C 출력을 사용합니다.

third_party/glad/include/glad/glad.h
third_party/glad/include/KHR/khrplatform.h
third_party/glad/src/glad.c

생성한 파일들을 프로젝트의 third_party/glad 폴더에 복사하면 됩니다.

💻 코드 구현

1. CMakeLists.txt

프로젝트 루트에 CMakeLists.txt를 만듭니다.

cmake_minimum_required(VERSION 3.16)
project(modern_opengl_renderer LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# OpenGL은 플랫폼별로 찾아서 링크합니다.
find_package(OpenGL REQUIRED)

# GLFW는 FetchContent로 가져옵니다.
include(FetchContent)

set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)

FetchContent_Declare(
    glfw
    GIT_REPOSITORY https://github.com/glfw/glfw.git
    GIT_TAG 3.4
)

FetchContent_MakeAvailable(glfw)

# GLAD는 로더 소스를 정적 라이브러리로 빌드합니다.
add_library(glad STATIC
    third_party/glad/src/glad.c
)

target_include_directories(glad PUBLIC
    third_party/glad/include
)

add_executable(opengl_window
    src/main.cpp
)

target_link_libraries(opengl_window PRIVATE
    glfw
    glad
    OpenGL::GL
)

if (MSVC)
    target_compile_options(opengl_window PRIVATE /W4)
else()
    target_compile_options(opengl_window PRIVATE -Wall -Wextra)
endif()

여기서 중요한 부분은 다음입니다.

  • LANGUAGES C CXX를 지정합니다. glad.c는 C 파일이므로 C 컴파일러가 필요합니다.
  • find_package(OpenGL REQUIRED)는 플랫폼별 OpenGL 라이브러리를 찾습니다.
  • OpenGL::GL은 CMake가 제공하는 imported target입니다. Windows에서는 opengl32.lib, Linux에서는 libGL, macOS에서는 OpenGL 프레임워크를 링크하는 데 사용됩니다.
  • glad 라이브러리는 target_include_directoriesPUBLIC으로 설정해서 opengl_window에서 #include <glad/glad.h>를 사용할 수 있게 합니다.

2. src/main.cpp

src/main.cpp를 만들고 아래 코드를 넣습니다.

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>

// GLFW 오류를 콘솔에 출력합니다.
void glfw_error_callback(int error, const char* description)
{
    std::cerr << "GLFW error " << error << ": " << description << '\n';
}

// 창 또는 프레임버퍼 크기가 바뀌면 OpenGL 렌더링 영역을 다시 설정합니다.
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // 고해상도 디스플레이에서는 창 크기와 실제 픽셀 버퍼 크기가 다를 수 있습니다.
    // GLFW는 프레임버퍼 크기를 알려주므로 이 값을 사용해야 합니다.
    glViewport(0, 0, width, height);
}

// 간단한 OpenGL 오류 확인 함수입니다.
// glGetError는 호출 시점에 쌓여 있는 오류를 조회하고 해당 오류 플래그를 초기화합니다.
void check_gl_error(const char* label)
{
    GLenum error = glGetError();
    while (error != GL_NO_ERROR)
    {
        std::cerr << "[OpenGL Error] " << error << " at " << label << '\n';
        error = glGetError();
    }
}

int main()
{
    // GLFW 오류 콜백은 가능한 한 빨리 등록합니다.
    glfwSetErrorCallback(glfw_error_callback);

    // 1. GLFW 초기화
    if (!glfwInit())
    {
        std::cerr << "Failed to initialize GLFW\n";
        return -1;
    }

    // 2. OpenGL 3.3 Core Profile 요청
    // 이 힌트는 glfwCreateWindow 전에 설정해야 합니다.
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    // macOS에서 OpenGL 3.2 이상 Core Profile을 사용하려면
    // forward-compatible 힌트가 필요합니다.
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

    // 3. 창 생성
    GLFWwindow* window = glfwCreateWindow(
        960,
        540,
        "Modern OpenGL Renderer",
        nullptr,
        nullptr
    );

    if (window == nullptr)
    {
        std::cerr << "Failed to create GLFW window\n";
        glfwTerminate();
        return -1;
    }

    // 4. OpenGL 컨텍스트를 현재 스레드에 바인딩
    // 이 호출 이후에야 OpenGL 상태를 다루는 작업을 할 수 있습니다.
    glfwMakeContextCurrent(window);

    // 창 크기 변경 콜백 등록
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // 수직 동기화를 켭니다.
    // 1은 프레임 속도를 모니터 주사율에 맞춥니다.
    glfwSwapInterval(1);

    // 5. GLAD로 OpenGL 함수 포인터 로딩
    // 반드시 glfwMakeContextCurrent 이후에 호출해야 합니다.
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cerr << "Failed to initialize GLAD\n";
        glfwTerminate();
        return -1;
    }

    // 드라이버 정보를 출력합니다.
    std::cout << "OpenGL Vendor:   "
              << reinterpret_cast<const char*>(glGetString(GL_VENDOR)) << '\n';

    std::cout << "OpenGL Renderer: "
              << reinterpret_cast<const char*>(glGetString(GL_RENDERER)) << '\n';

    std::cout << "OpenGL Version:  "
              << reinterpret_cast<const char*>(glGetString(GL_VERSION)) << '\n';

    // 6. OpenGL 상태 설정
    // glClearColor는 상태를 설정합니다. 아직 화면을 지우지는 않습니다.
    glClearColor(0.12f, 0.18f, 0.28f, 1.0f);
    check_gl_error("initial state");

    // 7. 렌더링 루프
    while (!glfwWindowShouldClose(window))
    {
        // ESC 키를 누르면 창을 닫습니다.
        if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        {
            glfwSetWindowShouldClose(window, GLFW_TRUE);
        }

        // 현재 clear color로 색상 버퍼를 지웁니다.
        glClear(GL_COLOR_BUFFER_BIT);
        check_gl_error("after glClear");

        // 백 버퍼에 그린 결과를 화면에 보이는 프런트 버퍼와 교환합니다.
        glfwSwapBuffers(window);

        // 키보드/마우스 이벤트를 처리합니다.
        glfwPollEvents();
    }

    // 8. 종료 처리
    glfwDestroyWindow(window);
    glfwTerminate();

    return 0;
}

3. 코드에서 중요한 순서

이 코드에서 특히 중요한 순서는 다음입니다.

glfwInit
↓
glfwWindowHint
↓
glfwCreateWindow
↓
glfwMakeContextCurrent
↓
gladLoadGLLoader
↓
OpenGL 상태 설정 및 렌더링

 

이 순서가 바뀌면 문제가 생깁니다. 예를 들어 gladLoadGLLoaderglfwMakeContextCurrent보다 먼저 호출하면 OpenGL 컨텍스트가 아직 현재 상태가 아니므로 함수 로딩에 실패할 수 있습니다. 또한 glfwMakeContextCurrent 전에 glClearColor 같은 OpenGL 함수를 호출하면 접근 위반이 발생할 수 있습니다.

4. 왜 glViewport를 콜백에서 갱신하는가?

glViewport는 OpenGL이 정규화 장치 좌표, 즉 NDC 좌표를 실제 화면 픽셀 좌표로 어떻게 변환할지를 정의합니다. 창 크기가 달라졌는데도 기존 viewport를 그대로 쓰면 이후에 그릴 도형의 화면 비율이 깨질 수 있습니다.

이 글에서는 아직 도형을 그리지 않지만, 창 크기 변경 콜백을 미리 등록해 두는 것이 좋습니다.

glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

 

고해상도 디스플레이에서는 창 크기와 실제 프레임버퍼 크기가 다를 수 있습니다. 예를 들어 macOS Retina나 Windows DPI 스케일링 환경에서는 960x540 창이라도 실제 프레임버퍼는 더 클 수 있습니다. 따라서 glfwSetWindowSizeCallback보다 glfwSetFramebufferSizeCallback을 사용하는 것이 더 안전합니다.

5. 왜 glfwSwapBuffers가 필요한가?

OpenGL은 보통 더블 버퍼링을 사용합니다. 우리가 그리는 버퍼는 화면에 직접 보이는 버퍼가 아니라 백 버퍼입니다. glfwSwapBuffers는 백 버퍼를 화면에 보이는 프런트 버퍼와 교환해서 그려진 결과를 표시합니다.

이 호출이 없으면 창은 뜨지만 화면이 갱신되지 않거나, 이전 프레임이 그대로 남아 있는 것처럼 보일 수 있습니다.

6. 왜 glGetError를 사용하는가?

OpenGL은 오류가 발생해도 예외를 던지지 않습니다. 대신 내부 오류 플래그를 설정합니다. glGetError는 그 플래그를 조회합니다. 이번 편에서는 단순한 상태 설정과 clear만 있지만, 이후 버퍼, 셰이더, 텍스처를 다루게 되면 오류 확인 루틴이 필수적입니다.

다만 glGetError는 모든 문제를 잡지는 못합니다. 이후 OpenGL 디버그 출력 기능을 사용할 수 있는 환경이 되면 더 자세한 로그를 받을 수 있습니다. 하지만 초기 학습 단계에서는 간단한 glGetError 체크만으로도 많은 문제를 빠르게 찾을 수 있습니다.

✅ 실행 방법

프로젝트 루트에서 다음 명령을 실행합니다.

cmake -S . -B build
cmake --build build

Linux 또는 macOS에서는 보통 다음 경로에 실행 파일이 생깁니다.

./build/opengl_window

 

Windows Visual Studio처럼 멀티 구성 생성기를 사용하는 경우 다음처럼 빌드합니다.

cmake -S . -B build
cmake --build build --config Release

실행 파일은 보통 다음 경로에 생깁니다.

build\Release\opengl_window.exe

 

정상 실행되면 콘솔에 GPU와 OpenGL 버전 정보가 출력되고, 어두운 파란색 계열의 창이 하나 뜹니다. ESC 키를 누르면 창이 닫힙니다.

예상 콘솔 출력은 시스템에 따라 다르지만 다음과 비슷한 형태입니다.

OpenGL Vendor:   NVIDIA Corporation
OpenGL Renderer: NVIDIA GeForce RTX 3060/PCIe/SSE2
OpenGL Version:  3.3.0 NVIDIA 550.54.14

⚠️ 흔한 실수와 디버깅 체크리스트

1. glfwCreateWindow가 실패한다

가능한 원인:

  • GPU 또는 드라이버가 OpenGL 3.3 Core Profile을 지원하지 않음
  • macOS에서 GLFW_OPENGL_FORWARD_COMPAT 힌트를 설정하지 않음
  • Linux에서 그래픽 세션이 없거나 원격 SSH 환경에서 디스플레이가 없음
  • 가상 머신에서 3D 가속이 꺼져 있음

체크리스트:

  • glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3)을 설정했는가?
  • glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3)을 설정했는가?
  • glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE)을 설정했는가?
  • macOS에서 glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE)을 설정했는가?
  • GLFW 오류 콜백에서 출력된 메시지를 확인했는가?

2. gladLoadGLLoader가 실패한다

가능한 원인:

  • glfwMakeContextCurrent를 호출하지 않음
  • OpenGL 컨텍스트 생성에 이미 실패했는데 무시하고 진행함
  • 잘못된 로더 함수를 전달함

체크리스트:

  • glfwCreateWindow 반환값이 nullptr이 아닌가?
  • glfwMakeContextCurrent(window)를 호출했는가?
  • gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)를 호출했는가?
  • gladLoadGLLoader 전에 OpenGL 함수를 호출하지 않았는가?

3. 링크 오류가 발생한다

대표적인 링크 오류:

undefined reference to gladLoadGLLoader
undefined reference to glClear
unresolved external symbol __imp_glfwInit

체크리스트:

  • third_party/glad/src/glad.c가 CMake의 add_library(glad STATIC ...)에 포함되었는가?
  • target_link_libraries(opengl_window PRIVATE glfw glad OpenGL::GL)에 세 라이브러리가 모두 들어갔는가?
  • find_package(OpenGL REQUIRED)가 성공했는가?
  • Linux에서 libgl1-mesa-dev 또는 해당 패키지 설치되어 있는가?

4. 창은 뜨는데 검은색이다

가능한 원인:

  • glClearColor를 검정색으로 설정함
  • glClear(GL_COLOR_BUFFER_BIT)를 호출하지 않음
  • glfwSwapBuffers(window)를 호출하지 않음
  • 렌더링 루프가 즉시 종료됨

체크리스트:

  • glClearColor의 RGB 값이 눈에 보이는 색인가?
  • 매 프레임 glClear(GL_COLOR_BUFFER_BIT)를 호출하는가?
  • 매 프레임 glfwSwapBuffers(window)를 호출하는가?
  • while (!glfwWindowShouldClose(window)) 루프가 정상적으로 유지되는가?

5. 창 크기를 조절하면 화면이 이상해 보인다

아직 도형을 그리지 않기 때문에 이번 편에서는 큰 차이가 없을 수 있습니다. 하지만 이후 삼각형이나 사각형을 그릴 때 glViewport를 갱신하지 않으면 화면 비율이 깨집니다.

체크리스트:

  • glfwSetFramebufferSizeCallback(window, framebuffer_size_callback)을 등록했는가?
  • 콜백에서 glViewport(0, 0, width, height)를 호출하는가?
  • 창 크기가 아닌 프레임버퍼 크기를 사용하고 있는가?

6. Windows에서 GLFW를 DLL로 사용할 때 실행 파일이 바로 종료된다

이 글의 CMake는 GLFW를 기본적으로 정적 라이브러리처럼 빌드하는 구성이므로 보통 추가 DLL이 필요하지 않습니다. 하지만 외부에서 GLFW를 DLL로 빌드한 뒤 가져온 경우 glfw3.dll이 실행 파일과 같은 폴더에 있어야 할 수 있습니다.

체크리스트:

  • GLFW를 정적으로 빌드했는지 공유 라이브러리로 빌드했는지 확인했는가?
  • 공유 라이브러리라면 실행 파일 폴더에 DLL이 있는가?
  • 32비트/64비트 구성이 일치하는가?

🧪 연습 문제

연습 문제 1: 필수 개념 확인

현재 코드는 ESC 키를 누르면 종료됩니다. 다음 조건을 만족하도록 코드를 수정해 보세요.

  • Q 키를 누르면 창이 종료되게 수정
  • clear color를 본인이 원하는 색으로 변경
  • 창 제목을 "OpenGL Setup Complete"로 변경

힌트:

glfwGetKey(window, GLFW_KEY_Q)
glfwSetWindowShouldClose(window, GLFW_TRUE)
glfwSetWindowTitle(window, "OpenGL Setup Complete")

연습 문제 2: 응용 확장

매 프레임 glfwGetTime을 사용해 창 제목에 경과 시간 또는 FPS를 출력해 보세요. 단, 이번 편에서는 아직 도형을 그리지 않으므로 창 제목만 바뀌면 됩니다.

요구 사항:

  • 1초마다 창 제목을 갱신
  • 창 제목에 FPS 또는 경과 시간 표시
  • glfwSetWindowTitle 사용

힌트:

double current_time = glfwGetTime();

연습 문제 3: 디버깅 연습

check_gl_error 함수를 사용해 다음 상황에서 어떤 오류가 발생하는지 관찰해 보세요.

  • glfwMakeContextCurrent를 주석 처리한 뒤 glClearColor 호출
  • gladLoadGLLoader를 주석 처리한 뒤 glClear 호출
  • glClear의 인자를 잘못된 값으로 변경

이 연습은 실제로 커밋하기 위한 코드가 아니라, 오류가 어떤 순서에서 발생하는지 이해하기 위한 실험입니다. 실험 후에는 반드시 원래 코드로 되돌립니다.

🔭 다음 편 예고

다음 편에서는 드디어 OpenGL 파이프라인에 실제로 데이터를 넣어 봅니다. VAO와 VBO를 만들고, 버텍스 셰이더와 프래그먼트 셰이더를 컴파일해서 화면에 첫 삼각형을 그려보겠습니다.

📚 참고 자료