grep

Engineering

[Jetpack Compose — Part 2] Compose, 실전에서 빛을 발하다: 코드는 1/4로, 생산성은 4배로!

야놀자

2025년 9월 26일

원문에서 보기 ↗

이 이미지는 생성형 AI를 활용해 제작하였습니다.

안녕하세요, 앱개발팀의 안드로이드 개발자 지지입니다.

최근 저희 팀은 사내 서비스의 UI를 기존의 View 시스템에서 Jetpack Compose로 전환하는 프로젝트를 진행했습니다.

앞서 공유한 #Part 1의 내용처럼 Compose를 점진적으로 도입하기 위한 준비를 마치고, 홈 개편, 리브랜딩, 디자인 시스템 구축이라는 큰 과제들과 함께 본격적으로 적용을 하게되었습니다.

이 글에서는 저희가 Compose를 적용하는데에 어떤 도전 과제에 마주하게 되었고 어떻게 개발 생산성을 극적으로 향상시킬 수 있었는지 솔직하게 공유하고자 합니다.

1. 홈 개편과 리브랜딩: 디자인 시스템의 위력

프로젝트의 시작은 ‘홈 개편’이었습니다. 홈 화면의 모든 위젯을 새로운 디자인으로 교체해야 하는 대규모 작업이었죠. 저희는 이 기회에 단순히 화면만 바꾸는 것이 아니라, 앞으로의 확장성과 유지보수성을 위해 신규 디자인 시스템을 구축하기로 결정했습니다. 그리고 그 디자인 시스템의 구현 도구로 Jetpack Compose를 적극 활용하게 되었습니다.

신규 디자인 시스템 구축 — 약 30가지 이상의 컴포넌트와 파운데이션이 새로 정의되었습니다.

신규 디자인 시스템을 Compose로 구현하고, 미리 약속된 컴포넌트들을 활용해 새로운 홈 위젯들을 개발했습니다. 선언형 UI 패러다임 덕분에 코드의 양이 줄고 가독성이 높아져 개발 속도가 눈에 띄게 향상되었습니다. 테마 코드 구현은 MaterialTheme을 참고했습니다.

커스텀 테마 속성 정의하기

디자인팀과 협의하여 우리 앱만의 컬러 시스템을 구축하는 작업을 먼저 진행했습니다. 요소에 따라 적용할 컬러를 구분하여 시멘틱 컬러 시스템을 만들고 확장 가능한 형태의 이름 규칙을 만든 다음, 정해진 규칙에 맞게 코드로 커스텀 테마를 구현하였습니다. 커스텀 색상은 코드상 어떻게 체계적으로 관리하고 쉽게 사용할 수 있을까요?

CompositionLocal을 이용해 커스텀 속성을 정의하고, CustomTheme에 프로퍼티를 추가하여 마치 기본 속성처럼 편리하게 사용할 수 있습니다.

먼저 커스텀 색상 클래스와 CompositionLocal을 정의합니다. (아래 코드들은 예시코드이며, 실제 구현 코드와 차이가 있습니다. 참고로만 봐주세요)

// 디자인팀과 협의하여 정한 시멘틱 컬러
fun customColors() =
    CustomColors(
        fillPrimary: Color = Color.blue600,
     textMain: Color = Color.gray900,
     textSub: Color = Color.gray600,
     lineWarning: Color = Color.orange600,
     lineError: Color = Color.red600,
     textSuccess: Color = Color.green600,
    )
internal val LocalCustomColors = staticCompositionLocalOf { customColors() }

앱의 최상단 테마 Composable에서 CompositionLocalProvider로 값을 제공합니다.

// 커스텀한 Theme
@Composable
fun CustomAppTheme(
    colors: CustomColors = CustomDesignSystemTheme.colors,
    typography: CustomTypography = CustomDesignSystemTheme.typography,
    dimensions: CustomDimension = CustomDesignSystemTheme.dimensions,
    shapes: CustomShapes = CustomDesignSystemTheme.shapes,
    content: @Composable () -> Unit,
) {
    CompositionLocalProvider(
        LocalCustomColors provides colors.copy(),
        LocalCustomDimensions provides dimensions,
        LocalCustomTypography provides typography,
        LocalCustomShapes provides shapes
    ) {
        content()
    }
}

이제 어떤 Composable에서든 CustomDesignSystemTheme.colors.textNeutralMain와 같이 직관적으로 커스텀 색상을 사용할 수 있습니다. 리브랜딩 시에는 CustomColors 클래스의 색상 값만 변경하면 앱 전체에 반영됩니다.

디자인 시스템을 Compose로 구현하며 느낀 3가지 강점

