기술자로서 가장 어려웠던 순간을 회고하며

치킨치·2025년 3월 28일
0

얼마 전, “최근 기술적으로 어려웠던 점이 있었나요?“라는 질문을 받았다.
그런 질문을 받았을 때, 솔직히 바로 대답이 떠오르지 않아 애매한 답을 해버렸다.
그 순간이 꽤 부끄럽게 느껴졌고, 그때부터 ’기술적으로 정말 어려웠던 작업은 무엇이었을까?’라는 고민을 하게 됐다.
이번 기회에 그 질문에 진지하게 답해보고자 한다.
특히 인간관계의 문제를 제외한 기술자로서의 허들만을 다뤄보겠다.

Facebook IGListKit으로 피드 기능 개발

7년 차에 접어들 무렵, 전문 iOS 개발자로서는 경력 1년이 채 안 되었던 때였다.
그 시점에서 하나의 도전이 찾아왔다.
IGListKit을 사용해 피드 기능을 구현하라는 미션.

당시 IGListKit은 Facebook에서 오픈소스로 공개한 라이브러리로, 인스타그램의 피드 구현에 사용된 기술이었다.
팀의 한 시니어 개발자가 해당 라이브러리의 구조적 장점을 보고 선택한 것이었다.

하지만 이 작업은 단순한 라이브러리 적용을 넘어서, 팀 내 처음 도입되는 ‘상태 기반 UI 갱신 방식(Diffing)’이라는 큰 변화의 시작이기도 했다.
심지어 나는 신입 두 명을 이끌며 개발을 진행해야 했고, 그 책임감과 중압감 또한 상당했다.

💡 기술적 난관 1: Diffing 방식 도입

Diffing은 React나 Virtual DOM처럼, 변화된 데이터만 갱신해 효율적으로 UI를 업데이트하는 방식이다.
이를 위해선 모델 객체들이 적절하게 Hashable을 구현해야 하고,
어떤 속성 조합이 UI 갱신에 영향을 미치는지를 명확히 정의해야 했다.

또 하나의 문제는 UICollectionViewCompositionalLayout의 채택 여부였다.
성능 버그나 iOS 하위 호환성 문제로 인해 꺼려지는 상황이었고,
결국 안정성과 오픈소스 커스터마이징 가능성을 고려해 IGListKit을 선택하게 되었다.

그 과정에서 특히 까다로웠던 부분은 다음과 같다:

  • 어떤 정보를 기준으로 Diff를 수행할지 판단해야 했던 점
  • 피드 구성의 핵심 단위인 Section의 Cell 높이를 동적으로 계산하는 로직

모든 것이 낯설고 익숙하지 않은 환경 속에서 시행착오가 이어졌고, 그만큼 많은 학습이 필요했다.

💡 기술적 난관 2: 리액티브 대신 Delegate 프로그래밍

상위 뷰와의 커뮤니케이션 방식으로 RxRelay와 같은 리액티브 인터페이스를 사용할 수도 있었지만,
그 당시 IGListKit만으로도 팀에게는 큰 변화였기에,
추가적인 러닝 커브를 줄이고자 Delegate 프로토콜 방식을 선택했다.

이 방식은 SwiftUI의 Binding처럼 직관적이진 않지만 UIKit 전통적인 방식으로는 가장 안정적인 선택이었다.
프로토콜 파일이 많아지고 이름이 길어져 코드 가독성 이슈도 있었지만,
많은 subview 간 통신을 고려했을 때 불가피한 선택이었다고 생각한다.

결과적으로 여러 시행착오를 겪으며, 수많은 패치노트가 쌓여갔다.
그럼에도 앱의 핵심 기능으로 안정적으로 자리 잡게 되었고, 그 과정에서 얻은 경험과 노하우는 지금까지도 내 개발 철학에 영향을 주고 있다.

Unity 엔진 기반 UI 시스템 개발

이전 회사에서는 Unity 기반의 모바일 게임 클라이언트를 개발했다.
당시엔 지금처럼 정형화된 UI 시스템이 존재하지 않아,
UIKit으로 따지면 모두가 직접 버튼, 뷰, 네비게이션을 만들어 사용하고 있는 상황이었다.
우리는 NGUI라는 써드파티 툴을 사용했지만,
디자이너들은 미적 기준 때문에 NGUI의 기본 UI를 대부분 거부했다.

따라서 나는 iOS의 UIKit에서의 경험을 바탕으로,
자체 UI 시스템을 새롭게 구축해야 했다.
이 과정에서 스택 기반의 화면 전환 시스템, 모달 애니메이션, ViewController 관리 기능 등을 직접 설계하고 구현했다.

