본문으로 건너뛰기
Previous
Next
SDL2 실전 가이드 | 게임 개발 입문

SDL2 실전 가이드 | 게임 개발 입문

SDL2 실전 가이드 | 게임 개발 입문

이 글의 핵심

윈도우·렌더링·이벤트·오디오·게임 루프까지. C++·Python 예제로 보는 SDL2 사용법. 실전 예제와 코드로 개념부터 활용까지 정리합니다. SDL·SDL2·Game 중심으로 설명합니다. Start now.

이 글의 핵심

SDL2로 창 띄우고 입력 받고, 렌더러로 그리고, 믹서로 소리까지 내는 흐름을 한 번에 잡는다. 엔진 없이 직접 굴릴 때 자주 마주치는 지점 위주다.

사전 지식 (초보자를 위한 기초)

1. 게임 개발의 기본 구조

게임은 보통 입력 → 갱신 → 그리기무한 루프로 돌린다.

게임 루프:
┌─────────────────────┐
│  1. 입력 처리       │ ← 키보드, 마우스
│     (Input)         │
├─────────────────────┤
│  2. 게임 로직       │ ← 물리, AI, 충돌
│     (Update)        │
├─────────────────────┤
│  3. 화면 그리기     │ ← 렌더링
│     (Render)        │
└─────────────────────┘

    (반복)

입력은 플레이어 의도를 받고, 업데이트에서 위치·충돌·AI를 갱신한 뒤, 렌더링에서 화면에 반영한다.

2. 좌표계

SDL 좌표계:

(0,0) ────────→ X (오른쪽)




  Y (아래)
800×600 창을 예로 들면:
- 좌측 상단: (0, 0)
- 우측 상단: (800, 0)
- 좌측 하단: (0, 600)
- 우측 하단: (800, 600)
- 중앙: (400, 300)

3. 프레임레이트 (FPS)

FPS (Frames Per Second)는 초당 화면을 갱신하는 횟수다.

60 FPS:
- 1초에 60번 화면 갱신
- 1프레임 = 16.67ms
- 부드러운 움직임
30 FPS:
- 1초에 30번 화면 갱신
- 1프레임 = 33.33ms
- 일반적
15 FPS:
- 1초에 15번 화면 갱신
- 1프레임 = 66.67ms
- 끊김 현상

4. 델타 타임 (Delta Time)

델타 타임은 직전 프레임과 이번 프레임 사이의 시간 간격이다.

// ❌ 잘못된 방법 (FPS에 따라 속도 다름)
x += 5;  // 60 FPS면 초당 300 픽셀, 30 FPS면 150 픽셀
// ✅ 올바른 방법 (FPS 무관)
x += 300 * deltaTime;  // 항상 초당 300 픽셀
// deltaTime 계산
float currentTime = SDL_GetTicks() / 1000.0f;
float deltaTime = currentTime - lastTime;
lastTime = currentTime;

5. Surface vs Texture

Surface:

  • CPU 메모리에 저장
  • 느림
  • 소프트웨어 렌더링 Texture:
  • GPU 메모리에 저장
  • 빠름
  • 하드웨어 가속
Surface (느림):
CPU ─→ Surface ─→ 화면
Texture (빠름):
CPU ─→ Texture ─→ GPU ─→ 화면

1. SDL이란?

SDL (Simple DirectMedia Layer)크로스 플랫폼 멀티미디어 라이브러리다.

SDL의 역할

창·렌더러·입력(키보드·마우스·패드)·오디오·타이머 등을 묶어 준다. C API라 여러 언어에서 감싸 쓰기 좋다.

SDL vs 다른 라이브러리

SDL은 PC·모바일까지 포괄적으로 쓰이는 편이고, SFML은 C++에 익숙한 사람에게 더 단정하게 느껴질 수 있다. Raylib는 더 얇은 API다. 엔진(Unity, Unreal)은 도구가 많은 대신 무겁고 학습량도 크다.

SDL 모듈

SDL2:           핵심 (윈도우, 렌더링, 이벤트)
SDL2_image:     이미지 로딩 (PNG, JPG, BMP)
SDL2_ttf:       트루타입 폰트
SDL2_mixer:     오디오 믹싱
SDL2_net:       네트워킹

2. 개발 환경 설정

설치

macOS:

brew install sdl2
brew install sdl2_image sdl2_ttf sdl2_mixer

Ubuntu/Debian:

sudo apt update
sudo apt install libsdl2-dev
sudo apt install libsdl2-image-dev libsdl2-ttf-dev libsdl2-mixer-dev

Windows (vcpkg):

vcpkg install sdl2
vcpkg install sdl2-image sdl2-ttf sdl2-mixer

Windows (수동):

1. https://www.libsdl.org/download-2.0.php
2. Development Libraries 다운로드
3. 압축 해제
4. Visual Studio 프로젝트 설정:
   - Include Directories: SDL2/include
   - Library Directories: SDL2/lib/x64
   - Linker Input: SDL2.lib, SDL2main.lib

CMake 프로젝트

CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(SDLGame)
set(CMAKE_CXX_STANDARD 17)
# SDL2 찾기
find_package(SDL2 REQUIRED)
find_package(SDL2_image REQUIRED)
find_package(SDL2_ttf REQUIRED)
find_package(SDL2_mixer REQUIRED)
# 실행 파일
add_executable(game
    src/main.cpp
    src/Game.cpp
    src/Texture.cpp
    src/Player.cpp
)
# 라이브러리 링크
target_link_libraries(game
    SDL2::SDL2
    SDL2::SDL2main
    SDL2_image::SDL2_image
    SDL2_ttf::SDL2_ttf
    SDL2_mixer::SDL2_mixer
)
# 인클루드 디렉토리
target_include_directories(game PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/include
)

빌드:

mkdir build
cd build
cmake ..
make
./game

3. 첫 번째 윈도우

기본 윈도우

#include <SDL2/SDL.h>
#include <iostream>
const int SCREEN_WIDTH = 800;
const int SCREEN_HEIGHT = 600;
int main(int argc, char* argv[]) {
    // SDL 초기화
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        std::cerr << "SDL 초기화 실패: " << SDL_GetError() << std::endl;
        return -1;
    }
    
    // 윈도우 생성
    SDL_Window* window = SDL_CreateWindow(
        "SDL Tutorial",
        SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED,
        SCREEN_WIDTH,
        SCREEN_HEIGHT,
        SDL_WINDOW_SHOWN
    );
    
    if (!window) {
        std::cerr << "윈도우 생성 실패: " << SDL_GetError() << std::endl;
        SDL_Quit();
        return -1;
    }
    
    // 렌더러 생성
    SDL_Renderer* renderer = SDL_CreateRenderer(
        window,
        -1,
        SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC
    );
    
    if (!renderer) {
        std::cerr << "렌더러 생성 실패: " << SDL_GetError() << std::endl;
        SDL_DestroyWindow(window);
        SDL_Quit();
        return -1;
    }
    
    // 메인 루프
    bool running = true;
    SDL_Event event;
    
    while (running) {
        // 이벤트 처리
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                running = false;
            }
        }
        
        // 렌더링
        SDL_SetRenderDrawColor(renderer, 100, 149, 237, 255);  // 코른플라워 블루
        SDL_RenderClear(renderer);
        SDL_RenderPresent(renderer);
    }
    
    // 정리
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
    
    return 0;
}

4. 이벤트 처리

키보드 입력

bool running = true;
SDL_Event event;
while (running) {
    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_QUIT) {
            running = false;
        }
        
        // 키 눌림
        if (event.type == SDL_KEYDOWN) {
            switch (event.key.keysym.sym) {
                case SDLK_ESCAPE:
                    running = false;
                    break;
                case SDLK_UP:
                    std::cout << "위쪽 화살표" << std::endl;
                    break;
                case SDLK_DOWN:
                    std::cout << "아래쪽 화살표" << std::endl;
                    break;
                case SDLK_LEFT:
                    std::cout << "왼쪽 화살표" << std::endl;
                    break;
                case SDLK_RIGHT:
                    std::cout << "오른쪽 화살표" << std::endl;
                    break;
                case SDLK_SPACE:
                    std::cout << "스페이스바" << std::endl;
                    break;
            }
        }
        
        // 키 떼어짐
        if (event.type == SDL_KEYUP) {
            std::cout << "키 떼어짐" << std::endl;
        }
    }
}

연속 입력 (키 누르고 있기):

