상태 재구성
1. 개요
1. 개요
상태 재구성은 Android Compose와 같은 선언형 UI 프레임워크의 핵심 동작 원리이다. 이 개념은 시간이 지남에 따라 변할 수 있는 값인 상태와, 그 상태 변화에 따라 사용자 인터페이스를 자동으로 갱신하는 재구성 과정을 포함한다.
상태는 일반적으로 부모 컴포저블 함수에서 선언되며, 상태 호이스팅 패턴을 통해 자식 컴포저블로 전달된다. 상태가 변경되면, 해당 상태를 읽는 모든 자식 컴포저블에 재구성이 트리거되어 새로운 상태 값을 반영한 UI를 다시 그린다. 이는 단방향 데이터 흐름 구조를 형성하여 테스트 가능성과 코드 캡슐화를 향상시킨다.
효율적인 재구성을 위해 Compose는 지능적 재구성 기법을 사용한다. 이는 상태 변화의 영향을 직접 받는 컴포저블 함수만을 다시 호출하여, 불필요한 전체 UI 트리 갱신을 방지한다. 상태를 올바르게 관리하려면 remember나 rememberSaveable과 같은 효과 API를 사용하여 재구성 과정에서도 상태 값을 유지해야 한다.
2. 상태(State)의 개념
2. 상태(State)의 개념
상태는 안드로이드 Jetpack Compose와 같은 선언형 UI 프레임워크에서 시간이 지남에 따라 변할 수 있는 값을 의미한다. 이는 매우 광범위한 정의로, 데이터베이스에 저장된 값부터 클래스 변수까지 모든 항목을 포함할 수 있다. Compose에서는 UI가 현재 상태를 반영하도록 구성되며, 상태가 변화하면 재구성이라는 과정을 통해 화면이 업데이트된다.
상태는 일반적으로 부모 컴포저블 함수에서 선언된다. 이렇게 함으로써 상태의 변화가 해당 상태를 전달받은 모든 자식 컴포저블에 반영되는 단방향 데이터 흐름을 구현할 수 있다. 컴포저블 함수는 함수이기 때문에, 상태값을 일반적인 지역 변수로 저장하면 재구성 시마다 값이 초기화되어 소실된다. 따라서 재구성이 발생해도 값을 유지하기 위해 remember 키워드와 mutableStateOf() API를 함께 사용하여 관찰 가능한 상태 객체를 생성한다.
상태를 관리하는 컴포저블은 상태가 있는(Stateful) 컴포저블이라 부르며, 상태를 직접 관리하지 않고 부모로부터 전달받는 컴포저블은 상태 없는(Stateless) 컴포저블이 된다. 상태 관리의 핵심 원칙 중 하나는 상태 호이스팅으로, 상태를 가능한 한 사용하는 컴포저블들의 공통 상위 요소로 끌어올려 관리함으로써 코드의 재사용성과 테스트 용이성을 높인다.
3. 재구성(Recomposition)의 원리
3. 재구성(Recomposition)의 원리
재구성은 안드로이드 컴포즈에서 상태가 변경될 때 사용자 인터페이스를 업데이트하는 핵심 메커니즘이다. 상태는 시간이 지남에 따라 변할 수 있는 값으로 정의되며, 이러한 상태의 변화가 감지되면 컴포즈는 해당 상태를 읽는 컴포저블 함수들을 자동으로 다시 호출한다. 이 과정을 재구성이라고 한다. 재구성의 목적은 현재의 상태 값을 반영하여 화면을 다시 그리는 것이다.
컴포즈는 효율성을 위해 지능적 재구성 방식을 채택한다. 이는 상태 변화 시 모든 UI 컴포넌트 트리를 재구성하는 대신, 변경된 상태를 실제로 읽고 있는 컴포저블 함수들만 선별적으로 다시 실행하는 것을 의미한다. 예를 들어, 특정 텍스트 필드의 값만 변경되었다면, 그 값을 참조하는 컴포저블과 그 자식들만 재구성 대상이 된다. 이를 통해 불필요한 렌더링 오버헤드를 방지하고 앱의 성능을 최적화한다.
재구성이 올바르게 동작하려면 상태가 적절히 관리되어야 한다. 컴포저블 함수 내부에서 일반 변수로 상태를 선언하면, 함수가 재호출될 때마다 값이 초기화되어 지속성을 유지할 수 없다. 따라서 remember와 mutableStateOf()를 함께 사용하여 재구성 과정에서도 값을 기억하고, 상태 변화를 관찰 가능하게 만들어야 한다. 상태는 일반적으로 이를 사용하는 모든 컴포저블의 가장 가까운 공통 부모에 선언되어, 단방향 데이터 흐름 구조를 유지한다.
재구성 과정에서 주의할 점은 의도치 않은 부수 효과를 방지하는 것이다. 컴포저블 함수 내에서 토스트 메시지 출력이나 네트워크 요청과 같은 비컴포저블 로직이 포함되면, 상태 변경에 따라 해당 로직이 반복적으로 실행될 수 있다. 이러한 부수 효과는 LaunchedEffect 같은 이펙트 API를 사용하여 제어해야 한다. 또한, derivedStateOf를 활용하면 불필요한 재구성 트리거를 줄여 성능을 더욱 개선할 수 있다.
4. 상태 호이스팅(State Hoisting)
4. 상태 호이스팅(State Hoisting)
상태 호이스팅은 Android Compose에서 상태를 관리하는 중요한 패턴이다. 이는 자식 컴포저블이 직접 상태를 소유하는 대신, 그 상태를 자신의 부모 컴포저블로 "끌어올리는" 방식을 의미한다. 구체적으로, 상태 변수와 그 상태를 변경하는 이벤트 핸들러 함수를 부모 컴포저블에서 선언한 후, 이를 자식 컴포저블에 매개변수로 전달한다. 이렇게 하면 원래 상태를 가지고 있던 자식 컴포저블은 상태를 직접 관리하지 않는 Stateless 컴포저블로 변환된다.
이 패턴을 적용하면 단방향 데이터 흐름 구조를 구현할 수 있다. 자식 컴포저블은 UI 이벤트를 감지하여 부모로부터 전달받은 콜백 함수를 호출하기만 한다. 실제 상태 변경과 이에 따른 재구성은 상태를 소유한 부모 컴포저블의 책임이 된다. 예를 들어, 텍스트 필드 컴포저블은 자신의 텍스트 값을 직접 저장하지 않고, 부모로부터 현재 값(value)과 값이 변경될 때 호출할 함수(onValueChange)를 전달받아 사용한다.
상태 호이스팅의 주요 장점은 재사용성과 테스트 용이성, 상태 캡슐화에 있다. 상태 로직이 한 곳에 집중되므로 동일한 상태를 여러 컴포저블이 쉽게 공유할 수 있으며, UI를 구성하는 코드와 상태를 변경하는 비즈니스 로직을 분리하여 테스트하기가 더 쉬워진다. 또한, 복잡한 UI 계층 구조에서도 상태 변경 지점을 명확하게 제한함으로써 버그 발생 가능성을 줄일 수 있다. 이 패턴은 ViewModel과 같은 상태 홀더와 결합하여 앱 아키텍처를 구성하는 데에도 널리 활용된다.
5. 상태 관리 패턴
5. 상태 관리 패턴
상태 관리 패턴은 Android Compose에서 UI의 일관성과 테스트 용이성을 보장하기 위한 설계 원칙이다. 핵심은 단방향 데이터 흐름(Unidirectional Data Flow)을 따르는 것이다. 이 패턴에서는 상태가 특정 컴포저블에서 소유되고, 하위 컴포저블은 이벤트를 상위로 전달만 하며 상태를 직접 변경하지 않는다.
이를 구현하는 주요 기법이 상태 호이스팅(State Hoisting)이다. 상태와 상태를 변경하는 로직을 컴포저블 계층 구조에서 가능한 높은 위치, 즉 상태를 필요로 하는 모든 자식 컴포저블의 공통 부모로 끌어올린다. 그 결과, 상태를 소유하는 컴포저블은 Stateful 컴포저블이 되고, 매개변수로 상태와 이벤트 핸들러만 받는 자식 컴포저블은 Stateless 컴포저블이 된다.
이러한 패턴을 적용하면 몇 가지 장점이 있다. 첫째, 상태 변경 로직이 한 곳에 집중되어 캡슐화가 잘되고 버그 발생 가능성이 줄어든다. 둘째, 상태와 UI 로직이 분리되어 단위 테스트가 용이해진다. 셋째, 동일한 상태를 여러 컴포저블이 쉽게 공유하고 관찰할 수 있어 UI 일관성을 유지하기 좋다. 복잡한 상태 관리는 ViewModel과 같은 상태 홀더(State Holder)에 위임하여 컴포저블의 수명 주기와 분리하는 것이 일반적이다.
