순수 함수 모르고 Compose 쓰면 그냥 API 따라치는 겁니다

지훈·2026년 1월 21일

그 바보가 바로 접니다

많은 유명한 개발자들이 하는 말처럼, 저도 항상 이런 말을 했습니다.:

"기술은 그냥 배우는 게 아니라, 그 기술의 철학과 패러다임을 알아야 한다고 생각해요. 그래야 코더가 아니라 개발자고, AI가 대체 못하는 사람이 되지 않을까요?"

어느날 사내에서 제가 진행한 객체지향 프로그래밍 관련 세미나가 끝나고 나서 동료가 질문을 던졌습니다.

"객체지향 프로그래밍 패러다임도 언젠가 바뀔 수 있겠죠?"

"그럼요~ 함수형 프로그래밍 패러다임도 굉장히 많이 쓰이잖아요, Compose 도 함수형 프로그래밍 패러다임에서 태어난 기술인 걸요~"

"오 그러면 나중에 Compose 와 함수형 프로그래밍 패러다임 세미나도 해주시나요?"

"어...... 😅"

순수 함수? 참조 투명성? 알고는 있었습니다. 하지만 Compose와 어떻게 연결되는지 '제대로' 설명하지 못했습니다. 평소 제가 하던 말에 제가 찔렸습니다.


이 글의 독자

이 글은 다음과 같은 분들을 위해 작성되었습니다:

  1. Compose API보다 본질을 이해하고 싶은 개발자
    • remember, LaunchedEffect 같은 개별 API 사용법은 알지만, "왜 이렇게 설계되었는가?"에 대한 깊은 이해를 원하는 분
  2. 함수형 프로그래밍에 생소하지만 관심 있는 개발자
    • 이론을 실무 코드로 연결하고 싶은 분
  3. Android View System에서 Compose로 전환을 고민하는 개발자
    • 뷰 시스템에 익숙한 분
    • Compose/선언형 UI 도입이 실제로 어떤 이점을 가져오는지 궁금한 분

순수 함수란

Jetpack Compose는 선언형 UI 프레임워크입니다.
선언형 UI의 핵심은 "상태가 변경되면 UI를 다시 그린다"는 단순한 원칙입니다.
그런데 이 단순한 원칙이 안전하게 작동하려면, UI를 그리는 함수가 순수 함수여야 합니다.

왜일까요?

Compose 는 언제든지 Recomposition을 실행할 수 있기 때문입니다.
같은 상태로 UI를 10번 그리든, 100번 그리든 결과가 달라지면 안 됩니다. 순수 함수만이 이 안전성을 보장할 수 있습니다.

참조 투명성

"같은 입력에 대해 항상 같은 출력을 반환한다"

수학 함수를 떠올려 봅시다. f(x) = 2x라는 함수가 있을 때, f(5)는 항상 10입니다. 1초 후에 호출하든, 다른 곳에서 호출하든 결과는 같습니다.

Composable 함수도 이렇게 동작해야 합니다.

// ✅ 순수 함수: 같은 name이면 항상 같은 결과
@Composable
fun PureGreeting(name: String) {
    Text(text = "Hello, $name!")
}

// ❌ 비순수 함수: 호출할 때마다 다른 결과
@Composable
fun ImpureGreeting(name: String) {
    val timestamp = System.currentTimeMillis()
    Text(text = "Hello, $name! (at $timestamp)")
}

ImpureGreeting은 같은 name을 전달해도 시간이 지나면 다른 텍스트를 표시합니다. 이는 참조 투명성을 위배합니다.

부수효과 없음

"외부 상태를 변경하지 않는다"

순수 함수는 자신의 스코프 밖의 무언가를 변경하지 않습니다. 전역 변수를 수정하거나, 파일에 쓰거나, 네트워크 요청을 보내는 것은 모두 부수효과입니다.

var globalCounter = 0  // 전역 변수

// ❌ 부수효과: 전역 변수 변경
@Composable
fun BadCounter() {
    Button(onClick = { globalCounter++ }) {
        Text("Count: $globalCounter")
    }
}