// 키보드 상태 배열
// 변수 선언 및 초기화
const Uint8* keyState = SDL_GetKeyboardState(nullptr);
// 게임 루프
while (running) {
    // 이벤트 처리
    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_QUIT) running = false;
    }
    
    // 연속 입력 확인
    if (keyState[SDL_SCANCODE_W]) {
        playerY -= 5;  // 위로 이동
    }
    if (keyState[SDL_SCANCODE_S]) {
        playerY += 5;  // 아래로 이동
    }
    if (keyState[SDL_SCANCODE_A]) {
        playerX -= 5;  // 왼쪽으로 이동
    }
    if (keyState[SDL_SCANCODE_D]) {
        playerX += 5;  // 오른쪽으로 이동
    }
    
    // 렌더링...
}

마우스 입력

while (SDL_PollEvent(&event)) {
    // 마우스 버튼 클릭
    if (event.type == SDL_MOUSEBUTTONDOWN) {
        if (event.button.button == SDL_BUTTON_LEFT) {
            int x = event.button.x;
            int y = event.button.y;
            std::cout << "왼쪽 클릭: (" << x << ", " << y << ")" << std::endl;
        }
        if (event.button.button == SDL_BUTTON_RIGHT) {
            std::cout << "오른쪽 클릭" << std::endl;
        }
    }
    
    // 마우스 이동
    if (event.type == SDL_MOUSEMOTION) {
        int x = event.motion.x;
        int y = event.motion.y;
        int dx = event.motion.xrel;  // 상대 이동
        int dy = event.motion.yrel;
    }
    
    // 마우스 휠
    if (event.type == SDL_MOUSEWHEEL) {
        if (event.wheel.y > 0) {
            std::cout << "휠 위로" << std::endl;
        } else if (event.wheel.y < 0) {
            std::cout << "휠 아래로" << std::endl;
        }
    }
}
// 현재 마우스 위치
int mouseX, mouseY;
Uint32 mouseState = SDL_GetMouseState(&mouseX, &mouseY);
if (mouseState & SDL_BUTTON(SDL_BUTTON_LEFT)) {
    std::cout << "왼쪽 버튼 누르고 있음" << std::endl;
}

게임패드 입력

// 게임패드 초기화
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER);
// 게임패드 열기
SDL_GameController* controller = nullptr;
for (int i = 0; i < SDL_NumJoysticks(); i++) {
    if (SDL_IsGameController(i)) {
        controller = SDL_GameControllerOpen(i);
        if (controller) {
            std::cout << "게임패드 연결: " << SDL_GameControllerName(controller) << std::endl;
            break;
        }
    }
}
// 이벤트 처리
while (SDL_PollEvent(&event)) {
    // 버튼
    if (event.type == SDL_CONTROLLERBUTTONDOWN) {
        if (event.cbutton.button == SDL_CONTROLLER_BUTTON_A) {
            std::cout << "A 버튼" << std::endl;
        }
        if (event.cbutton.button == SDL_CONTROLLER_BUTTON_START) {
            std::cout << "Start 버튼" << std::endl;
        }
    }
    
    // 아날로그 스틱
    if (event.type == SDL_CONTROLLERAXISMOTION) {
        if (event.caxis.axis == SDL_CONTROLLER_AXIS_LEFTX) {
            float value = event.caxis.value / 32767.0f;  // -1.0 ~ 1.0
            std::cout << "왼쪽 스틱 X: " << value << std::endl;
        }
    }
}
// 정리
SDL_GameControllerClose(controller);

5. 렌더링

기본 도형 그리기

// 배경색 설정
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);  // 검정
SDL_RenderClear(renderer);
// 점
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);  // 흰색
SDL_RenderDrawPoint(renderer, 400, 300);
// 선
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);  // 빨강
SDL_RenderDrawLine(renderer, 100, 100, 700, 500);
// 사각형 (테두리)
SDL_Rect rect = {100, 100, 200, 150};
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);  // 초록
SDL_RenderDrawRect(renderer, &rect);
// 사각형 (채우기)
SDL_SetRenderDrawColor(renderer, 0, 0, 255, 255);  // 파랑
SDL_RenderFillRect(renderer, &rect);
// 화면 업데이트
SDL_RenderPresent(renderer);

여러 도형 그리기

// 여러 점
SDL_Point points[] = {
    {100, 100}, {150, 120}, {200, 100}, {250, 150}
};
SDL_RenderDrawPoints(renderer, points, 4);
// 여러 선 (연결된)
SDL_RenderDrawLines(renderer, points, 4);
// 여러 사각형
SDL_Rect rects[] = {
    {100, 100, 50, 50},
    {200, 100, 50, 50},
    {300, 100, 50, 50}
};
SDL_RenderDrawRects(renderer, rects, 3);
SDL_RenderFillRects(renderer, rects, 3);

6. 이미지 및 텍스처

SDL_image 사용

#include <SDL2/SDL_image.h>
// SDL_image 초기화
int imgFlags = IMG_INIT_PNG | IMG_INIT_JPG;
if (!(IMG_Init(imgFlags) & imgFlags)) {
    std::cerr << "SDL_image 초기화 실패: " << IMG_GetError() << std::endl;
    return -1;
}
// 이미지 로딩
SDL_Texture* loadTexture(const char* path, SDL_Renderer* renderer) {
    SDL_Surface* surface = IMG_Load(path);
    if (!surface) {
        std::cerr << "이미지 로딩 실패: " << IMG_GetError() << std::endl;
        return nullptr;
    }
    
    SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
    SDL_FreeSurface(surface);
    
    if (!texture) {
        std::cerr << "텍스처 생성 실패: " << SDL_GetError() << std::endl;
        return nullptr;
    }
    
    return texture;
}
// 사용
SDL_Texture* playerTexture = loadTexture("player.png", renderer);
// 렌더링
SDL_Rect destRect = {100, 100, 64, 64};
SDL_RenderCopy(renderer, playerTexture, nullptr, &destRect);
// 정리
SDL_DestroyTexture(playerTexture);
IMG_Quit();

스프라이트 시트

// 스프라이트 시트에서 특정 영역 잘라내기
SDL_Rect srcRect = {0, 0, 32, 32};  // 시트에서의 위치
SDL_Rect destRect = {100, 100, 64, 64};  // 화면에서의 위치
SDL_RenderCopy(renderer, spriteSheet, &srcRect, &destRect);
// 애니메이션
int frameWidth = 32;
int frameHeight = 32;
int currentFrame = 0;
int totalFrames = 8;
// 프레임 업데이트 (60 FPS 기준, 0.1초마다)
if (SDL_GetTicks() % 100 < 16) {
    currentFrame = (currentFrame + 1) % totalFrames;
}
// 현재 프레임 렌더링
SDL_Rect srcRect = {
    currentFrame * frameWidth,
    0,
    frameWidth,
    frameHeight
};
SDL_RenderCopy(renderer, spriteSheet, &srcRect, &destRect);

회전 및 플립

// 회전
double angle = 45.0;  // 각도
SDL_Point center = {32, 32};  // 회전 중심
SDL_RenderCopyEx(renderer, texture, nullptr, &destRect, angle, &center, SDL_FLIP_NONE);
// 좌우 반전
SDL_RenderCopyEx(renderer, texture, nullptr, &destRect, 0, nullptr, SDL_FLIP_HORIZONTAL);
// 상하 반전
SDL_RenderCopyEx(renderer, texture, nullptr, &destRect, 0, nullptr, SDL_FLIP_VERTICAL);
// 회전 + 반전
SDL_RenderCopyEx(renderer, texture, nullptr, &destRect, angle, &center, SDL_FLIP_HORIZONTAL);

7. 텍스트 렌더링

SDL_ttf 사용

#include <SDL2/SDL_ttf.h>
// TTF 초기화
if (TTF_Init() == -1) {
    std::cerr << "SDL_ttf 초기화 실패: " << TTF_GetError() << std::endl;
    return -1;
}
// 폰트 로딩
TTF_Font* font = TTF_OpenFont("arial.ttf", 24);
if (!font) {
    std::cerr << "폰트 로딩 실패: " << TTF_GetError() << std::endl;
    return -1;
}
// 텍스트 렌더링
SDL_Color textColor = {255, 255, 255, 255};  // 흰색
SDL_Surface* textSurface = TTF_RenderText_Solid(font, "Hello SDL!", textColor);
SDL_Texture* textTexture = SDL_CreateTextureFromSurface(renderer, textSurface);
SDL_FreeSurface(textSurface);
// 텍스트 크기 확인
int textWidth, textHeight;
TTF_SizeText(font, "Hello SDL!", &textWidth, &textHeight);
// 렌더링
SDL_Rect textRect = {100, 100, textWidth, textHeight};
SDL_RenderCopy(renderer, textTexture, nullptr, &textRect);
// 정리
SDL_DestroyTexture(textTexture);
TTF_CloseFont(font);
TTF_Quit();

