Android/오르다 다이어리

[Android/오르다 다이어리] 레거시 리팩토링 04 - Fragment 마이그레이션

yujinius 2025. 2. 24. 23:56

 

[Android/오르다 다이어리] 레거시 리팩토링 04 - Fragment 마이그레이션
 

[Android/오르다 다이어리] 레거시 리팩토링 03 - Navigation Component 적용

[Android/오르다 다이어리] 레거시 리팩토링 03 - Navigation Component 적용이전 글: https://yujinius45.tistory.com/156 [Android/오르다 다이어리] 레거시 리팩토링 02 - targetSdk 34 적용 이슈와 해결보호되어 있는 글

yujinius45.tistory.com

 

지난 번 오르다 다이어리 앱의 메뉴 이동 과정이 불편하여 Navigation을 도입해 사용자 경험을 향상시켰다. 그리고 이제 기존 Activity 중심 구조에서 Fragment로 전환하는 작업이 필요해지게 되었다. 오늘은 그 기나 긴 여정을 보내며 남겨둔 기록을 정리해두려고 한다.


1. 패키지 구조 변경

기존의 Activity 중심 구조에서 Fragment 중심으로 전환하면서, 패키지 구조를 보다 체계적으로 정리했다.
이전에는 모든 UI 관련 클래스가 한 패키지에 섞여 있었다. 이번에 다시 코드를 수정하다 보니, 파일을 찾아 가는 데에 불편함이 있어 각 기능별로 폴더를 나누어 가독성을 높였다.

아래와 같이 변경되었다.

 


2. Activity에서 Fragment로 전환

지난번 Navigation Component를 적용했지만, 아직 모든 Activity를 Fragment로 전환하지 않은 상태였다. 이번 작업에서는 실제 기능을 하는 모든 Activity를 Fragment로 전환하는 작업을 진행했다.

이 과정은 예상보다 시간이 오래 걸렸다. Fragment로 바꾸면서 기존 Activity에서 쉽게 처리하던 기능들이 Fragment 환경에서는 다르게 동작하기 때문에 여러 이슈가 발생했다.

💡 Activity → Fragment 전환 시 주의할 점

  1. Activity에서는 Intent를 사용하여 데이터를 전달하지만, Fragment에서는 Bundle 또는 Safe Args를 활용해야 한다. & Fragment에서 데이터를 다른 Fragment로 전달할 때 Safe Args를 사용할 수 있다.
    • 처음에는 Activity 간 데이터 전달에서 Intent를 사용했었다.
    • Fragment에서는 Intent를 사용할 수 없기 때문에 Bundle을 활용해야 한다는 것을 알게 되었고, 따라서 putSerializable()을 사용하여 데이터를 전달하는 방식으로 변경했었다.
    • Bundle을 사용하고 null 체크를 확인해주는 작업을 해주었는데, Safe Args를 사용하면 아래와 같은 장점이 있다는 것을 알게 되었다.
      • 타입 안전성 보장OnePageDiary를 넘기기로 설정했으면, 다른 타입을 넘길 수 없음.
      • 컴파일 타임 오류 방지 → key값을 잘못 쓰거나 타입이 다르면 컴파일 시점에 오류를 잡아줌.
      • 더 간결한 코드Bundle을 따로 만들 필요가 없음.
    • Safe Args의 장점
    • 그래서 Navigation Component에서 제공하는 Safe Args 기능을 활용하기로 했다.
  2. Fragment에서 getSupportFragmentManager()가 아니라 getChildFragmentManager()를 써야 할 때가 있다.
    • Fragment 내에서 다른 Fragment를 다룰 때는 getChildFragmentManager()를 사용해야 했다.
    • Fragment 내부에서 또 다른 Fragment(SupportMapFragment)를 다룰 때, 기존의 getSupportFragmentManager()를 사용하면 런타임 오류가 발생한다.

3. 데이터 전달 방식 변경 (Intent → Bundle → Safe Args 적용)

  • 처음에는 Fragment 간 데이터 전달을 위해 Bundle을 사용했지만, Navigation Component에서 Safe Args를 제공하는 것을 알게 되었다. Safe Args는 타입 안정성을 보장하고, key 값을 직접 관리할 필요가 없어 코드의 유지보수성이 더 높아지는 장점이 있다.

🔹 Activity에서 데이터 전달 방식 (기존 방식)

기존에는 Intent를 사용하여 데이터를 전달했다.

 

📌 기존 코드 (Activity 간 데이터 전달)