// ⚠️ Compose-safe: Compose가 관리하는 상태 사용 (엄밀히는 순수 아님)
@Composable
fun GoodCounter() {
    var count by remember { mutableStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}

BadCounter는 클릭할 때마다 전역 변수를 변경합니다. 이는 부수효과입니다.
반면, GoodCounterremember로 Composable 내부에서 상태를 관리합니다.

"잠깐, GoodCountercount++로 상태를 변경하는데, 이게 순수한가요?"

좋은 질문입니다. 엄밀히 말하면 GoodCounter는 순수 함수가 아닙니다.

이 글에서는 이런 패턴을 "Compose-safe"라고 부르겠습니다:

Compose-safe

  • 엄밀히 순수 함수는 아니지만
  • Compose가 상태를 추적하고 관리하기 때문에
  • Recomposition에 안전한 패턴

주의: "Compose-safe"는 공식 용어가 아닙니다. 순수 함수와 구분하기 위해 이 글에서 사용하는 표현입니다.

remember { mutableStateOf(0) }로 생성된 상태를 onClick에서 변경하기 때문입니다.
다만, Compose가 이 상태를 추적하고 관리하기 때문에 Recomposition에는 안전합니다.

Compose-safe ≠ 순수 함수

  • GoodCounterCompose가 안전하게 관리하는 패턴
  • 하지만 수학적으로 순수한 것은 아님
  • 그렇다면 진정한 순수 함수는 어떻게 만들까요?

진정한 순수 함수: State Hoisting

공식 문서에서 권장하는 패턴을 보면:

"Instead, trigger side-effects from callbacks such as onClick that always execute on the UI thread."

공식 문서의 예시는 State Hoisting 패턴입니다:

// 공식 문서의 권장 패턴 (State Hoisting)
@Composable
fun SharedPrefsToggle(
    text: String,
    value: Boolean,
    onValueChanged: (Boolean) -> Unit  // 상태 변경을 위로 전달
) {
    Row {
        Text(text)
        Checkbox(checked = value, onCheckedChange = onValueChanged)
    }
}

이 패턴을 카운터에 적용하면:

// State Hoisting 적용
@Composable
fun Counter(
    count: Int,
    onCountChange: (Int) -> Unit
) {
    Button(onClick = { onCountChange(count + 1) }) {
        Text("Count: $count")
    }
}

// 사용하는 쪽
@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }
    Counter(
        count = count,
        onCountChange = { count = it }
    )
}

Counter 함수를 분석해보면:

Composition 시점:

  • count: Int → 불변 값을 받음
  • onCountChange → 콜백 함수를 받음
  • ✅ 순수: 외부 상태를 변경하지 않음

Event 시점:

  • onClickonCountChange(count + 1) 호출
  • 함수 자체는 상태를 변경하지 않음
  • 단지 "새 값을 계산해서 콜백으로 전달"할 뿐
  • ✅ 순수: 부수효과가 없음 (결정권은 caller에게)
버전Compose-safe순수 함수공식 문서 권장
BadCounter❌ 위험
GoodCounter✅ 안전△ 허용
Counter (hoisted)✅ 안전권장

핵심 정리:

  • GoodCounterCompose-safe하지만 순수 함수는 아님
  • State Hoisting을 적용한 Counter진정한 순수 함수
  • 이것이 공식 문서에서 권장하는 패턴과 일치함

State Hoisting은 단순히 "상태를 위로 올리는" 패턴이 아닙니다.
Composable 함수를 진정한 순수 함수로 만드는 함수형 프로그래밍 패턴입니다.

"그러면 모든 Composable을 완전히 순수하게 만들어야 하나요?"

아닙니다. 상태는 어딘가에 존재할 수밖에 없습니다.

// Screen Composable - 상태 관리 담당
@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }
    Counter(
        count = count,
        onCountChange = { count = it }  // 여기서 상태 변경
    )
}

CounterScreen은 상태를 더 이상 위로 올릴 곳이 없습니다.
누군가는 상태를 "소유"해야 하고, 그 역할을 Screen Composable이 담당합니다.

State Hoisting의 진짜 목표:

Composable 유형역할순수성
Screen/Container상태 관리, 이벤트 위임Compose-safe (순수 아님)
PresentationalUI 렌더링, 이벤트 전달진정한 순수 함수

목표는 "모든 것을 순수하게"가 아니라,
"순수한 것과 상태 관리하는 것을 명확히 분리" 하는 것입니다.
물론 상태를 더 이상 올릴 곳이 없을 때까지 끌어올려야 할 필요는 없습니다.
관련해서는 State Hoisting 관련 공식 문서를 보시는 것을 추천드립니다.

순수 함수의 장점

  1. 테스트 용이성: 입력과 출력만 검증하면 됨
  2. 예측 가능성: 입력만 보면 출력을 예측할 수 있음
  3. Recomposition 안정성: 언제 다시 실행해도 안전함

1. 테스트 용이성

순수 함수는 입력과 출력만 검증하면 됩니다.
전역 상태나 부수효과를 신경 쓸 필요가 없습니다.

@Test
fun 같은_입력에_항상_같은_출력() {
    composeTestRule.setContent {
        PureGreeting("Alice")
    }
    composeTestRule.onNodeWithTag("greeting")
        .assertTextEquals("Hello, Alice!")
}

2. 예측 가능성

코드를 읽을 때 함수의 입력만 보면 출력을 예측할 수 있습니다.
전역 변수나 숨겨진 의존성을 찾을 필요가 없습니다.

3. Recomposition 안정성

Compose는 언제든지 Recomposition을 실행할 수 있습니다.
순수 함수이기 때문에 이것이 안전합니다.

