본문 바로가기
Development/Android

[Android] Android Fragment 핵심 개념과 활용법

by 은스타 2019. 9. 6.
반응형
Android Fragment 핵심 개념과 활용법

Android Fragment 핵심 개념과 활용법

Android Fragment는 Activity 내의 동작이나 사용자 인터페이스의 일부를 나타내는 모듈식 UI 구성 요소입니다. 여러 개의 Fragment를 하나의 Activity에 결합하여 멀티 패널 UI를 구축할 수 있으며, 하나의 Fragment를 여러 Activity에서 재사용할 수 있습니다. Fragment는 자체 생명주기와 입력 이벤트를 가진 독립적인 모듈로서, 현대 Android 앱 개발의 핵심 아키텍처 구성 요소입니다.
목차
1. Fragment의 정의와 핵심 특징
2. Fragment 생명주기와 Activity의 관계
3. Fragment를 Activity에 추가하는 방법
4. Fragment 디자인 철학과 등장 배경
5. Fragment 활용 베스트 프랙티스

#1. Fragment의 정의와 핵심 특징
1) Fragment란 무엇인가
Fragment는 FragmentActivity 내의 동작 또는 사용자 인터페이스의 일부를 나타냅니다. Fragment를 Activity의 모듈식 섹션이라고 생각하면 이해하기 쉽습니다. 다른 Activity에서 재사용할 수 있는 "하위 Activity"와 같은 개념으로 볼 수 있습니다.
(1) Fragment의 주요 기능
자체 생명주기: Fragment는 독립적인 생명주기를 가지며 onCreate, onStart, onResume 등의 콜백 메서드를 제공합니다
자체 입력 이벤트 처리: 사용자 입력을 독립적으로 수신하고 처리할 수 있습니다
동적 추가/삭제: Activity 실행 중에 Fragment를 추가하거나 삭제할 수 있습니다
재사용성: 동일한 Fragment를 여러 Activity에서 재사용할 수 있습니다
. . . . .
2) Fragment의 멀티 패널 UI 구현
여러 개의 Fragment를 하나의 Activity에 결합하여 창이 여러 개인 UI를 빌드할 수 있습니다. 이는 특히 태블릿과 같은 대형 화면에서 효과적입니다.
(1) 멀티 패널 UI 구성 예시
화면 크기 Fragment 구성 사용자 경험
스마트폰 (세로) 단일 Fragment 전체 화면 목록 → 상세 화면 순차 이동
태블릿 (가로) 2개 Fragment 나란히 배치 좌측 목록, 우측 상세 동시 표시
폴더블 디바이스 화면 상태에 따라 동적 변경 펼침/접힘에 따라 UI 자동 전환
(2) Fragment 호스팅 요구사항
Fragment는 항상 Activity 내에서 호스팅되어야 합니다. Fragment는 독립적으로 존재할 수 없으며, 반드시 FragmentActivity 또는 AppCompatActivity의 컨텍스트 안에서 실행되어야 합니다.

#2. Fragment 생명주기와 Activity의 관계
1) Fragment 생명주기의 Activity 종속성
Fragment의 생명주기는 호스트 Activity의 생명주기에 직접적으로 영향을 받습니다. Activity의 상태 변화에 따라 Fragment의 상태도 자동으로 변경됩니다.
(1) Activity 상태에 따른 Fragment 상태 변화
Activity가 일시정지되면 그 안의 모든 Fragment도 일시정지되며, Activity가 소멸되면 모든 Fragment도 마찬가지로 소멸됩니다. 이는 시스템에서 자동으로 처리하는 동작입니다.
Activity 상태 Fragment 상태 설명
onCreate() onCreate() Activity 생성 시 Fragment도 생성됨
onStart() onStart() Activity가 보이기 시작하면 Fragment도 표시 시작
onResume() onResume() Activity가 활성화되면 Fragment도 활성화
onPause() onPause() Activity 일시정지 시 Fragment도 일시정지
onStop() onStop() Activity가 보이지 않으면 Fragment도 정지
onDestroy() onDestroy() Activity 소멸 시 Fragment도 소멸
. . . . .
2) Activity 실행 중 Fragment의 독립적 조작
Activity가 실행 중인 동안 (onResume 생명주기 상태)에는 각 Fragment를 추가, 제거하는 등 개별적으로 조작할 수 있습니다. 이는 Fragment의 강력한 기능 중 하나입니다.
(1) Fragment Transaction과 Back Stack
Fragment Transaction을 수행할 때 이를 Activity가 관리하는 Back Stack에 추가할 수 있습니다. 각 Back Stack 항목은 발생한 Fragment Transaction의 기록이 됩니다.
Back Stack을 사용하면 사용자가 Back 버튼을 눌러 Fragment Transaction을 역순으로 되돌릴 수 있습니다. 이는 Activity 간 탐색과 유사한 사용자 경험을 Fragment 레벨에서 제공합니다.
// Fragment Transaction 예시
val fragment = DetailFragment()

supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, fragment)
.addToBackStack("detail") // Back Stack에 추가
.commit()

// 사용자가 Back 버튼을 누르면 이전 Fragment로 복원됨
(2) Fragment 조작 가능 시점
Activity가 onResume 상태: Fragment를 자유롭게 추가/제거/교체 가능
Activity가 일시정지 이전 상태: Fragment Transaction 수행 가능
Activity가 onStop 또는 onDestroy 상태: Fragment 조작 불가

#3. Fragment를 Activity에 추가하는 방법
1) ViewGroup 내에서의 Fragment 배치
Fragment를 Activity 레이아웃에 추가하면, 해당 Fragment는 Activity의 뷰 계층 내 ViewGroup에 배치되고 자체적인 뷰 레이아웃을 정의합니다.
(1) Fragment 추가 방법 비교
추가 방법 구현 위치 장점 단점
XML 선언 레이아웃 XML 파일 간단한 구현, 즉시 로딩 런타임 제거 불가
코드 추가 Activity 코드 동적 제어 가능, 유연성 추가 코드 필요
. . . . .
2) XML을 통한 Fragment 선언
Activity의 레이아웃 파일에서 <fragment> 요소로 Fragment를 선언할 수 있습니다. 이는 정적인 UI 구성에 적합한 방법입니다.
(1) XML Fragment 선언 예시
<!-- activity_main.xml -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">

<!-- Fragment를 XML에서 직접 선언 -->
<fragment
android:name="com.example.app.ListFragment"
android:id="@+id/list_fragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />

<fragment
android:name="com.example.app.DetailFragment"
android:id="@+id/detail_fragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2" />

</LinearLayout>
. . . . .
3) 코드를 통한 Fragment 추가
기존 ViewGroup에 Fragment를 추가하는 방법으로 애플리케이션 코드에서 Fragment를 선언할 수 있습니다. 이 방식은 런타임에 동적으로 Fragment를 제어할 수 있는 장점이 있습니다.
(1) 동적 Fragment 추가 예시
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// 최초 실행 시에만 Fragment 추가
if (savedInstanceState == null) {
val listFragment = ListFragment()

supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, listFragment)
.commit()
}
}

// Fragment 교체 메서드
fun showDetail(itemId: Int) {
val detailFragment = DetailFragment.newInstance(itemId)

supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, detailFragment)
.addToBackStack(null)
.commit()
}
}
(2) Fragment 컨테이너 레이아웃
<!-- activity_main.xml -->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />

#4. Fragment 디자인 철학과 등장 배경
1) Fragment 도입의 역사적 배경
Android가 Fragment를 처음 도입한 것은 Android 3.0 (API 레벨 11)부터입니다. 기본적으로 태블릿과 같은 큰 화면에서 보다 역동적이고 유연한 UI 디자인을 지원하는 것이 목적이었습니다.
(1) 태블릿 시대의 도전 과제
태블릿의 화면은 핸드셋 화면보다 훨씬 크기 때문에 UI 구성 요소를 조합하고 상호 교환할 공간이 더 많습니다. Fragment는 개발자가 뷰 계층에 복잡한 변경 내용을 관리하지 않아도 이러한 디자인을 쉽게 구현할 수 있도록 해줍니다.
(2) Fragment Support Library의 등장
현재 Fragment는 Fragment Support Library를 통해 폭넓게 제공됩니다. 이를 통해 구형 Android 버전에서도 최신 Fragment 기능을 사용할 수 있습니다.
. . . . .
2) Fragment의 핵심 디자인 원칙
Activity의 레이아웃을 여러 Fragment로 나누면 런타임에서 Activity의 외관을 수정할 수 있고, 그러한 변경 내용을 Activity가 관리하는 Back Stack에 보존할 수도 있습니다.
(1) 뉴스 앱 예시: 멀티 패널 UI
뉴스 애플리케이션을 예로 들면, 하나의 Fragment를 사용하여 왼쪽에 기사 목록을 표시하고, 또 다른 Fragment로 오른쪽에 기사 내용을 표시할 수 있습니다.
① 두 Fragment 모두 한 가지 Activity에서 나란히 표시
② 각 Fragment에 자체 생명주기 콜백 메서드 존재
③ 각자 사용자 입력 이벤트를 독립적으로 처리
④ 사용자는 같은 Activity 안에서 기사 선택과 읽기를 모두 완료 가능
사용자는 기사를 선택하는 데 하나의 Activity를 쓰고 기사를 읽는 데 다른 Activity를 선택하는 대신, 같은 Activity 안에서 모든 과정을 끝낼 수 있습니다.
(2) 화면 크기별 Fragment 활용 전략
디바이스 Fragment 배치 사용자 흐름
스마트폰 단일 Fragment (전체 화면) 목록 Fragment → 상세 Fragment 순차 이동
7인치 태블릿 2개 Fragment (상하 분할) 상단 목록, 하단 상세 동시 표시
10인치 태블릿 2개 Fragment (좌우 분할) 좌측 목록, 우측 상세 동시 표시