고품질 텍스트:

// Solid (빠름, 낮은 품질)
SDL_Surface* surface = TTF_RenderText_Solid(font, text, color);
// Shaded (중간 품질, 배경색 포함)
SDL_Color bgColor = {0, 0, 0, 255};
SDL_Surface* surface = TTF_RenderText_Shaded(font, text, color, bgColor);
// Blended (느림, 최고 품질, 안티앨리어싱)
SDL_Surface* surface = TTF_RenderText_Blended(font, text, color);
// UTF-8 지원
SDL_Surface* surface = TTF_RenderUTF8_Blended(font, "안녕하세요", color);

8. 오디오

SDL_mixer 사용

#include <SDL2/SDL_mixer.h>
// SDL_mixer 초기화
if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) < 0) {
    std::cerr << "SDL_mixer 초기화 실패: " << Mix_GetError() << std::endl;
    return -1;
}
// 음악 로딩
Mix_Music* music = Mix_LoadMUS("background.mp3");
if (!music) {
    std::cerr << "음악 로딩 실패: " << Mix_GetError() << std::endl;
}
// 효과음 로딩
Mix_Chunk* jumpSound = Mix_LoadWAV("jump.wav");
if (!jumpSound) {
    std::cerr << "효과음 로딩 실패: " << Mix_GetError() << std::endl;
}
// 음악 재생
Mix_PlayMusic(music, -1);  // -1: 무한 반복
// 음악 제어
Mix_VolumeMusic(64);  // 볼륨 (0~128)
Mix_PauseMusic();
Mix_ResumeMusic();
Mix_HaltMusic();
// 효과음 재생
Mix_PlayChannel(-1, jumpSound, 0);  // -1: 자동 채널 선택, 0: 반복 없음
// 효과음 제어
Mix_Volume(-1, 64);  // 모든 채널 볼륨
Mix_Volume(0, 128);  // 채널 0 볼륨
// 정리
Mix_FreeChunk(jumpSound);
Mix_FreeMusic(music);
Mix_CloseAudio();

9. 게임 루프

기본 게임 루프

class Game {
private:
    SDL_Window* window;
    SDL_Renderer* renderer;
    bool running;
    
    Uint32 lastTime;
    float deltaTime;
    
public:
    Game() : window(nullptr), renderer(nullptr), running(false), lastTime(0), deltaTime(0) {}
    
    bool init() {
        if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) {
            std::cerr << "SDL 초기화 실패" << std::endl;
            return false;
        }
        
        window = SDL_CreateWindow(
            "Game",
            SDL_WINDOWPOS_CENTERED,
            SDL_WINDOWPOS_CENTERED,
            800, 600,
            SDL_WINDOW_SHOWN
        );
        
        if (!window) {
            std::cerr << "윈도우 생성 실패" << std::endl;
            return false;
        }
        
        renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
        
        if (!renderer) {
            std::cerr << "렌더러 생성 실패" << std::endl;
            return false;
        }
        
        running = true;
        lastTime = SDL_GetTicks();
        
        return true;
    }
    
    void handleEvents() {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                running = false;
            }
            
            if (event.type == SDL_KEYDOWN) {
                if (event.key.keysym.sym == SDLK_ESCAPE) {
                    running = false;
                }
            }
        }
    }
    
    void update() {
        // 델타 타임 계산
        Uint32 currentTime = SDL_GetTicks();
        deltaTime = (currentTime - lastTime) / 1000.0f;
        lastTime = currentTime;
        
        // 게임 로직
        // 플레이어 이동, 충돌 감지, AI 등
    }
    
    void render() {
        // 화면 지우기
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
        SDL_RenderClear(renderer);
        
        // 게임 오브젝트 그리기
        // ...
        
        // 화면 업데이트
        SDL_RenderPresent(renderer);
    }
    
    void run() {
        while (running) {
            handleEvents();
            update();
            render();
        }
    }
    
    void clean() {
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(window);
        SDL_Quit();
    }
};
int main(int argc, char* argv[]) {
    Game game;
    
    if (!game.init()) {
        return -1;
    }
    
    game.run();
    game.clean();
    
    return 0;
}

FPS 제한

const int FPS = 60;
const int frameDelay = 1000 / FPS;  // 16.67ms
Uint32 frameStart;
int frameTime;
while (running) {
    frameStart = SDL_GetTicks();
    
    handleEvents();
    update();
    render();
    
    frameTime = SDL_GetTicks() - frameStart;
    
    if (frameDelay > frameTime) {
        SDL_Delay(frameDelay - frameTime);
    }
}

10. 충돌 감지

AABB 충돌

AABB (Axis-Aligned Bounding Box):

bool checkCollision(SDL_Rect a, SDL_Rect b) {
    // 왼쪽 끝
    int leftA = a.x;
    int leftB = b.x;
    
    // 오른쪽 끝
    int rightA = a.x + a.w;
    int rightB = b.x + b.w;
    
    // 위쪽 끝
    int topA = a.y;
    int topB = b.y;
    
    // 아래쪽 끝
    int bottomA = a.y + a.h;
    int bottomB = b.y + b.h;
    
    // 충돌 확인
    if (bottomA <= topB) return false;  // A가 B 위에
    if (topA >= bottomB) return false;  // A가 B 아래에
    if (rightA <= leftB) return false;  // A가 B 왼쪽에
    if (leftA >= rightB) return false;  // A가 B 오른쪽에
    
    return true;  // 충돌!
}
// 또는 SDL 내장 함수
bool collision = SDL_HasIntersection(&rectA, &rectB);

원 충돌

struct Circle {
    int x, y;
    int radius;
};
bool checkCollision(Circle a, Circle b) {
    // 두 원의 중심 거리
    int dx = a.x - b.x;
    int dy = a.y - b.y;
    int distanceSquared = dx * dx + dy * dy;
    
    // 반지름 합
    int radiusSum = a.radius + b.radius;
    
    // 거리가 반지름 합보다 작으면 충돌
    return distanceSquared < (radiusSum * radiusSum);
}

점과 사각형 충돌

bool pointInRect(int x, int y, SDL_Rect rect) {
    return (x >= rect.x && x <= rect.x + rect.w &&
            y >= rect.y && y <= rect.y + rect.h);
}
// 마우스 클릭 확인
if (event.type == SDL_MOUSEBUTTONDOWN) {
    int mouseX = event.button.x;
    int mouseY = event.button.y;
    
    if (pointInRect(mouseX, mouseY, buttonRect)) {
        std::cout << "버튼 클릭!" << std::endl;
    }
}

11. 실전 프로젝트

프로젝트 1: Pong 게임

#include <SDL2/SDL.h>
#include <iostream>
const int SCREEN_WIDTH = 800;
const int SCREEN_HEIGHT = 600;
const int PADDLE_WIDTH = 15;
const int PADDLE_HEIGHT = 100;
const int BALL_SIZE = 15;
class Pong {
private:
    SDL_Window* window;
    SDL_Renderer* renderer;
    bool running;
    
    // 패들
    SDL_Rect leftPaddle;
    SDL_Rect rightPaddle;
    int leftPaddleVel;
    int rightPaddleVel;
    
    // 공
    SDL_Rect ball;
    int ballVelX;
    int ballVelY;
    
    // 점수
    int leftScore;
    int rightScore;
    
public:
    Pong() : running(false), leftScore(0), rightScore(0) {
        // 패들 초기화
        leftPaddle = {50, SCREEN_HEIGHT / 2 - PADDLE_HEIGHT / 2, PADDLE_WIDTH, PADDLE_HEIGHT};
        rightPaddle = {SCREEN_WIDTH - 50 - PADDLE_WIDTH, SCREEN_HEIGHT / 2 - PADDLE_HEIGHT / 2, PADDLE_WIDTH, PADDLE_HEIGHT};
        leftPaddleVel = 0;
        rightPaddleVel = 0;
        
        // 공 초기화
        resetBall();
    }
    
    void resetBall() {
        ball = {SCREEN_WIDTH / 2 - BALL_SIZE / 2, SCREEN_HEIGHT / 2 - BALL_SIZE / 2, BALL_SIZE, BALL_SIZE};
        ballVelX = (rand() % 2 == 0) ? -5 : 5;
        ballVelY = (rand() % 10 - 5);
    }
    