Intent intent = new Intent(myViewHolder.itemView.getContext(), DiarySelectedActivity.class);
intent.putExtra("selectedDiary", dataModels.get(position));
ContextCompat.startActivity(myViewHolder.itemView.getContext(), intent, null);

 

📌 Activity에서 데이터 받기

Intent intent = getIntent();
selectedDiary = (OnePageDiary) intent.getSerializableExtra("selectedDiary");

➡️ 문제점: Activity에서 Fragment로 전환할 때 Intent를 사용할 수 없기 때문에, 새로운 데이터 전달 방식이 필요했다.

🔹 Fragment에서 데이터 전달 방식 (Bundle 적용 후 Safe Args로 개선)

처음에는 Bundle을 사용하여 데이터를 전달했다.

 

📌 Fragment 간 데이터 전달 (Bundle 방식)

Bundle bundle = new Bundle();
bundle.putSerializable("selectedDiary", diary);
Navigation.findNavController(v).navigate(R.id.action_diaryListFragment_to_diarySelectedFragment, bundle);

 

📌 Fragment에서 데이터 받기 (Bundle 방식)

if (getArguments() != null) {
    selectedDiary = (OnePageDiary) getArguments().getSerializable("selectedDiary");
}

➡️ 문제점: Bundle을 사용하면 key-value 형식으로 데이터를 전달해야 하며, 타입 안전성이 보장되지 않는다. 또한 key 관리도 필요하다.

🔹 Safe Args 적용 (최종 개선)

Navigation Component에서는 Safe Args를 제공하여 타입 안전한 데이터 전달을 보장한다.

 

📌 Safe Args 적용 코드 (nav_graph.xml 수정)

<fragment
    android:id="@+id/diarySelectedFragment"
    android:name="smu.team3_orda_diary.ui.diary.DiarySelectedFragment"
    android:label="Diary Selected">
    <argument
        android:name="selectedDiary"
        app:argType="smu.team3_orda_diary.model.OnePageDiary" />
</fragment>

 

📌 Fragment 간 데이터 전달 (Safe Args 적용)

DiaryListFragmentDirections.ActionDiaryListFragmentToDiarySelectedFragment action =
        DiaryListFragmentDirections.actionDiaryListFragmentToDiarySelectedFragment(diary);
Navigation.findNavController(v).navigate(action);

 

📌 Fragment에서 데이터 받기 (Safe Args 적용)

DiarySelectedFragmentArgs args = DiarySelectedFragmentArgs.fromBundle(getArguments());
selectedDiary = args.getSelectedDiary();

 

➡️ Safe Args 적용 후 개선점

  • 타입 안전성 보장 (컴파일 시점에서 타입 검증 가능)
  • Key 값 없이 직관적인 메서드 호출로 데이터 전달 가능
  • Nullable / Non-nullable 자동 처리selectedDiary는 Non-nullable 타입이므로 null 체크가 필요 없음

➡️ 따라서 if (selectedDiary != null) 체크를 하지 않아도 경고가 발생하지 않음.


4. XML 및 리소스 정리

  • 모든 하드코딩된 문자열을 strings.xml로 이동시켰다.
  • UI 대응을 위해 ConstraintLayout을 적용하여 다양한 화면 크기에 적절히 대응할 수 있도록 변경했다.

5. Fragment 내에서 Fragment 사용 시 주의점 (getChildFragmentManager() 활용) 관련 추가 사항

Fragment 내부에서 또 다른 Fragment(SupportMapFragment)를 다룰 때, 기존의 getSupportFragmentManager()를 사용하면 런타임 오류가 발생한다.

 

📌 문제 상황
기존에는 getSupportFragmentManager()를 사용하여 SupportMapFragment를 추가하려고 했으나, Fragment 내부에서 Fragment를 추가할 경우 getChildFragmentManager()를 사용해야 한다는 점을 간과했다. Activity 내에서 Map이 어떻게 사용되고 있는지를 먼저 체크하지 않은 탓이었다.

 

📌 수정 전 (잘못된 코드)

SupportMapFragment mapFragment = new SupportMapFragment();
getSupportFragmentManager().beginTransaction()
        .replace(R.id.map_container, mapFragment)
        .commit(); // ❌ 오류 발생

 

📌 수정 후 (정상 작동 코드)

SupportMapFragment mapFragment = new SupportMapFragment();
getChildFragmentManager().beginTransaction()
        .replace(R.id.map_container, mapFragment)
        .commit(); // ✅ 정상 동작

 