#5. Fragment 활용 베스트 프랙티스
1) 모듈화와 재사용성 원칙
각 Fragment는 모듈식이고 재사용 가능한 Activity 구성 요소로 디자인해야 합니다. 이는 Fragment 아키텍처의 가장 중요한 설계 원칙입니다.
(1) Fragment 독립성 유지
각 Fragment가 자체적인 생명주기 콜백으로 레이아웃과 동작을 정의하기 때문에, 한 Fragment를 여러 Activity에 포함할 수 있습니다. 재사용을 염두에 두고 디자인해야 합니다.
(2) Fragment 간 직접 조작 금지
하나의 Fragment를 다른 Fragment에서 직접 조작하는 것은 삼가야 합니다. Fragment 간 통신은 반드시 호스트 Activity를 통해 이루어져야 합니다.
// 잘못된 방식: Fragment 간 직접 참조
class FragmentA : Fragment() {
fun updateData() {
val fragmentB = activity?.supportFragmentManager
.findFragmentById(R.id.fragmentB) as? FragmentB
fragmentB?.updateUI() // ❌ 직접 조작 금지
}
}

// 올바른 방식: Activity를 통한 통신
interface FragmentListener {
fun onDataChanged(data: String)
}

class FragmentA : Fragment() {
private var listener: FragmentListener? = null

override fun onAttach(context: Context) {
super.onAttach(context)
listener = context as? FragmentListener
}

fun updateData(data: String) {
listener?.onDataChanged(data) // ✅ Activity를 통한 통신
}
}
. . . . .
2) 다양한 화면 크기 대응 전략
모듈식 Fragment를 사용하면 Fragment 조합을 여러 가지 화면 크기에 맞춰 변경할 수 있습니다. 태블릿과 핸드셋을 모두 지원하는 애플리케이션을 디자인하는 경우, 사용 가능한 화면 공간을 최적화하도록 Fragment를 여러 레이아웃 구성에 재사용할 수 있습니다.
(1) 반응형 레이아웃 구현 패턴
동일한 Fragment를 다른 레이아웃 구성 파일에서 재사용하여 반응형 UI를 구현할 수 있습니다.
res/layout/: 스마트폰용 단일 Fragment 레이아웃
res/layout-sw600dp/: 7인치 태블릿용 듀얼 Fragment 레이아웃
res/layout-sw720dp/: 10인치 태블릿용 멀티 Fragment 레이아웃
res/layout-land/: 가로 모드 전용 레이아웃
(2) Fragment 재사용 시 고려사항
고려사항 설명 구현 방법
독립성 Fragment가 특정 Activity에 종속되지 않도록 설계 인터페이스를 통한 통신
유연성 다양한 레이아웃 구성에 적응 가능 동적 크기 조정, ConstraintLayout 활용
데이터 전달 Fragment 간 데이터 공유 방식 ViewModel 또는 Bundle 사용
상태 보존 화면 회전 시 데이터 유지 savedInstanceState 활용
. . . . .
3) Fragment 설계 체크리스트
효과적인 Fragment를 설계하기 위해 다음 원칙을 준수해야 합니다.
단일 책임 원칙: 각 Fragment는 하나의 명확한 역할을 가져야 함
느슨한 결합: Fragment 간 직접 참조 대신 인터페이스 사용
높은 응집도: 관련된 UI와 로직을 Fragment 내에 캡슐화
재사용성 우선: 여러 컨텍스트에서 사용 가능하도록 설계
생명주기 인식: 생명주기 콜백을 올바르게 구현하여 메모리 누수 방지
테스트 가능성: 비즈니스 로직과 UI 로직을 분리하여 테스트 용이성 확보
(1) Fragment 통신 패턴
// Fragment → Activity 통신 (Interface 패턴)
interface OnItemSelectedListener {
fun onItemSelected(itemId: Int)
}