    bool init() {
        if (SDL_Init(SDL_INIT_VIDEO) < 0) return false;
        
        window = SDL_CreateWindow("Pong", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
        if (!window) return false;
        
        renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
        if (!renderer) return false;
        
        running = true;
        return true;
    }
    
    void handleEvents() {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                running = false;
            }
        }
        
        // 키보드 상태
        const Uint8* keyState = SDL_GetKeyboardState(nullptr);
        
        // 왼쪽 패들 (W/S)
        leftPaddleVel = 0;
        if (keyState[SDL_SCANCODE_W]) leftPaddleVel = -6;
        if (keyState[SDL_SCANCODE_S]) leftPaddleVel = 6;
        
        // 오른쪽 패들 (Up/Down)
        rightPaddleVel = 0;
        if (keyState[SDL_SCANCODE_UP]) rightPaddleVel = -6;
        if (keyState[SDL_SCANCODE_DOWN]) rightPaddleVel = 6;
    }
    
    void update() {
        // 패들 이동
        leftPaddle.y += leftPaddleVel;
        rightPaddle.y += rightPaddleVel;
        
        // 패들 경계 체크
        if (leftPaddle.y < 0) leftPaddle.y = 0;
        if (leftPaddle.y + PADDLE_HEIGHT > SCREEN_HEIGHT) leftPaddle.y = SCREEN_HEIGHT - PADDLE_HEIGHT;
        if (rightPaddle.y < 0) rightPaddle.y = 0;
        if (rightPaddle.y + PADDLE_HEIGHT > SCREEN_HEIGHT) rightPaddle.y = SCREEN_HEIGHT - PADDLE_HEIGHT;
        
        // 공 이동
        ball.x += ballVelX;
        ball.y += ballVelY;
        
        // 공 벽 충돌 (위/아래)
        if (ball.y <= 0 || ball.y + BALL_SIZE >= SCREEN_HEIGHT) {
            ballVelY = -ballVelY;
        }
        
        // 공 패들 충돌
        if (SDL_HasIntersection(&ball, &leftPaddle) || SDL_HasIntersection(&ball, &rightPaddle)) {
            ballVelX = -ballVelX;
        }
        
        // 득점
        if (ball.x < 0) {
            rightScore++;
            resetBall();
        }
        if (ball.x > SCREEN_WIDTH) {
            leftScore++;
            resetBall();
        }
    }
    
    void render() {
        // 배경
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
        SDL_RenderClear(renderer);
        
        // 중앙선
        SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
        for (int y = 0; y < SCREEN_HEIGHT; y += 20) {
            SDL_Rect line = {SCREEN_WIDTH / 2 - 2, y, 4, 10};
            SDL_RenderFillRect(renderer, &line);
        }
        
        // 패들
        SDL_RenderFillRect(renderer, &leftPaddle);
        SDL_RenderFillRect(renderer, &rightPaddle);
        
        // 공
        SDL_RenderFillRect(renderer, &ball);
        
        SDL_RenderPresent(renderer);
    }
    
    void run() {
        while (running) {
            handleEvents();
            update();
            render();
            SDL_Delay(16);  // ~60 FPS
        }
    }
    
    void clean() {
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(window);
        SDL_Quit();
    }
};
int main(int argc, char* argv[]) {
    Pong game;
    
    if (!game.init()) {
        return -1;
    }
    
    game.run();
    game.clean();
    
    return 0;
}

프로젝트 2: 플랫포머 게임

class Player {
public:
    SDL_Rect rect;
    float velocityX;
    float velocityY;
    bool onGround;
    
    Player(int x, int y) : velocityX(0), velocityY(0), onGround(false) {
        rect = {x, y, 32, 48};
    }
    
    void handleInput(const Uint8* keyState) {
        velocityX = 0;
        
        if (keyState[SDL_SCANCODE_LEFT]) {
            velocityX = -200;  // 픽셀/초
        }
        if (keyState[SDL_SCANCODE_RIGHT]) {
            velocityX = 200;
        }
        if (keyState[SDL_SCANCODE_SPACE] && onGround) {
            velocityY = -500;  // 점프
            onGround = false;
        }
    }
    
    void update(float deltaTime) {
        // 중력
        velocityY += 1000 * deltaTime;  // 픽셀/초²
        
        // 위치 업데이트
        rect.x += velocityX * deltaTime;
        rect.y += velocityY * deltaTime;
        
        // 바닥 충돌
        if (rect.y + rect.h >= 550) {
            rect.y = 550 - rect.h;
            velocityY = 0;
            onGround = true;
        }
        
        // 화면 경계
        if (rect.x < 0) rect.x = 0;
        if (rect.x + rect.w > 800) rect.x = 800 - rect.w;
    }
    
    void render(SDL_Renderer* renderer) {
        SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
        SDL_RenderFillRect(renderer, &rect);
    }
};
class Platform {
public:
    SDL_Rect rect;
    
    Platform(int x, int y, int w, int h) {
        rect = {x, y, w, h};
    }
    
    void render(SDL_Renderer* renderer) {
        SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
        SDL_RenderFillRect(renderer, &rect);
    }
};
class Platformer {
private:
    SDL_Window* window;
    SDL_Renderer* renderer;
    bool running;
    
    Player player;
    std::vector<Platform> platforms;
    
    Uint32 lastTime;
    float deltaTime;
    
public:
    Platformer() : player(100, 100), lastTime(0), deltaTime(0) {
        // 플랫폼 생성
        platforms.push_back(Platform(0, 550, 800, 50));      // 바닥
        platforms.push_back(Platform(200, 450, 150, 20));
        platforms.push_back(Platform(400, 350, 150, 20));
        platforms.push_back(Platform(600, 250, 150, 20));
    }
    
    bool init() {
        if (SDL_Init(SDL_INIT_VIDEO) < 0) return false;
        
        window = SDL_CreateWindow("Platformer", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 800, 600, SDL_WINDOW_SHOWN);
        if (!window) return false;
        
        renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
        if (!renderer) return false;
        
        running = true;
        lastTime = SDL_GetTicks();
        
        return true;
    }
    
    void handleEvents() {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) running = false;
        }
        
        const Uint8* keyState = SDL_GetKeyboardState(nullptr);
        player.handleInput(keyState);
    }
    
    void update() {
        Uint32 currentTime = SDL_GetTicks();
        deltaTime = (currentTime - lastTime) / 1000.0f;
        lastTime = currentTime;
        
        // 플레이어 업데이트
        player.update(deltaTime);
        
        // 플랫폼 충돌 체크
        for (auto& platform : platforms) {
            if (SDL_HasIntersection(&player.rect, &platform.rect)) {
                // 위에서 착지
                if (player.velocityY > 0 && player.rect.y + player.rect.h - 10 < platform.rect.y) {
                    player.rect.y = platform.rect.y - player.rect.h;
                    player.velocityY = 0;
                    player.onGround = true;
                }
            }
        }
    }
    
    void render() {
        SDL_SetRenderDrawColor(renderer, 135, 206, 235, 255);  // 하늘색
        SDL_RenderClear(renderer);
        
        // 플랫폼 렌더링
        for (auto& platform : platforms) {
            platform.render(renderer);
        }
        
        // 플레이어 렌더링
        player.render(renderer);
        
        SDL_RenderPresent(renderer);
    }
    
    void run() {
        while (running) {
            handleEvents();
            update();
            render();
        }
    }
    
    void clean() {
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(window);
        SDL_Quit();
    }
};
int main(int argc, char* argv[]) {
    Platformer game;
    
    if (!game.init()) return -1;
    
    game.run();
    game.clean();
    
    return 0;
}

12. Python에서 SDL 사용

PySDL2 설치

pip install PySDL2 PySDL2-dll

기본 윈도우 (Python)

import sdl2
import sdl2.ext
import sys
def main():
    # SDL 초기화
    sdl2.ext.init()
    
    # 윈도우 생성
    window = sdl2.ext.Window("SDL Python", size=(800, 600))
    window.show()
    
    # 렌더러 생성
    renderer = sdl2.ext.Renderer(window)
    
    # 메인 루프
    running = True
    while running:
        # 이벤트 처리
        events = sdl2.ext.get_events()
        for event in events:
            if event.type == sdl2.SDL_QUIT:
                running = False
            
            if event.type == sdl2.SDL_KEYDOWN:
                if event.key.keysym.sym == sdl2.SDLK_ESCAPE:
                    running = False
        
        # 렌더링
        renderer.clear(sdl2.ext.Color(100, 149, 237))  # 코른플라워 블루
        renderer.present()
    
    # 정리
    sdl2.ext.quit()
    return 0