(1) 재사용이 가능한 컴포넌트 제작

가장 핵심적인 전략은 UI 조각들을 단순히 재사용하는 것을 넘어, 다양한 상황에 대응할 수 있는 스마트 컴포넌트로 만드는 것입니다. 카드나 다이얼로그와 같은 복잡한 컴포넌트는 내부 컨텐츠를 커스터마이징할 수 있도록 content: @Composable () -> Unit 형태의 Slot API를 적극적으로 활용하면 정해진 레이아웃 틀 안에서 유연하게 내용을 채워 넣을 수 있어 재사용성이 극대화됩니다.

예를 들어, 아래와 같은 TopAppBar 컴포넌트가 있고, 버튼의 자리가 정해져있으면 어떤 버튼이 오든지 상황에 따라 유연하게 활용할 수 있도록 코드를 구성할 수 있습니다.

@Composable
fun BaseTopAppBar(
    title: @Composable () -> Unit, // 'title' 영역에 들어갈 Composable을 통째로 받음
    modifier: Modifier = Modifier,
    navigationIcon: @Composable () -> Unit = {}, // 좌측 네비게이션 아이콘 영역
    actions: @Composable RowScope.() -> Unit = {} // 우측 액션 아이템 영역
) {
    TopAppBar(
        title = {
            // 전달받은 title Composable을 이곳에 배치
            title() 
        },
        modifier = modifier,
        navigationIcon = {
            navigationIcon()
        },
        actions = {
            // RowScope 컨텍스트 안에서 actions Composable을 배치
            actions()
        }
    )
}

(2) ‘상속(Inheritance)’이 아닌 ‘조합(Composition)’을 활용

물론 기존 View 시스템으로도 재사용 가능한 커스텀 뷰는 충분히 제작할 수 있습니다. 그렇지만 컴포넌트 하나를 제작하는데에 드는 생산 비용에서 차이를 경험할 수 있습니다. 기존 View는 대부분 상속을 기반으로 합니다. View나 ConstraintLayout 등을 상속하는 클래스 파일을 생성하고 레이아웃 xml으로 UI를 제작하는 과정에 추가로 상태 변경을 원한다면 BindingAdapter와 같은 코드를 작성하게 될 것입니다. 이 모든 과정은 여러 파일을 넘나들어야 하며 보일러플레이트 코드를 동반합니다.

Compose는 조합을 기반으로 한다는 점에서 그 강력함이 발휘됩니다. 여러 개의 작은 컴포넌트(함수)를 조합하여 더 큰 컴포넌트를 만드는 것이 가능하죠. Image와 Text를 Column 안에 넣어 ProfileCard를 만드는 식. 이는 레고 블록을 조립하는 것과 같아서 훨씬 더 유연하고 직관적입니다.

(3) 즉각적인 Preview

기존 View로 만든 컴포넌트가 다양한 상황에서 어떻게 보이는지 확인하려면 앱을 빌드하고 실행하거나, 제한적인 기능의 레이아웃 에디터에 의존해야 했지만, Compose에선 @Preview 어노테이션 하나로 컴포넌트의 모든 상태(다크 모드, 다양한 텍스트 길이, 활성/비활성, 클릭이벤트 등)를 즉시, 동시에 확인할 수 있습니다. 이 빠른 피드백 루프는 개발 속도를 n번의 빌드시간 만큼이나 빠르게 만들어 주었습니다.

ComposeUI로 완성된 홈/서브홈 개편의 결과물 -짜잔-

(카테고리 하위페이지를 서브홈이라 칭합니다)

그리고 6개월 후, Compose의 진정한 위력을 경험하는 사건이 발생했습니다. 바로 서비스 전체 리브랜딩 과제였습니다. 서비스의 메인 컬러를 핑크에서 블루로 바꿔야 했죠.

// 디자인팀과 협의하여 정한 시멘틱 컬러
fun customColors() =
    CustomColors(
  //Color.pink400에서 리브랜딩 색상값으로 변경
        brand: Color = Color.blue600, 
)

구)야놀자, 현)NOL

이 경험을 통해 저희는 잘 구축된 Compose 기반 디자인 시스템이 얼마나 유지보수를 용이하게 하고, 변화에 유연하게 대처할 수 있게 하는지 온몸으로 체감할 수 있었습니다.

2. 검색홈 개편: 생동감을 더하는 Compose 애니메이션