class ListFragment : Fragment() {
private var listener: OnItemSelectedListener? = null

override fun onAttach(context: Context) {
super.onAttach(context)
listener = context as? OnItemSelectedListener
}

private fun selectItem(itemId: Int) {
listener?.onItemSelected(itemId)
}

override fun onDetach() {
super.onDetach()
listener = null // 메모리 누수 방지
}
}

// Activity에서 Fragment 간 통신 중재
class MainActivity : AppCompatActivity(), OnItemSelectedListener {

override fun onItemSelected(itemId: Int) {
val detailFragment = DetailFragment.newInstance(itemId)

supportFragmentManager.beginTransaction()
.replace(R.id.detail_container, detailFragment)
.commit()
}
}
(2) 화면 구성별 최적화 전략
class MainActivity : AppCompatActivity() {

private var isDualPane: Boolean = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// 듀얼 패널 모드 감지
isDualPane = findViewById<View>(R.id.detail_container) != null

if (savedInstanceState == null) {
val listFragment = ListFragment()

supportFragmentManager.beginTransaction()
.add(R.id.list_container, listFragment)
.commit()
}
}

fun showDetail(itemId: Int) {
if (isDualPane) {
// 태블릿: 오른쪽 패널에 표시
val detailFragment = DetailFragment.newInstance(itemId)

supportFragmentManager.beginTransaction()
.replace(R.id.detail_container, detailFragment)
.commit()
} else {
// 스마트폰: 전체 화면으로 교체
val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("itemId", itemId)
startActivity(intent)
}
}
}
. . . . .
4) Fragment 생명주기 관리 모범 사례
Fragment의 생명주기를 올바르게 관리하는 것은 메모리 누수 방지와 앱 안정성에 필수적입니다.
(1) 주요 생명주기 메서드별 처리사항
생명주기 메서드 수행할 작업 주의사항
onAttach() Activity 참조 저장, 리스너 등록 Context 대신 Activity 타입으로 캐스팅
onCreate() 초기 설정, Bundle에서 데이터 복원 UI 초기화는 하지 않음
onCreateView() 뷰 생성 및 반환 뷰 참조는 onViewCreated()에서 수행
onViewCreated() 뷰 초기화, 리스너 설정 뷰가 완전히 생성된 상태
onDestroyView() 뷰 참조 해제, 리스너 제거 메모리 누수 방지를 위해 필수
onDetach() Activity 참조 해제 리스너를 null로 설정
(2) 생명주기 인식 코드 예시
class MyFragment : Fragment() {

private var _binding: FragmentMyBinding? = null
private val binding get() = _binding!!

private var listener: OnDataListener? = null

override fun onAttach(context: Context) {
super.onAttach(context)
listener = context as? OnDataListener
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentMyBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

// 뷰 초기화 및 리스너 설정
binding.button.setOnClickListener {
listener?.onDataChanged("data")
}
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null // 메모리 누수 방지
}

override fun onDetach() {
super.onDetach()
listener = null // Activity 참조 해제
}
}

마무리
Android Fragment는 모듈식 UI 구성의 핵심 요소로서, Activity 내에서 독립적인 생명주기를 가지며 재사용 가능한 컴포넌트입니다. Fragment는 태블릿과 같은 대형 화면을 위해 도입되었지만, 현재는 모든 Android 앱 개발에서 필수적인 아키텍처 구성 요소가 되었습니다.
Fragment의 생명주기는 호스트 Activity에 종속되지만, Activity가 실행 중일 때는 독립적으로 추가, 제거, 교체가 가능합니다. Back Stack을 활용하면 사용자가 Fragment 간 탐색을 자연스럽게 되돌릴 수 있어 뛰어난 사용자 경험을 제공합니다.
효과적인 Fragment 설계의 핵심은 모듈화와 재사용성입니다. Fragment 간 직접 조작을 피하고, Activity를 통한 통신 패턴을 사용하며, 생명주기를 정확히 이해하고 관리해야 합니다. 이러한 원칙을 따르면 다양한 화면 크기와 방향에 유연하게 대응하는 현대적인 Android 앱을 개발할 수 있습니다.
긴 글 읽어주셔서 감사합니다.

끝.
반응형