Notice
Recent Comments
Recent Posts
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Link
Today
Total
관리 메뉴

[게임개발자] 레드핑

나쁜 코드의 가치: 완벽하지 않아도 괜찮아 본문

TIL

나쁜 코드의 가치: 완벽하지 않아도 괜찮아

레드핑(redping) 2025. 3. 21. 09:37

 

안녕하세요, 여러분! 오늘은 게임 개발에서 종종 간과되지만 매우 중요한 주제인 "나쁜 코드의 가치"에 대해 이야기해보려고 합니다. 저는 현재 Godot 엔진으로 인디 게임을 개발 중인데요, 스킬 시스템(예: BleedSkillTimer를 활용한 쿨다운 로직)을 구현하면서 깨달은 점들을 공유해볼게요. 특히, "완벽한 코드"만 고집하다가 놓칠 수 있는 기회와, "나쁜 코드"가 가진 의외의 장점에 대해 이야기해보려 합니다.


나쁜 코드란 무엇일까?

먼저 "나쁜 코드"가 뭔지 정의해볼게요. 나쁜 코드는 보통 다음과 같은 특징을 가집니다:

  • 가독성이 낮음: 변수 이름이 a, b 같은 의미 없는 이름이거나, 주석이 없어서 이해하기 어려움.
  • 구조가 엉망: 한 함수가 500줄 넘게 길거나, 모든 로직이 if-else로 얽혀 있음.
  • 유지보수 불가: 새로운 기능을 추가하려면 코드를 전부 뜯어고쳐야 함.
  • 테스트 불가: 단위 테스트(Unit Test)를 작성하기 어려운 구조.

게임 개발자라면 이런 코드를 보면 "이건 정말 고쳐야 해!"라는 생각이 들 거예요. 저도 처음엔 그랬어요. 하지만, 나쁜 코드에도 가치가 있다는 걸 깨닫게 된 계기가 있었죠.


게임 개발에서 나쁜 코드의 필요성

게임 개발은 다른 소프트웨어 개발과는 조금 다릅니다. 특히 인디 게임 개발에서는 빠른 실험과 반복이 성공의 열쇠입니다. 게임 디자인이 종이 위에서 완벽하게 설계될 수는 없어요. 플레이어가 실제로 게임을 플레이해봐야 "이 스킬이 재미있네!" 또는 "이 쿨다운 시간이 너무 길어!" 같은 피드백을 얻을 수 있죠.

제가 Godot에서 BleedSkill 시스템을 구현할 때도 비슷한 경험을 했어요. 처음엔 깔끔한 아키텍처를 설계하려고 했죠. SkillManager 싱글톤을 만들어 스킬 상태를 관리하고, 시그널을 동적으로 연결하며, 모든 스킬 노드가 독립적으로 동작하도록 설계했어요. SOLID 원칙도 지키고, 디커플링도 신경 썼죠. 하지만 문제는… 너무 오래 걸렸다는 거예요.

스킬 하나를 테스트하려면 설계, 구현, 디버깅까지 며칠이 걸렸어요. 그런데 막상 플레이어가 테스트해보니 "이 스킬, 별로 재미없네요"라는 피드백이 돌아왔죠. 그때 깨달았어요. 완벽한 코드를 작성하는 데 시간을 쏟는 대신, 빠르게 테스트해볼 수 있는 코드를 먼저 작성하는 게 더 중요하다는 걸.


나쁜 코드의 진짜 가치: 빠른 실험

게임 개발에서 "나쁜 코드"는 프로토타이핑(prototyping)의 핵심 도구입니다. 프로토타이핑은 특정 아이디어가 실제로 재미있는지, 구현 가능한지를 빠르게 확인하는 과정이에요. 이때 중요한 건 속도예요. 깔끔한 코드를 작성하는 데 시간을 쓰는 대신, "일단 동작하는" 코드를 만드는 게 더 유용할 때가 많아요.

예를 들어, 제 BleedSkill 시스템에서 Timer를 활용한 쿨다운 로직을 테스트하고 싶었어요. 처음엔 이런 생각을 했죠:

  • SkillManager에서 스킬 상태를 관리해야지.
  • UI 버튼과 스킬 노드 간의 결합도를 낮추기 위해 시그널 버스를 만들자.
  • 스킬 상태를 저장/로드할 수 있도록 설계해야 해.

하지만 이런 설계를 다 구현하려면 시간이 너무 많이 걸릴 것 같았어요. 그래서 방향을 바꿨어요. "일단 동작하는 코드를 빠르게 만들자!"

# bleed_skill.gd (프로토타입 버전)
extends Node

var is_learned = false

func _ready():
    $Timer.wait_time = 5.0
    $Timer.connect("timeout", self, "_on_timer_timeout")

func _on_learn_button_pressed():  # UI 버튼에서 직접 호출
    is_learned = true
    $Timer.start()

func _on_timer_timeout():
    if is_learned:
        print("Bleed skill activated!")
        var player = get_node("/root/Player")
        player.take_damage(10)

이 코드는 "나쁜 코드" 그 자체였어요:

  • UI 버튼이 스킬 노드를 직접 호출해서 결합도가 높음.
  • SkillManager 같은 중앙 관리 시스템이 없음.
  • 상태 저장/로드 로직도 없음.
  • 경로(/root/Player)가 하드코딩되어 있음.

하지만 이 코드를 작성하는 데 30분밖에 안 걸렸어요. 그리고 바로 테스트해볼 수 있었죠. 결과적으로, 플레이어가 "이 스킬은 쿨다운이 너무 길다"는 피드백을 주었고, 저는 쿨다운 시간을 3초로 줄여서 다시 테스트해봤어요. 이 과정에서 깨달은 점은 스킬의 재미를 판단하는 데는 깔끔한 코드가 필요하지 않다는 것이었어요.