다음 과제는 ‘검색홈 개편’이었습니다. 기존 검색홈은 여러 탭(국내숙소 / 레저티켓 / 공연전시 / 해외숙소) 중 어떤 탭이 선택되었는지 사용자가 명확하게 인지하기 어렵다는 문제점이 있었습니다. 저희는 애니메이션을 추가하여 탭의 선택 상태를 시각적으로 강조하는 방향으로 개편을 진행하였습니다.

이 또한 Compose로 전환하면서 놀라운 경험을 했습니다. 기존 View 시스템에서 부드러운 애니메이션을 구현하려면 ValueAnimator, ObjectAnimator 등을 사용하며 다소 복잡하고 번거로운 코드를 작성해야 했습니다.

하지만 Compose에서는 animate*AsState, Animatable 과 같은 직관적인 API를 사용하여 훨씬 적은 양의 코드로 더 자연스럽고 세련된 애니메이션을 구현할 수 있었습니다. 코드 자체가 애니메이션의 시작과 끝 상태를 선언적으로 명시하기 때문에, 로직을 이해하고 수정하기도 훨씬 쉬웠습니다.

[기존 View vs Compose 애니메이션 코드 비교 예시]

View vs Compose

이전에 애니메이션 구현은 매우 귀찮고 하기싫은 작업 중 하나였으나, 이처럼 Compose는 기능적으로 우수할 뿐만 아니라, 개발자가 더 창의적이고 동적인 UI를 만드는 데 집중할 수 있도록 도와주었습니다.

ScaleAnimation과 포인터가 탭을 따라다니는 인터랙션

기존 뷰로는…저는 구현 못할 것 같습니다ㅠ

3. 폴더블 시대 대응: 스크린샷 테스트로 UI 품질 자동화하기

폴더블폰과 같은 새로운 폼팩터의 등장은 저희에게 또 다른 과제를 안겨주었습니다. 바로 다양한 화면 크기와 폰트 크기에 대응하는 반응형 UI의 필요성이었죠. 구 디자인 시스템에서는 고정된 폰트 크기를 사용했었고 큰 화면 대응은 우선순위가 아니었지만, 이제는 필수 요건이 되었습니다.

저희는 이 문제를 해결하고 UI 품질을 사전에 보장하기 위해, Compose UI를 적용하며 스크린샷 테스트(Screenshot Test)를 도입 했습니다. Roborazzi와 Showkase 등의 라이브러리들도 함께 검토를 했는데, 최종적으로 우리의 작업 스타일과 가장 적합한 Compose Preview ScreenshotTest를 사용하기로 결정했습니다.

목표는 명확했습니다. 개발자의 수동 테스트에 의존하지 않고, 다양한 환경에서 UI가 깨지거나 의도치 않게 변경되는 것을 자동으로 감지하는 것이었습니다.

스크린샷 테스트 활용하기

⚫ 자동화 프로세스

CI(지속적 통합) 파이프라인에 스크린샷 테스트를 연동하여, 모든 PR(Pull Request) 생성 시 자동으로 테스트가 실행되도록 구성했습니다. 스크린샷 테스트는 1px 단위로 변경을 감지할 수 있습니다. 이를 통해 코드 변경 사항이 UI에 미치는 영향을 머지(Merge) 전에 미리 확인할 수 있습니다.

⚫ 테스트 케이스

사용자가 마주할 수 있는 극단적인 환경까지 검증하기 위해, 아래 4 조합의 테스트 케이스를 필수로 지정했습니다.

커스텀 어노테이션으로 Preview 활용 극대화하기

컴포넌트 하나를 검증하기 위해 @Preview(name = “Foldable”), @Preview(name = “FontScale”, fontScale = 1.0f) 등 여러 개의 어노테이션을 매번 복사/붙여넣기하는 것은 번거롭습니다. 그래서 정해진 Preview 설정들을 묶어 커스텀 어노테이션을 만들어 사용하였습니다.

여러 개의 @Preview를 포함하는 새로운 어노테이션 클래스를 정의합니다.

// 여러 Preview 설정을 하나로 묶는 어노테이션 정의
@Preview(name = "Default", showBackground = true)
@Preview(name = "Foldable", device = "spec:width=600dp, dpi=480", showBackground = true)
annotation class DevicePreview

// 폰트 크기까지 테스트하고 싶다면?
@Preview(name = "SmallFont", fontScale = 0.5f, showBackground = true)
@Preview(name = "LargeFont", fontScale = 2.0f, showBackground = true)
annotation class FontScalePreview

// 네가지 케이스를 모두 테스트
@DevicePreview
@FontScalePreview
annotation class ScreenShotTestPreviews

이제 매번 Composable 함수에 긴 설정 대신 커스텀 어노테이션 하나만 붙이면 됩니다.