if __name__ == '__main__':
    sys.exit(main())

간단한 게임 (Python)

import sdl2
import sdl2.ext
import random
class Ball:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.vx = random.randint(-5, 5)
        self.vy = random.randint(-5, 5)
        self.size = 20
        self.color = sdl2.ext.Color(255, 255, 255)
    
    def update(self):
        self.x += self.vx
        self.y += self.vy
        
        # 벽 충돌
        if self.x <= 0 or self.x >= 800 - self.size:
            self.vx = -self.vx
        if self.y <= 0 or self.y >= 600 - self.size:
            self.vy = -self.vy
    
    def draw(self, renderer):
        rect = sdl2.SDL_Rect(int(self.x), int(self.y), self.size, self.size)
        renderer.fill((rect,), self.color)
def main():
    sdl2.ext.init()
    
    window = sdl2.ext.Window("Bouncing Balls", size=(800, 600))
    window.show()
    
    renderer = sdl2.ext.Renderer(window)
    
    # 공 생성
    balls = [Ball(random.randint(0, 780), random.randint(0, 580)) for _ in range(10)]
    
    running = True
    clock = sdl2.ext.time.Clock()
    
    while running:
        # 이벤트
        events = sdl2.ext.get_events()
        for event in events:
            if event.type == sdl2.SDL_QUIT:
                running = False
            
            # 클릭 시 공 추가
            if event.type == sdl2.SDL_MOUSEBUTTONDOWN:
                balls.append(Ball(event.button.x, event.button.y))
        
        # 업데이트
        for ball in balls:
            ball.update()
        
        # 렌더링
        renderer.clear(sdl2.ext.Color(0, 0, 0))
        
        for ball in balls:
            ball.draw(renderer)
        
        renderer.present()
        
        # FPS 제한
        clock.tick(60)
    
    sdl2.ext.quit()
if __name__ == '__main__':
    main()

13. SDL + OpenGL

OpenGL 컨텍스트 생성

#include <SDL2/SDL.h>
#include <glad/glad.h>
int main(int argc, char* argv[]) {
    SDL_Init(SDL_INIT_VIDEO);
    
    // OpenGL 속성 설정
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
    
    // 윈도우 생성 (OpenGL 플래그)
    SDL_Window* window = SDL_CreateWindow(
        "SDL + OpenGL",
        SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED,
        800, 600,
        SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN
    );
    
    // OpenGL 컨텍스트 생성
    SDL_GLContext context = SDL_GL_CreateContext(window);
    
    // GLAD 초기화
    if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress)) {
        std::cerr << "GLAD 초기화 실패" << std::endl;
        return -1;
    }
    
    // VSync 활성화
    SDL_GL_SetSwapInterval(1);
    
    // OpenGL 설정
    glViewport(0, 0, 800, 600);
    glEnable(GL_DEPTH_TEST);
    
    // 메인 루프
    bool running = true;
    SDL_Event event;
    
    while (running) {
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) running = false;
        }
        
        // OpenGL 렌더링
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        
        // 여기에 OpenGL 렌더링 코드...
        
        SDL_GL_SwapWindow(window);
    }
    
    // 정리
    SDL_GL_DeleteContext(context);
    SDL_DestroyWindow(window);
    SDL_Quit();
    
    return 0;
}

14. 고급 렌더링

알파 블렌딩

// 텍스처에 알파 블렌딩 활성화
SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND);
// 텍스처 투명도 설정
SDL_SetTextureAlphaMod(texture, 128);  // 0~255 (128 = 50% 투명)
// 색상 모드 설정
SDL_SetTextureColorMod(texture, 255, 0, 0);  // 빨간색 틴트

렌더 타겟

// 텍스처를 렌더 타겟으로 생성
SDL_Texture* targetTexture = SDL_CreateTexture(
    renderer,
    SDL_PIXELFORMAT_RGBA8888,
    SDL_TEXTUREACCESS_TARGET,
    800, 600
);
// 렌더 타겟 변경
SDL_SetRenderTarget(renderer, targetTexture);
// 이 텍스처에 렌더링
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
SDL_RenderClear(renderer);
// ....더 많은 렌더링 ...
// 기본 렌더 타겟으로 복원
SDL_SetRenderTarget(renderer, nullptr);
// 렌더링된 텍스처 사용
SDL_RenderCopy(renderer, targetTexture, nullptr, nullptr);

스케일 및 논리 크기

// 논리 크기 설정 (자동 스케일링)
SDL_RenderSetLogicalSize(renderer, 800, 600);
// 이제 윈도우 크기가 변해도 800×600으로 렌더링됨
// 자동으로 스케일링됨
// 스케일 품질 설정
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");  // 선형 필터링
// "0": 최근접 이웃 (픽셀 아트)
// "1": 선형 (부드러움)
// "2": 이방성 (최고 품질)

15. 타이머 및 시간

기본 타이머

// 시작 시간
Uint32 startTime = SDL_GetTicks();  // 밀리초
// 경과 시간
Uint32 elapsed = SDL_GetTicks() - startTime;
std::cout << "경과: " << elapsed << "ms" << std::endl;
// 대기
SDL_Delay(1000);  // 1초 대기
// 고해상도 타이머
Uint64 start = SDL_GetPerformanceCounter();
// ....작업 ...
Uint64 end = SDL_GetPerformanceCounter();
double elapsed = (end - start) / (double)SDL_GetPerformanceFrequency();
std::cout << "경과: " << elapsed << "초" << std::endl;

FPS 카운터

class FPSCounter {
private:
    Uint32 frameCount;
    Uint32 lastTime;
    float fps;
    
public:
    FPSCounter() : frameCount(0), lastTime(SDL_GetTicks()), fps(0) {}
    
    void update() {
        frameCount++;
        
        Uint32 currentTime = SDL_GetTicks();
        Uint32 elapsed = currentTime - lastTime;
        
        // 1초마다 FPS 계산
        if (elapsed >= 1000) {
            fps = frameCount / (elapsed / 1000.0f);
            frameCount = 0;
            lastTime = currentTime;
        }
    }
    
    float getFPS() const {
        return fps;
    }
};
// 사용
FPSCounter fpsCounter;
while (running) {
    // ...
    
    fpsCounter.update();
    
    // FPS 표시
    if (SDL_GetTicks() % 1000 < 16) {
        std::cout << "FPS: " << fpsCounter.getFPS() << std::endl;
    }
}

16. 파티클 시스템

struct Particle {
    float x, y;
    float vx, vy;
    Uint8 r, g, b, a;
    float life;
    float maxLife;
};
class ParticleSystem {
private:
    std::vector<Particle> particles;
    int maxParticles;
    
public:
    ParticleSystem(int max) : maxParticles(max) {
        particles.reserve(maxParticles);
    }
    
    void emit(float x, float y, int count) {
        for (int i = 0; i < count && particles.size() < maxParticles; i++) {
            Particle p;
            p.x = x;
            p.y = y;
            
            // 랜덤 속도
            float angle = (rand() % 360) * 3.14159f / 180.0f;
            float speed = (rand() % 100 + 50) / 10.0f;
            p.vx = cos(angle) * speed;
            p.vy = sin(angle) * speed;
            
            // 랜덤 색상
            p.r = rand() % 256;
            p.g = rand() % 256;
            p.b = rand() % 256;
            p.a = 255;
            
            p.life = 1.0f;
            p.maxLife = 1.0f;
            
            particles.push_back(p);
        }
    }
    
    void update(float deltaTime) {
        for (auto it = particles.begin(); it != particles.end();) {
            it->life -= deltaTime;
            
            if (it->life <= 0) {
                it = particles.erase(it);
            } else {
                // 위치 업데이트
                it->x += it->vx * deltaTime * 60;
                it->y += it->vy * deltaTime * 60;
                
                // 중력
                it->vy += 9.8f * deltaTime * 60;
                
                // 페이드 아웃
                it->a = (Uint8)(255 * (it->life / it->maxLife));
                
                ++it;
            }
        }
    }
    
    void render(SDL_Renderer* renderer) {
        for (const auto& p : particles) {
            SDL_SetRenderDrawColor(renderer, p.r, p.g, p.b, p.a);
            SDL_Rect rect = {(int)p.x, (int)p.y, 4, 4};
            SDL_RenderFillRect(renderer, &rect);
        }
    }
};
// 사용
ParticleSystem particles(1000);
// 마우스 클릭 시 파티클 방출
if (event.type == SDL_MOUSEBUTTONDOWN) {
    particles.emit(event.button.x, event.button.y, 50);
}
// 업데이트 및 렌더링
particles.update(deltaTime);
particles.render(renderer);