나쁜 코드의 함정: 버릴 수 있어야 한다

나쁜 코드의 가치를 최대한 누리려면 한 가지 중요한 전제가 있어요. 그 코드를 버릴 수 있어야 한다는 거예요. 프로토타입 코드는 말 그대로 "임시" 코드예요. 동작 확인이 끝나면 버리고, 제대로 된 설계로 다시 작성해야 합니다. 하지만 현실에서는 이런 일이 자주 발생하죠:

상사/팀원: "이 프로토타입, 꽤 잘 동작하네! 그냥 좀 정리해서 실제 코드로 쓰자!"
개발자: "…이건 임시 코드라 유지보수가 불가능한데요…"
상사/팀원: "괜찮아, 시간 없으니 그냥 써!"

이런 상황이 반복되면 나쁜 코드가 프로젝트에 쌓여서 나중에 큰 문제를 일으킵니다. 제가 처음 작성한 BleedSkill 프로토타입 코드를 그대로 사용했다면, 나중에 다른 스킬(예: FireSkill, IceSkill)을 추가할 때마다 경로 문제, 결합도 문제로 고생했을 거예요.

이를 방지하기 위해 몇 가지 전략을 사용할 수 있어요:

  • 다른 언어로 작성하기: 프로토타입을 Godot의 GDScript 대신 Python이나 JavaScript로 작성하면, 실제 게임에 바로 사용할 수 없어서 강제로 다시 작성해야 함.
  • 명확한 경고: 팀원들에게 "이 코드는 프로토타입용이고, 나중에 반드시 리팩토링해야 한다"고 명확히 전달.
  • 별도 브랜치 사용: Git에서 프로토타입 코드를 별도 브랜치에 저장하고, 메인 브랜치에는 절대 병합하지 않음.

나쁜 코드에서 좋은 코드로: 리팩토링

프로토타입으로 아이디어가 검증되었다면, 이제 나쁜 코드를 좋은 코드로 바꿀 차례예요. 저는 BleedSkill 시스템을 리팩토링하면서 다음과 같은 설계를 적용했어요:

# SkillManager.gd (싱글톤)
extends Node

var skill_status = {"bleed_skill": false}
var skill_nodes = {}

func _ready():
    call_deferred("initialize_skill_nodes")

func initialize_skill_nodes():
    skill_nodes["bleed_skill"] = get_node("/root/FuryBuildTree/BleedSkill")

func learn_skill(skill_name: String):
    skill_status[skill_name] = true
    var node = skill_nodes[skill_name]
    var timer = node.get_node("Timer")
    if not timer.is_connected("timeout", node, "_on_timer_timeout"):
        timer.connect("timeout", node, "_on_timer_timeout")
    timer.start()

# bleed_skill.gd (리팩토링 버전)
extends Node

func _ready():
    $Timer.stop()

func _on_timer_timeout():
    print("Bleed skill activated!")
    var player = get_node("/root/Player")
    player.take_damage(10)
    $Timer.start()

# UI 스크립트
func _on_learn_bleed_skill_button_pressed():
    SkillManager.learn_skill("bleed_skill")

이렇게 리팩토링한 결과:

  • 결합도 감소: UI와 스킬 노드가 직접 통신하지 않고 SkillManager를 통해 중개.
  • 확장성 향상: 새로운 스킬을 추가할 때 skill_statusskill_nodes에 항목만 추가하면 됨.
  • 유지보수성: 상태 관리와 시그널 연결이 중앙에서 관리되어 코드 이해가 쉬워짐.

나쁜 코드와 좋은 코드의 균형

게임 개발에서 중요한 건 균형이에요. 나쁜 코드는 빠른 실험을 가능하게 하지만, 장기적으로 유지보수 문제를 일으킵니다. 반대로, 좋은 코드는 유지보수와 확장성을 높여주지만, 초기 개발 속도를 느리게 만들죠.

저의 경험상, 초기에는 나쁜 코드를 활용해 빠르게 실험하고, 디자인이 확정되면 리팩토링하는 방식이 가장 효과적이더라고요. Godot에서 스킬 시스템을 구현하면서도 이 접근법을 통해 많은 시간을 절약할 수 있었어요. 예를 들어, BleedSkill의 쿨다운 시간을 조정하거나 새로운 효과를 추가하는 과정에서 프로토타입 코드가 큰 도움이 됐죠.


마무리: 나쁜 코드도 괜찮아

"나쁜 코드"라는 말은 부정적으로 들릴 수 있지만, 게임 개발에서는 오히려 창의성과 실험의 도구가 될 수 있어요. 완벽한 코드를 작성하려고 너무 많은 시간을 들이기보다는, 일단 동작하는 코드를 만들어서 아이디어를 테스트해보세요. 그리고 그 아이디어가 유효하다면, 그때 깔끔한 설계로 리팩토링하면 됩니다.

게임 개발의 궁극적인 목표는 재미있는 게임을 만드는 거예요. 나쁜 코드를 통해 더 많은 아이디어를 빠르게 테스트할 수 있다면, 그건 충분히 가치 있는 선택이에요. 여러분도 다음 프로젝트에서 "나쁜 코드"를 두려워하지 말고, 오히려 그 가치를 활용해보세요!

혹시 여러분만의 프로토타이핑 팁이나 나쁜 코드 경험담이 있다면 댓글로 공유해주세요. 함께 이야기 나누면 더 좋은 아이디어가 나올 것 같아요! 😊


Write bad code , frist ! but You must need refactoring time!

https://gameprogrammingpatterns.com/architecture-performance-and-games.html