그러나 문제는 ‘스택 구조’라는 개념을 이해하지 못한 상위 리더에게 있었다.
내가 만든 시스템을 설명하고 설득하는 데에도 많은 시간이 필요했고,
결국 개인 시간을 쪼개 거의 모든 기능을 구현한 후에서야 간신히 승인받을 수 있었다.


차세대 엔진으로의 Entity Component System(ECS) 이식기

이야기는 내가 한 슈팅 게임 프로젝트의 주니어 개발자로 일하던 시절로 거슬러 올라간다.
해당 게임은 이미 상업적으로 수익을 낸 상태였고,
오랜 라이브 서비스로 인해 점차 수익이 감소하는 상황이었다.
이후, 같은 IP를 기반으로 차세대 엔진으로 리빌딩하는 프로젝트가 시작 후 나는 그 핵심적인 기술 이식 작업을 맡게 되었다.

이 프로젝트는 단순한 리메이크를 넘어, 시스템 레벨에서의 구조 개선이 필요한 작업이었다.

💡 기술적 난관 1: ECS 이식 - 두 개의 시스템, 하나의 아키텍처

이전 프로젝트에서는 cocos2d-x 위에 직접 구현한 ECS(Entity Component System)를 기반으로 배틀 시스템이 돌아가고 있었다.
특히, XML 데이터를 기반으로 시나리오를 제어하는 구조는 당시 나에게도 굉장히 인상 깊은 설계였다.

차세대 엔진으로 넘어가면서 문제는 예상보다 복잡해졌다.
새 엔진에는 자체 ECS가 이미 내장되어 있었던 것이다.
즉, 기존 시스템을 그대로 옮기면 두 개의 ECS가 공존하는 구조가 되는 셈이었다.
이는 성능과 유지보수 모두에서 큰 부담이 되는 구조였다.
기술적인 도전을 즐기는 나에게 팀장은 이 ECS 이식 작업을 맡겼는데,
출시 리스크를 줄이기 위해 나는 두 가지 버전을 동시에 개발하게 된다:

  1. 2개의 ECS가 공존하는 버전
  • 기존 시스템을 그대로 옮기되, 병렬적으로 작동하도록 구성
  • 개발 리스크를 고려한 백업용 버전
  1. 차세대 엔진의 ECS에 기존 시스템을 통합한 버전
  • 아키텍처를 새로 정리하며, 기존 배틀 시스템의 핵심만 이식
  • 개발 난이도는 높지만 장기적으로 훨씬 이상적인 구조

결국 두 번째 버전을 완성시키며 이식 작업을 성공적으로 마무리할 수 있었고,
덕분에 중복된 시스템으로 인한 리스크도 줄일 수 있었다.

💡 기술적 난관 2: Unity 엔진과 C#의 퍼포먼스 한계

차세대 엔진은 Unity였고, 이로 인해 또 하나의 난관에 부딪혔다.
당시의 Unity는 (특히 1~2세대 ECS 등장 이전) 모든 게임 로직이 싱글 스레드에서 동작했다.
이 구조는 복잡한 로직이 들어가는 게임에서 병목 현상을 쉽게 일으켰고, 이는 성능 저하로 이어졌다.

더불어 Unity는 C#을 사용하는데, 기존 C++ 기반 시스템과 비교해 런타임 성능에서의 한계가 명확히 드러났다.
특히 다음과 같은 문제들이 있었다:

  • 배열 변수의 boxing/unboxing 이슈
  • foreach 루프의 퍼포먼스 저하
  • object array 사용으로 인한 GC 부담

이를 해결하기 위해 다음과 같은 리팩터링을 수행했다:

  • 모든 foreach 구문을 for 루프로 변경
  • object array를 generic array로 전환
  • 화면에 표시되는 오브젝트 수를 제한하여 렌더링 부하를 감소

이러한 노력 덕분에, 프로젝트의 프레임 드랍 현상은 일정 수준까지 개선되었고,
실제 출시 버전에서도 안정적인 플레이가 가능해졌다.


마무리하며

돌이켜보면, 내가 겪었던 가장 기술적으로 어려운 작업은 단지 복잡한 구현이나 새로운 라이브러리를 쓰는 것이 아니었다.
‘낯선 개념을 팀에 도입하고, 기술적인 선택에 대한 확신을 가지고 끝까지 끌고 가는 과정’ 그 자체가 가장 어려웠다.

그렇기에 지금도 어떤 기술이 '최선의 선택이었을까?'라는 질문이 머릿속을 맴돈다.
하지만 역사는 ‘if’를 허락하지 않기에,
그때의 시행착오를 통해 지금의 나의 성장 그리고 안정된 기능으로 자리잡은 점에 스스로를 다독인다.

profile
풀스텍이었던 iOS개발자

0개의 댓글