17. 타일맵

타일맵 클래스

class TileMap {
private:
    std::vector<std::vector<int>> map;
    SDL_Texture* tileset;
    int tileSize;
    int mapWidth;
    int mapHeight;
    
public:
    TileMap(const std::string& filename, SDL_Texture* tileset, int tileSize)
        : tileset(tileset), tileSize(tileSize) {
        loadMap(filename);
    }
    
    void loadMap(const std::string& filename) {
        std::ifstream file(filename);
        std::string line;
        
        while (std::getline(file, line)) {
            std::vector<int> row;
            std::stringstream ss(line);
            int tile;
            
            while (ss >> tile) {
                row.push_back(tile);
                if (ss.peek() == ',') ss.ignore();
            }
            
            map.push_back(row);
        }
        
        mapHeight = map.size();
        mapWidth = map[0].size();
    }
    
    void render(SDL_Renderer* renderer, int cameraX, int cameraY) {
        for (int y = 0; y < mapHeight; y++) {
            for (int x = 0; x < mapWidth; x++) {
                int tileID = map[y][x];
                
                if (tileID == 0) continue;  // 빈 타일
                
                // 타일셋에서 타일 위치 계산
                int tilesPerRow = 8;  // 타일셋 가로 타일 수
                int srcX = (tileID % tilesPerRow) * tileSize;
                int srcY = (tileID / tilesPerRow) * tileSize;
                
                SDL_Rect srcRect = {srcX, srcY, tileSize, tileSize};
                SDL_Rect destRect = {
                    x * tileSize - cameraX,
                    y * tileSize - cameraY,
                    tileSize,
                    tileSize
                };
                
                SDL_RenderCopy(renderer, tileset, &srcRect, &destRect);
            }
        }
    }
    
    int getTile(int x, int y) const {
        int tileX = x / tileSize;
        int tileY = y / tileSize;
        
        if (tileX < 0 || tileX >= mapWidth || tileY < 0 || tileY >= mapHeight) {
            return 0;
        }
        
        return map[tileY][tileX];
    }
};
// 맵 파일 (map.txt)
/*
1,1,1,1,1,1,1,1,1,1
1,0,0,0,0,0,0,0,0,1
1,0,2,2,0,0,3,3,0,1
1,0,0,0,0,0,0,0,0,1
1,1,1,1,1,1,1,1,1,1
*/
// 사용
TileMap tileMap("map.txt", tilesetTexture, 32);
// 카메라
int cameraX = playerX - 400;
int cameraY = playerY - 300;
tileMap.render(renderer, cameraX, cameraY);

18. 애니메이션

스프라이트 애니메이션 클래스

class Animation {
private:
    SDL_Texture* spriteSheet;
    std::vector<SDL_Rect> frames;
    int currentFrame;
    float frameTime;
    float elapsed;
    bool loop;
    
public:
    Animation(SDL_Texture* sheet, const std::vector<SDL_Rect>& frames, float frameTime, bool loop = true)
        : spriteSheet(sheet), frames(frames), currentFrame(0), frameTime(frameTime), elapsed(0), loop(loop) {}
    
    void update(float deltaTime) {
        elapsed += deltaTime;
        
        if (elapsed >= frameTime) {
            elapsed = 0;
            currentFrame++;
            
            if (currentFrame >= frames.size()) {
                if (loop) {
                    currentFrame = 0;
                } else {
                    currentFrame = frames.size() - 1;
                }
            }
        }
    }
    
    void render(SDL_Renderer* renderer, int x, int y, int width, int height, SDL_RendererFlip flip = SDL_FLIP_NONE) {
        SDL_Rect destRect = {x, y, width, height};
        SDL_RenderCopyEx(renderer, spriteSheet, &frames[currentFrame], &destRect, 0, nullptr, flip);
    }
    
    void reset() {
        currentFrame = 0;
        elapsed = 0;
    }
    
    bool isFinished() const {
        return !loop && currentFrame == frames.size() - 1;
    }
};
// 사용
std::vector<SDL_Rect> walkFrames = {
    {0, 0, 32, 48},
    {32, 0, 32, 48},
    {64, 0, 32, 48},
    {96, 0, 32, 48}
};
Animation walkAnimation(playerSheet, walkFrames, 0.1f, true);
// 게임 루프
walkAnimation.update(deltaTime);
walkAnimation.render(renderer, playerX, playerY, 64, 96);

19. 상태 머신

게임 상태 관리

enum class GameState {
    MENU,
    PLAYING,
    PAUSED,
    GAME_OVER
};
class Game {
private:
    GameState currentState;
    
public:
    void handleEvents(SDL_Event& event) {
        switch (currentState) {
            case GameState::MENU:
                handleMenuEvents(event);
                break;
            case GameState::PLAYING:
                handlePlayingEvents(event);
                break;
            case GameState::PAUSED:
                handlePausedEvents(event);
                break;
            case GameState::GAME_OVER:
                handleGameOverEvents(event);
                break;
        }
    }
    
    void update(float deltaTime) {
        switch (currentState) {
            case GameState::MENU:
                updateMenu(deltaTime);
                break;
            case GameState::PLAYING:
                updatePlaying(deltaTime);
                break;
            case GameState::PAUSED:
                // 일시정지 중에는 업데이트 안함
                break;
            case GameState::GAME_OVER:
                updateGameOver(deltaTime);
                break;
        }
    }
    
    void render() {
        switch (currentState) {
            case GameState::MENU:
                renderMenu();
                break;
            case GameState::PLAYING:
                renderPlaying();
                break;
            case GameState::PAUSED:
                renderPlaying();  // 게임 화면
                renderPauseOverlay();  // 일시정지 오버레이
                break;
            case GameState::GAME_OVER:
                renderGameOver();
                break;
        }
    }
    
    void changeState(GameState newState) {
        currentState = newState;
    }
};

20. 실전 게임: Space Shooter

#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <SDL2/SDL_mixer.h>
#include <vector>
#include <iostream>
const int SCREEN_WIDTH = 800;
const int SCREEN_HEIGHT = 600;
class Entity {
public:
    float x, y;
    int width, height;
    SDL_Texture* texture;
    
    Entity(float x, float y, int w, int h, SDL_Texture* tex)
        : x(x), y(y), width(w), height(h), texture(tex) {}
    
    SDL_Rect getRect() const {
        return {(int)x, (int)y, width, height};
    }
    
    void render(SDL_Renderer* renderer) {
        SDL_Rect rect = getRect();
        SDL_RenderCopy(renderer, texture, nullptr, &rect);
    }
};
class Player : public Entity {
public:
    float speed;
    int health;
    
    Player(float x, float y, SDL_Texture* tex)
        : Entity(x, y, 64, 64, tex), speed(300), health(100) {}
    
    void handleInput(const Uint8* keyState, float deltaTime) {
        if (keyState[SDL_SCANCODE_LEFT]) x -= speed * deltaTime;
        if (keyState[SDL_SCANCODE_RIGHT]) x += speed * deltaTime;
        if (keyState[SDL_SCANCODE_UP]) y -= speed * deltaTime;
        if (keyState[SDL_SCANCODE_DOWN]) y += speed * deltaTime;
        
        // 화면 경계
        if (x < 0) x = 0;
        if (x + width > SCREEN_WIDTH) x = SCREEN_WIDTH - width;
        if (y < 0) y = 0;
        if (y + height > SCREEN_HEIGHT) y = SCREEN_HEIGHT - height;
    }
};
class Bullet : public Entity {
public:
    float speed;
    
    Bullet(float x, float y, SDL_Texture* tex)
        : Entity(x, y, 8, 16, tex), speed(500) {}
    
    void update(float deltaTime) {
        y -= speed * deltaTime;
    }
    
    bool isOffScreen() const {
        return y + height < 0;
    }
};
class Enemy : public Entity {
public:
    float speed;
    
    Enemy(float x, float y, SDL_Texture* tex)
        : Entity(x, y, 48, 48, tex), speed(100) {}
    
    void update(float deltaTime) {
        y += speed * deltaTime;
    }
    
    bool isOffScreen() const {
        return y > SCREEN_HEIGHT;
    }
};
class SpaceShooter {
private:
    SDL_Window* window;
    SDL_Renderer* renderer;
    bool running;
    
    Player* player;
    std::vector<Bullet*> bullets;
    std::vector<Enemy*> enemies;
    