➡️ getChildFragmentManager()를 사용해야 하는 이유

  • getSupportFragmentManager()Activity에 속한 Fragment 관리자를 가져오는 것이기 때문에 Fragment 내부에서 새로운 Fragment를 추가하는 경우에는 사용하면 안 된다.
  • getChildFragmentManager()를 사용하면 현재 Fragment 내부에서 새로운 Fragment를 관리할 수 있다.

6. 가장 어려웠던 작업: 일기장 관련 변경

Fragment로 전환하는 과정에서 가장 어려웠던 부분은 일기장 관련 화면이었다. 특히 포토피커(Photo Picker)의 동작 방식에 대한 오해가 큰 문제였다.

 

포토피커(Photo Picker)란?
Android의 Photo Picker API는 사용자가 갤러리에서 이미지를 선택할 수 있도록 돕는 기능이다.
하지만, 선택된 이미지는 임시 URI로 제공되며, 개발자가 직접 이를 저장하거나 사용해야 한다.

 

📌 잘못된 예상


처음에는 Photo Picker에서 선택한 이미지가 자동으로 DB에 저장될 것이라고 착각했다. 기존 Media를 선택하고 저장하는 로직이 이미 있었기에 그 부분만 Photo Picker로 변경하면 저장될 것이라 생각해버린 것이었다. 하지만 실제로는 앱에서 URI를 활용하여 별도로 저장해야 했다.

 

📌 해결 방법

  1. Photo Picker에서 선택한 이미지의 URI를 DB에 저장하는 방식으로 변경
  2. URI가 만료되지 않도록 앱 내부 저장소에 이미지를 복사하여 보관 및 참조하여 해당 이미지 표시 (갤러리에서 삭제되더라도 앱 내부 저장소에 저장된 이미지가 유지되도록 함)

7. 할 일 목록(DB) 수정 로직 개선 (버그 수정)

🔹 문제 발생 원인

할 일 목록 수정 기능을 테스트하는 과정에서, 같은 날짜에 여러 개의 할 일을 추가한 후 하나를 수정하면 모든 아이템이 동일하게 변경되는 버그를 발견했다.
이는 기존 updateTodo()WHERE writeDate = '기존 Date'로 조건을 걸고 수정했기 때문이다.

🔹 해결 방법

기존 방식에서는 WHERE writeDate=?로 쿼리를 실행하여, 같은 날짜에 존재하는 모든 할 일이 동시에 변경되는 문제가 발생했다. 이를 해결하기 위해 각 할 일 항목이 고유한 ID(id)를 가지도록 설계하고, WHERE id=?로 변경하여 특정 아이템만 수정할 수 있도록 개선했다.

  • ID 기반 업데이트로 변경 (writeDate 대신 id 사용)
    • 기존: UPDATE TODOLIST_TB SET title=?, content=?, writeDate=? WHERE writeDate=?
    • 변경: UPDATE TODOLIST_TB SET title=?, content=?, writeDate=? WHERE id=?

이제 각 할 일 항목이 고유한 ID를 기준으로 업데이트되도록 수정하여, 같은 날짜에 여러 개의 할 일이 있을 경우에도 개별적으로 수정할 수 있게 되었다.


8. 결론

이번 작업을 통해 Activity를 Fragment로 전환하고, Navigation Component와 Safe Args를 활용하는 구조로 변경했다. Activity에서 사용하던 데이터 전달 방식을 Intent → Bundle → Safe Args 순으로 개선하며, 타입 안전성과 유지보수성을 높였다. 특히 Safe Args를 적용하면서 null 체크가 필요 없는 구조로 변경되었으며, Fragment 간 데이터 전달이 더욱 명확하고 안전하게 이루어질 수 있도록 개선했다. 또한, 할 일 목록의 데이터 수정 오류를 해결하여, 같은 날짜의 모든 할 일이 동시에 변경되는 문제를 방지했다.

 

마지막으로, 알람 관련 RingingAlarmActivity는 Fragment로 변경하지 않고 유지했다. 알람이 울릴 때는 사용자의 인터랙션이 흔들기로 제한되어 독립적인 화면이 필요하며, Navigation Component와의 연관성이 낮아 별도의 Activity로 유지하는 것이 적절했기 때문이다.

 

이번 리팩토링을 통해 코드 구조가 단순해지고, 유지보수성이 향상되었으며, UI/UX 측면에서도 개선된 사용자 경험을 제공할 수 있게 되었다.

 

▼ 변경된 구조

이제 홈 화면을 들렸다가 다른 곳으로 이동하지 않아도 된다!

아래 영상과 같이 Navigation을 통해 메뉴를 빠르게 이동할 수 있어서 사용자 경험이 개선되었다.