@Composable
fun DynamicUI() {
    var trigger by remember { mutableStateOf(0) }

    // 순수 함수이므로 여러 번 Recomposition해도 안전
    Text("Hello, World!")

    Button(onClick = { trigger++ }) {
        Text("Trigger Recomposition")
    }
}

다른 선언형 UI와 비교

이 섹션은 순수 함수 개념의 이해를 돕기 위한 비교입니다.
React, Vue, SwiftUI, Haskell 개념적 유사성을 이해하는 참고 자료로 활용하세요.

React, Vue, SwiftUI, Compose는 모두 "순수 함수로 UI를 선언한다"는 공통 철학을 공유합니다.

React

// React: 함수형 컴포넌트
function PureGreeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// 최적화: React.memo로 수동 메모이제이션
export default React.memo(PureGreeting);

특징:

  • 함수형 컴포넌트는 순수 함수 형태
  • React 18이하에서 React.memo()(컴포즈의 지능적 리컴포지션하는 것과 유사)로 수동 메모이제이션(Memoization) 필요.(React 19 에서 플러그인 설정으로 컴파일러가 자동으로 메모이제이션 처리)
  • useMemo, useCallback(컴포즈의 remember과 유사)으로 세밀한 최적화 가능
  • Virtual DOM()으로 변경 감지

Virtual DOM: 실제 DOM의 가벼운 복사본. 변경 사항을 먼저 Virtual DOM에 적용(가상으로 렌더링)한 후, 실제 DOM과 비교(diffing)하여 최소한의 업데이트만 수행합니다.

Vue

<!-- Vue 3: Composition API -->
<script setup>
import { ref, computed } from 'vue'

// 반응형 상태 (Compose의 mutableStateOf와 유사)
const name = ref('World')

// computed: 의존성 기반 자동 캐싱 (Compose의 derivedStateOf와 유사)
const greeting = computed(() => `Hello, ${name.value}!`)
</script>

<template>
  <h1>{{ greeting }}</h1>
</template>

특징:

  • ref/reactive(컴포즈의 mutableStateOf와 유사)로 반응형 상태 관리 (Proxy 기반)
  • computed(컴포즈의 derivedStateOf와 유사)는 순수 함수처럼 동작하며 자동 캐싱
  • watch/watchEffect(컴포즈의LauncedEffect와 유사)로 부수효과 관리

Proxy: JavaScript ES6 기능. 객체의 읽기/쓰기를 가로채서 Vue가 어떤 데이터가 변경되었는지 자동 추적

userProxy.name = "Alice"  → Proxy 감지 → Vue가 반응성 처리
                          ↑
                    여기서 가로챔

SwiftUI

// SwiftUI: View Protocol
struct PureGreeting: View {
    let name: String

    var body: some View {
        Text("Hello, \(name)!")
    }
}

특징:

  • struct로 불변성 보장 (Swift 언어 특성), View 프로토콜이 순수 함수 강제(컴포즈의 Composable 과 유사)
  • Property Wrapper (@State, @Binding)로 상태 관리(각각 컴포즈의 remember, 데이터 바인딩과 유사)
  • Diffing 알고리즘으로 최적화

Haskell

-- Haskell: 타입 시스템이 순수성 보장
pureGreeting :: String -> String
pureGreeting name = "Hello, " ++ name ++ "!"

특징:

  • 언어 자체가 순수 함수형 (기본이 순수, 부수효과는 IO Monad로 격리(부수 효과를 래핑하여 관리))
  • 타입 시스템이 순수성 보장
  • 지연 평가 (Lazy Evaluation == Lazy computating)
  • 참조 투명성이 언어 철학

Compose와의 관계:

  • Compose는 Haskell의 순수 함수 철학을 UI 프레임워크에 적용
  • @Composable = UI 도메인의 순수 함수 선언
  • Effect API = Haskell의 IO Monad와 유사

개념적 계보

Haskell (1990)
   ↓ 순수 함수 철학
Elm Architecture (2012 - 2015)
   ↓ 단방향 데이터 흐름
React (2013) → Vue (2014) → SwiftUI (2019), Jetpack Compose (2021)

연도는 정식 출시 기준이며, 발표 시점과 다를 수 있습니다.

공통 DNA:

  1. 순수 함수: 같은 입력 → 같은 출력
  2. 선언형 UI: "무엇을" 표시할지 선언
  3. 불변성: 상태 변경 대신 새 상태 생성
  4. 단방향 데이터 흐름: State → UI → Event → State

핵심:
순수 함수형 UI는 산업 표준을 향해 달려가고 있습니다. React (2013), SwiftUI (2019), Compose (2021) 모두 이 철학을 공유하며, 각 플랫폼에 최적화된 방식으로 구현했습니다.

Compose는 Kotlin 컴파일러와의 긴밀한 통합으로 이 철학을 가장 자동화하고 안전하게 구현한 프레임워크입니다.


참고 자료

공식 문서

함수형 프로그래밍

비교 프레임워크

profile
안드로이드 개발 공부

0개의 댓글