    SDL_Texture* playerTexture;
    SDL_Texture* bulletTexture;
    SDL_Texture* enemyTexture;
    
    Mix_Chunk* shootSound;
    Mix_Chunk* explosionSound;
    Mix_Music* bgMusic;
    
    Uint32 lastTime;
    float deltaTime;
    
    Uint32 lastShootTime;
    Uint32 shootCooldown;
    
    Uint32 lastEnemySpawn;
    Uint32 enemySpawnInterval;
    
    int score;
    
public:
    SpaceShooter() : running(false), lastTime(0), deltaTime(0),
                     lastShootTime(0), shootCooldown(200),
                     lastEnemySpawn(0), enemySpawnInterval(1000),
                     score(0) {}
    
    bool init() {
        if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) return false;
        
        window = SDL_CreateWindow("Space Shooter", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
        if (!window) return false;
        
        renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
        if (!renderer) return false;
        
        // SDL_image 초기화
        IMG_Init(IMG_INIT_PNG);
        
        // SDL_mixer 초기화
        Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048);
        
        // 리소스 로딩
        playerTexture = IMG_LoadTexture(renderer, "player.png");
        bulletTexture = IMG_LoadTexture(renderer, "bullet.png");
        enemyTexture = IMG_LoadTexture(renderer, "enemy.png");
        
        shootSound = Mix_LoadWAV("shoot.wav");
        explosionSound = Mix_LoadWAV("explosion.wav");
        bgMusic = Mix_LoadMUS("background.mp3");
        
        // 플레이어 생성
        player = new Player(SCREEN_WIDTH / 2 - 32, SCREEN_HEIGHT - 100, playerTexture);
        
        // 음악 재생
        Mix_PlayMusic(bgMusic, -1);
        
        running = true;
        lastTime = SDL_GetTicks();
        
        return true;
    }
    
    void handleEvents() {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                running = false;
            }
            
            if (event.type == SDL_KEYDOWN) {
                if (event.key.keysym.sym == SDLK_ESCAPE) {
                    running = false;
                }
            }
        }
        
        // 연속 입력
        const Uint8* keyState = SDL_GetKeyboardState(nullptr);
        player->handleInput(keyState, deltaTime);
        
        // 발사 (스페이스바)
        if (keyState[SDL_SCANCODE_SPACE]) {
            Uint32 currentTime = SDL_GetTicks();
            if (currentTime - lastShootTime >= shootCooldown) {
                bullets.push_back(new Bullet(player->x + player->width / 2 - 4, player->y, bulletTexture));
                Mix_PlayChannel(-1, shootSound, 0);
                lastShootTime = currentTime;
            }
        }
    }
    
    void update() {
        Uint32 currentTime = SDL_GetTicks();
        deltaTime = (currentTime - lastTime) / 1000.0f;
        lastTime = currentTime;
        
        // 총알 업데이트
        for (auto it = bullets.begin(); it != bullets.end();) {
            (*it)->update(deltaTime);
            
            if ((*it)->isOffScreen()) {
                delete *it;
                it = bullets.erase(it);
            } else {
                ++it;
            }
        }
        
        // 적 생성
        if (currentTime - lastEnemySpawn >= enemySpawnInterval) {
            int x = rand() % (SCREEN_WIDTH - 48);
            enemies.push_back(new Enemy(x, -48, enemyTexture));
            lastEnemySpawn = currentTime;
        }
        
        // 적 업데이트
        for (auto it = enemies.begin(); it != enemies.end();) {
            (*it)->update(deltaTime);
            
            if ((*it)->isOffScreen()) {
                delete *it;
                it = enemies.erase(it);
            } else {
                ++it;
            }
        }
        
        // 충돌 감지 (총알 vs 적)
        for (auto bulletIt = bullets.begin(); bulletIt != bullets.end();) {
            bool bulletHit = false;
            
            for (auto enemyIt = enemies.begin(); enemyIt != enemies.end();) {
                if (SDL_HasIntersection(&(*bulletIt)->getRect(), &(*enemyIt)->getRect())) {
                    // 충돌!
                    Mix_PlayChannel(-1, explosionSound, 0);
                    score += 10;
                    
                    delete *enemyIt;
                    enemyIt = enemies.erase(enemyIt);
                    
                    bulletHit = true;
                    break;
                } else {
                    ++enemyIt;
                }
            }
            
            if (bulletHit) {
                delete *bulletIt;
                bulletIt = bullets.erase(bulletIt);
            } else {
                ++bulletIt;
            }
        }
        
        // 충돌 감지 (플레이어 vs 적)
        for (auto& enemy : enemies) {
            if (SDL_HasIntersection(&player->getRect(), &enemy->getRect())) {
                player->health -= 10;
                std::cout << "피격! 체력: " << player->health << std::endl;
                
                if (player->health <= 0) {
                    std::cout << "게임 오버! 점수: " << score << std::endl;
                    running = false;
                }
            }
        }
    }
    
    void render() {
        // 배경
        SDL_SetRenderDrawColor(renderer, 0, 0, 20, 255);
        SDL_RenderClear(renderer);
        
        // 플레이어
        player->render(renderer);
        
        // 총알
        for (auto& bullet : bullets) {
            bullet->render(renderer);
        }
        
        // 적
        for (auto& enemy : enemies) {
            enemy->render(renderer);
        }
        
        SDL_RenderPresent(renderer);
    }
    
    void run() {
        while (running) {
            handleEvents();
            update();
            render();
        }
    }
    
    void clean() {
        // 엔티티 정리
        delete player;
        for (auto& bullet : bullets) delete bullet;
        for (auto& enemy : enemies) delete enemy;
        
        // 텍스처 정리
        SDL_DestroyTexture(playerTexture);
        SDL_DestroyTexture(bulletTexture);
        SDL_DestroyTexture(enemyTexture);
        
        // 오디오 정리
        Mix_FreeChunk(shootSound);
        Mix_FreeChunk(explosionSound);
        Mix_FreeMusic(bgMusic);
        Mix_CloseAudio();
        
        // SDL 정리
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(window);
        IMG_Quit();
        SDL_Quit();
    }
};
int main(int argc, char* argv[]) {
    SpaceShooter game;
    
    if (!game.init()) {
        return -1;
    }
    
    game.run();
    game.clean();
    
    return 0;
}

21. 네트워킹 (SDL_net)

서버

#include <SDL2/SDL_net.h>
// SDL_net 초기화
SDLNet_Init();
// 서버 소켓 생성
IPaddress ip;
SDLNet_ResolveHost(&ip, nullptr, 1234);  // 포트 1234
TCPsocket server = SDLNet_TCP_Open(&ip);
if (!server) {
    std::cerr << "서버 소켓 생성 실패: " << SDLNet_GetError() << std::endl;
    return -1;
}
std::cout << "서버 시작 (포트 1234)" << std::endl;
// 클라이언트 연결 대기
TCPsocket client = SDLNet_TCP_Accept(server);
if (client) {
    std::cout << "클라이언트 연결됨" << std::endl;
    
    // 데이터 수신
    char buffer[512];
    int received = SDLNet_TCP_Recv(client, buffer, 512);
    if (received > 0) {
        buffer[received] = '\0';
        std::cout << "받음: " << buffer << std::endl;
    }
    
    // 데이터 송신
    const char* message = "Hello from server!";
    SDLNet_TCP_Send(client, message, strlen(message) + 1);
    
    SDLNet_TCP_Close(client);
}
SDLNet_TCP_Close(server);
SDLNet_Quit();

클라이언트

// 서버 연결
IPaddress ip;
SDLNet_ResolveHost(&ip, "127.0.0.1", 1234);
TCPsocket client = SDLNet_TCP_Open(&ip);
if (!client) {
    std::cerr << "서버 연결 실패: " << SDLNet_GetError() << std::endl;
    return -1;
}
std::cout << "서버 연결됨" << std::endl;
// 데이터 송신
const char* message = "Hello from client!";
SDLNet_TCP_Send(client, message, strlen(message) + 1);
// 데이터 수신
char buffer[512];
int received = SDLNet_TCP_Recv(client, buffer, 512);
if (received > 0) {
    buffer[received] = '\0';
    std::cout << "받음: " << buffer << std::endl;
}
SDLNet_TCP_Close(client);
SDLNet_Quit();

22. 최적화

텍스처 아틀라스

// ❌ 느림 (텍스처 바인딩 많음)
for (auto& sprite : sprites) {
    SDL_RenderCopy(renderer, sprite.texture, nullptr, &sprite.rect);
}
// ✅ 빠름 (텍스처 아틀라스)
SDL_Texture* atlas = loadTexture("atlas.png", renderer);
for (auto& sprite : sprites) {
    SDL_RenderCopy(renderer, atlas, &sprite.srcRect, &sprite.destRect);
}