// Font Scale 프리뷰까지 네가지 케이스 모두 생성됨
@ScreenShotTestPreviews 
@Composable
fun MyTextPreview() {
    MyAppTheme {
        Text("안녕하세요")
    }
}

코드 가독성이 높아지고, 팀 전체가 동일한 기준으로 Preview를 작성하게 되어 일관성이 향상됩니다. n개의 빌드 시간을 아끼는 것을 넘어, Preview를 작성하는 시간 자체를 단축시킬 수 있습니다.

이 자동화된 테스트 덕분에, 저희는 더 이상 다양한 기기에서 UI가 어떻게 보일지 걱정하며 시간을 쏟지 않아도 됩니다. 예상치 못한 UI 깨짐 현상을 사전에 방지하고 모든 사용자에게 일관된 시각적 경험을 자신 있게 제공할 수 있게 되었습니다.

좌: width 600 스크린샷 / 우: width 360 스크린샷

UI가 깨지지 않는지 대략적으로 확인할 수 있습니다

4. 빛과 그림자: 직접 경험한 Compose의 장단점

이번 프로젝트를 통해 제가 직접 느낀 Compose의 장점과 단점을 솔직하게 정리해 보았습니다.

빛 ✨: 코드는 1/4로, 생산성은 4배로 -> 압도적인 생산성 향상

제가 직접 느낀 Compose의 가장 큰 장점은 바로 ‘압도적인 생산성 향상’이었습니다. 이는 단순히 감상적인 표현이 아니라, 정신적으로 그리고 물리적으로 체감할 수 있는 변화였습니다.

수정할 코드 파일이 1/4로 줄었습니다.

가장 대표적인 예가 리스트(List) UI 구현입니다.

⚫ 기존 View 방식 (RecyclerView):

최소 4개의 파일을 넘나들며 보일러플레이트 코드를 작성해야 했습니다. 컨텍스트를 전환하는 과정에서 정신적인 피로도도 상당했죠.

⚫ Compose 방식 (LazyColumn):

<예시 코드>

@Composable
fun MyListScreen(items: List<MyItem>) {
    LazyColumn {
        items(items) { item ->
            MyListItem(item)
        }
    }
}

@Composable
fun MyListItem(item: MyItem) {
    // 아이템 UI 구현
}

하나의 파일, 몇 줄의 선언적인 코드로 리스트가 완성됩니다. 작성해야 할 코드의 양과 파일 수가 물리적으로 1/4 수준 으로 줄어드니, 개발자는 비즈니스 로직에 더 집중할 수 있게 되었습니다. 이런 경험이 쌓여 개발 속도와 만족도를 모두 높이는 선순환을 만들었습니다. 오롯이 kotlin 언어로만 구성된 UI 코드를 작성할 수 있다는 것만으로도 생산성과 유지보수에 큰 도움이 된다고 느꼈습니다.

그림자 🌦️: 물론, 아쉬운 점도 있었습니다

마치며: 개인과 회사 모두를 성장시킨 Win-Win 전략

Jetpack Compose로의 전환은 장점만 가득한 은총알(Silver Bullet)은 아니었지만, 몇몇 단점을 감수하고서라도 얻는 이점이 훨씬 큰 결정이었습니다. 개발 프로세스는 더 유연하고 빨라졌으며, 자동화된 테스트를 통해 품질에 대한 자신감도 얻었습니다.

사전에 꼼꼼하게 검토한 뒤에 실제 비즈니스 과제와 함께 Compose 도입과 스크린샷 테스트 구축을 자연스럽게 녹여냈다는 점에서 팀원 개개인의 성장과 회사의 성과라는 두 마리 토끼를 모두 잡는 결과를 낳았다는 점도 좋은 경험이 되었습니다. 개발자들은 새로운 기술을 익히고 자동화된 품질 관리 체계를 구축하며 성장하는 동시에 직접적인 비즈니스 임팩트를 만들어냈고, 회사는 추가 리소스 투입 없이 서비스의 질과 개발 효율성이라는 실질적인 성과를 얻을 수 있었으니까요.

아직 Compose 도입을 망설이는 분이 있다면, 이 글이 여러분의 결정에 작은 도움이 되기를 바랍니다. 저희의 경험처럼, 여러분도 분명 즐거운 변화를 맞이하게 될 것입니다!

과연 Compose가 기존 View보다 성능적으로도 좋을까?에 대한 의구심이 있으시다면 다음편 #Part 3를 기대해주세요!

누구나 마음 편히 놀 수 있는 세상을 함께 만들어갈 동료를 찾고 있어요 :)

지금 채용 중인 포지션 보러가기 >