오브젝트 풀링

template<typename T>
class ObjectPool {
private:
    std::vector<T*> pool;
    std::vector<T*> active;
    
public:
    ObjectPool(int size) {
        for (int i = 0; i < size; i++) {
            pool.push_back(new T());
        }
    }
    
    T* acquire() {
        if (pool.empty()) {
            return new T();
        }
        
        T* obj = pool.back();
        pool.pop_back();
        active.push_back(obj);
        return obj;
    }
    
    void release(T* obj) {
        auto it = std::find(active.begin(), active.end(), obj);
        if (it != active.end()) {
            active.erase(it);
            pool.push_back(obj);
        }
    }
    
    ~ObjectPool() {
        for (auto obj : pool) delete obj;
        for (auto obj : active) delete obj;
    }
};
// 사용
ObjectPool<Bullet> bulletPool(100);
// 총알 생성
Bullet* bullet = bulletPool.acquire();
bullet->init(x, y);
// 총알 제거
if (bullet->isOffScreen()) {
    bulletPool.release(bullet);
}

23. 디버깅

렌더링 디버그

// FPS 표시
void renderDebugInfo(SDL_Renderer* renderer, TTF_Font* font, float fps) {
    char text[64];
    sprintf(text, "FPS: %.1f", fps);
    
    SDL_Color white = {255, 255, 255, 255};
    SDL_Surface* surface = TTF_RenderText_Solid(font, text, white);
    SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
    
    SDL_Rect rect = {10, 10, surface->w, surface->h};
    SDL_RenderCopy(renderer, texture, nullptr, &rect);
    
    SDL_FreeSurface(surface);
    SDL_DestroyTexture(texture);
}
// 충돌 박스 표시
void renderCollisionBox(SDL_Renderer* renderer, const SDL_Rect& rect) {
    SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
    SDL_RenderDrawRect(renderer, &rect);
}
// 그리드 표시
void renderGrid(SDL_Renderer* renderer, int gridSize) {
    SDL_SetRenderDrawColor(renderer, 50, 50, 50, 255);
    
    for (int x = 0; x < SCREEN_WIDTH; x += gridSize) {
        SDL_RenderDrawLine(renderer, x, 0, x, SCREEN_HEIGHT);
    }
    
    for (int y = 0; y < SCREEN_HEIGHT; y += gridSize) {
        SDL_RenderDrawLine(renderer, 0, y, SCREEN_WIDTH, y);
    }
}

24. 트러블슈팅

일반적인 문제

1) “SDL.h: No such file or directory”

# 원인: SDL 헤더 경로 문제
# 해결: 컴파일 시 인클루드 경로 추가
# macOS/Linux
g++ main.cpp -I/usr/local/include/SDL2 -L/usr/local/lib -lSDL2
# 또는 pkg-config 사용
g++ main.cpp `pkg-config --cflags --libs sdl2`

2) “Undefined reference to SDL_main”

// 원인: SDL_main 매크로 문제
// 해결 1: main 함수 시그니처 변경
int main(int argc, char* argv[]) {
    // ...
}
// 해결 2: SDL_main 비활성화
#define SDL_MAIN_HANDLED
#include <SDL2/SDL.h>

3) 검은 화면

// 원인 1: RenderPresent 누락
SDL_RenderPresent(renderer);  // 필수!
// 원인 2: 텍스처가 화면 밖
// 좌표 확인
// 원인 3: 알파 블렌딩 문제
SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND);

4) 느린 렌더링

// 원인 1: VSync 비활성화
SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
// 원인 2: 소프트웨어 렌더링
// SDL_RENDERER_ACCELERATED 플래그 확인
// 원인 3: 불필요한 렌더링
// 변경된 부분만 렌더링 (더티 렉트)

25. 모바일 (Android/iOS)

Android 빌드

# SDL2 Android 프로젝트 생성
# 1. SDL2 소스 다운로드
# 2. android-project 폴더 복사
# 3. jni/src에 코드 복사
# Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := main
LOCAL_SRC_FILES := main.cpp
LOCAL_SHARED_LIBRARIES := SDL2
LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -llog
include $(BUILD_SHARED_LIBRARY)
# 빌드
ndk-build
ant debug
adb install -r bin/MyGame-debug.apk

iOS 빌드

# Xcode 프로젝트 생성
# 1. SDL2.framework 추가
# 2. main.cpp 추가
# 3. Bridging Header 설정
# main.cpp는 동일하게 사용 가능
# iOS 특화 기능 (터치, 가속도계)은 SDL 이벤트로 처리

FAQ

Q1. SDL 1.2와 SDL2는? 1.2는 유지보수가 끊긴 지 오래됐고, SDL2가 렌더러·입력·모바일 쪽을 정리한 현재 기준이다. 새 프로젝트는 SDL2로 보면 된다. Q2. SDL과 SFML은 어떻게 고르나? SDL은 C 중심 API이고 바인딩이 많다. SFML은 C++에 맞춘 설계가 강하다. 모바일·타깃 범위를 먼저 정하고 선택하는 경우가 많다. Q3. SDL만으로 3D를 하나? SDL 자체 렌더러는 2D에 가깝다. 3D는 OpenGL·Vulkan 컨텍스트를 SDL에 붙여 쓰는 식으로 한다. Q4. 엔진과 SDL은? 엔진은 툴링·워크플로가 포함되어 생산성이 좋다. SDL은 가볍지만 렌더링·물리·오디오 파이프를 직접 구성해야 한다. 학습·소규모·특수 요구면 SDL, 제품 일정이 빠듯하면 엔진을 많이 고른다. Q5. 어떤 언어로 SDL을 쓰나? 공식은 C이고, C++·Python 등 바인딩이 있다. 성능이 크리티컬하면 C/C++, 실험·도구면 Python 같은 선택도 흔하다.

요약

핵심 정리

SDL 기본 구조:

// 1. 초기화
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
IMG_Init(IMG_INIT_PNG);
TTF_Init();
Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048);
// 2. 윈도우 및 렌더러
SDL_Window* window = SDL_CreateWindow(...);
SDL_Renderer* renderer = SDL_CreateRenderer(...);
// 3. 리소스 로딩
SDL_Texture* texture = IMG_LoadTexture(renderer, "image.png");
TTF_Font* font = TTF_OpenFont("font.ttf", 24);
Mix_Music* music = Mix_LoadMUS("music.mp3");
// 4. 게임 루프
while (running) {
    // 이벤트
    while (SDL_PollEvent(&event)) { ....}
    
    // 업데이트
    // 게임 로직...
    
    // 렌더링
    SDL_RenderClear(renderer);
    SDL_RenderCopy(renderer, texture, nullptr, &rect);
    SDL_RenderPresent(renderer);
}
// 5. 정리
SDL_DestroyTexture(texture);
TTF_CloseFont(font);
Mix_FreeMusic(music);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
Mix_CloseAudio();
TTF_Quit();
IMG_Quit();
SDL_Quit();

필수 개념:

// 실행 예제
1. 게임 루프: Input → Update → Render
2. 델타 타임: FPS 무관한 움직임
3. 이벤트: 키보드, 마우스, 게임패드
4. 텍스처: GPU 가속 이미지
5. 충돌 감지: AABB, 원
6. 상태 관리: 메뉴, 플레이, 일시정지

학습 로드맵

창·이벤트·클리어·프레젠트를 먼저 안정화하고, 텍스처·스프라이트·충돌로 2D를 만든 뒤 게임 루프·상태·사운드를 붙인다. Pong·플랫포머·슈팅처럼 작은 것을 끝까지 완성하는 경험이 이후에 크게 도움이 된다.

추천 게임 프로젝트

초반에는 Pong·Breakout·Snake처럼 규칙이 명확한 것부터, 중간에는 플랫포머·슈팅·테트리스로 범위를 넓힌다. RPG·TD·멀티플레이는 시스템이 많아져서 앞 단계가 잡힌 뒤가 수월하다.

다음 글 추천


키워드: SDL, SDL2, Game, Graphics, Audio, 게임, 게임개발, C++, Python, OpenGL, 2D Game

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「SDL2 실전 가이드 | 게임 개발 입문」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「SDL2 실전 가이드 | 게임 개발 입문」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

SDL, SDL2, Game, Graphics, Audio, 게임, 게임개발, C++, Python, OpenGL 등으로 검색하시면 이 글이 도움이 됩니다.