
안녕하세요!
오늘은 이전에 만들었던 드래그 앤 드롭과, 폴더 구조의 코드를 개선하고 리팩토링하는 과정을 기록해보려해요!
그렇게 잘 개선하진 않았더라도 생각의 흐름을 봐주시면 감사하겠습니다.
저는 이번 프로젝트를 진행하면서 위와 같이 폴더 구조를 지니고 그 폴더 사이를 드래그앤 드롭 할 수 있는 기능을 만들었었어요.
간단하게 요약하자면 폴더 구조는 아래와 같은 특징을 지니고
- 컴포지트 패턴을 사용하여 폴더와 파일이 합쳐진 목록 생성
- 리스트 컴포저블에서 만약 폴더를 누른다면 폴더의 선택을 기록하고 다시 한번 재귀적으로 리스트 컴포저블 생성
드래그 앤 드롭은 아래와 같은 특징을 지녀요
- 파일 또는 폴더 꾹 눌러 드래그 시작
- 폴더에 1초 닿으면 폴더 안으로 들어감
- 탑 앱바에 1초 닿으면 상위 폴더로 빠져 나감
보면 이 두 기능은 강하게 결합되어 있어요. 사실상 파일 표시의 기능으로 폴더 구조와 파일 이동이 있는 거죠
그런데 문제점은 이런 기능이 홈 화면 자체에도 강하게 결합되어 있다는 부분이였어요.

홈화면이 단순하게 홈화면에 필요한 정보를 상태로 지니는 것이 아니라 파일 이동, 폴더 구조와 관련된 상태가지고 있었고, 심지어 파일 이동의 과정에 관여하기 까지 하였어요

(드래그 관련 상태들이 존재한다)
이렇게 되자 갈수록 홈화면에 기능을 추가하거나 수정하는 것이 어려워지고, 복잡해지기 시작하였어요.
가장 먼저 어떤 부분이 문제인지 조사하여 다음과 같은 문제점을 파악했어요.
- 폴더 구조가 재귀호출로 되어있어 이벤트를 전달하고 상태를 전달하는게 복잡해진다.
- 드래그 앤 드롭 도중 아이템이 탑앱바 영역에도 도달할 수 있어야한다. -> 홈화면의 UI와 강하게 결합된다.
정리해보자면 일단 드래그 앤 드롭의 기능이 탑앱바(홈 UI의 영역)에도 닿을 수 있어야 하기에 드래그를 인식하는 대상이 홈 UI를 감싸야했어요.
이렇게 되면 드래그 인식이 홈에서 이루어지기에 자연스럽게 드래그의 로직 및 상태도 홈에 존재하게 된거죠.
그리고 기능 추가가 어려워진 것은 폴더 구조가 재귀호출이기 때문이였어요.
그래서 이 두가지를 수정하기로 결정하였습니다.
일단 위에서 이야기하였듯 드래그 앤 드롭과 폴더구조는 거의 하나의 기능이라 봐도돼요. 드래그 앤 드롭 자체가 폴더 구조 안에서 돌아가는 기능이기 때문인데, 그래서 저는 이 두가지를 묶어서 하나의 기능으로 분리하고자 하였어요.

하지만 이는 드래그 앤 드롭이 홈 UI 전체에서 가능해야하기에 그냥 간단하게 분리할 수는 없었어요.
또한 그냥 감쌀 뿐만 아니라 내부에는 폴더구조에 해당하는 파일 리스트르 컴포저블이 존재해요.
아래와 같은 구조에서 기능을 유지하며 분리를 해야하는 거죠

쉽지 않은 방식에 고민하다가 저는 Column을 떠올렸어요.

Column은 content라는 컴포저블을 받아서 자신의 안에 content를 배치하는 레이아웃이에요.
물론 Column은 배치형태를 수정하지만, 레이아웃이 아니더라도 컴포저블을 받아서 자신안에 배치하는 형식은 저희도 그냥 사용할 수 있는거죠!
분리 대상이 되는 드래그 인식 안에 홈 UI가 존재해야하는 문제는 이걸로 수정할 수 있겠다 생각하였어요!
그래서 바로 구현에 들어갔습니다.

FileListWrapper라는 컴포저블을 만들고 기존 홈 화면에 있던 드래그 앤 드롭 관련 상태를 전부 이동시켰어요.
이동하는 과정에서 가득한 붉은 에러가 좀 신경쓰였지만... 하나하나 수정하면 될 것 이기에 과감하게 다 먼저 이동시켰어요.
![]() | ![]() |
|---|
마지막으로 기존에 드래그를 인식하던 Box를 이곳으로 가져와 배치하고 내부에 매개변수로 받은 content 를 배치했어요.
이제 기존 Box를 FileListWrapper로 바꾸면!

짠! 드래그 관련 로직을 모두 제거하였어요!

그렇게 대단하진 않지만 깔끔해진 모습에 잠시 만족하다 다음으로 파일리스트를 분리하는 작업을 시작했어요.
하지만 홈 UI에서도 파일리스트와 관련된 변경이나 이벤트를 호출(탑앱바 이름, 뒤로가기)해야했고, 무엇보다 파일리스트 자체가 홈 UI 아래에 존재해야했어요.
이 또한 어떻게 해야하나... 고민을 하던 중 아까와 똑같은 content를 이용하는 방법이 떠올랐어요!
content는 콘텐츠라서 특별해보이지만 사실 일반 컴포저블과 같이 기본적으로는 함수에요. 그렇기에 매개변수를 받을 수 있고, 그걸 또 사용할 수 있어요.
그렇다면 당연히 함수도 넘길수 있고, 함수에 속하는 컴포저블도 content의 매개변수로 넘길 수 있는 것이지요!
우리는 파일리스트를 content의 매개변수로 넘기면 되는거에요!
그럼 홈에서 쓸 수 있는 거죠!
이 생각이 들자 바로 수정에 들어갔어요.
일단 기존 content의 선언을 아래와 같이 수정하였어요.

순서대로 기존 홈 UI에서 쓰던 탑앱바 이름, 파일리스트 뒤로가기(탑앱바이벤트) 그리고 파일리스트에요.
그리고 기존 FileListWrapper안의 content를 배치한 부분에서 파일리스트와 값들을 넘겨주었어요.

이제 이걸 홈 UI에서 받아서 사용하면 됩니다.

![]() | ![]() |
|---|
이렇게 해서 생각보다 더 간단하게 드래그 기능의 리팩토링이 완료되었어요!
다음으로는 FileList 컴포저블 내부의 폴더 구조를 리팩토링 하기로 했어요.
원래 기존 Mapper는 응답으로 들어온 폴더 목록과 파일목록을 바탕으로 트리 데이터 구조를 형성해주는 방식이였어요.

이 구조는 폴더 구조를 구현하는 것에 최적화 되어있었어요. 지금과 같이 재귀적 호출이든 아님 스택으로 관리하든 파일과 폴더를 동일시하여 폴더를 누르면 해당 폴더의 하위 파일들로 리스트를 보여주면 끝이였기때문이에요.
하지만, 드래그 앤 드롭이 생기면서 문제가 발생했어요.
Mapper가 하나의 온전한 형태를 구현해서 주기 때문에 드래그 앤 드롭을 통해 리스트가 바뀌면 이동 위치에 따라 상위, 현재, 하위 폴더의 데이터를 교체해야했어요.

즉 폴더 구조 구현 과정에서 리스트 자체를 가지게 될 경우, 파일이동에 따라 무결성이 깨지게 되며, 지금과 같은 트리 구조에서는 이에 대처하기가 쉽지 않았어요.
결국 다른 형식으로 데이터를 가져오기로 하였어요.

기존과 비슷하게 컴포지트 패턴 형식은 유지하지만, 하나의 트리 구조가 아닌 폴더 Id를 Key로 파일 리스트를 매칭 시킨 Map 형태로 변경하였어요.
또한 폴더는 직접 리스트를 가지는것이 아닌 번호만 가지도록 하였어요.
이렇게 구현하면 직접 리스트를 가지는 것이 아니기에 파일4를 옮겨 폴더 0, 폴더 1, 폴더 2가 변경되더라도 각각의 폴더가 보여질때 해당 폴더에 해당하는 파일 리스트만 새로 받아오면 해결되는 것이지요!
그리고 파일 리스트에 폴더가 간접 참조로 존재하기에 기존의 폴더 탐색 기능을 그대로 유지할 수 있었어요.

기존의 위상정렬을 이용한 Mapper 구현이 아깝기는 하였지만, 이 구조가 훨씬 더 좋은 구조였기에 과감하게 제거하고 Mapper의 코드를 새로 작성하였어요.
fun dtoToFileList(dto: GetFileListResponseDto): Map<Int,List<FileEntity.Document>> { val listMap = mutableMapOf<Int, MutableList<FileEntity.Document>>() for(item in dto.files){ if(listMap[-item.folderId] == null){ listMap[-item.folderId] = mutableListOf() } listMap[-item.folderId]?.add( FileEntity.Document( item.fileId, item.title, item.order, item.folderId ) ) } for(item in dto.folders){ if(listMap[-item.parentFolderId] == null){ listMap[-item.parentFolderId] = mutableListOf() } listMap[-item.parentFolderId]?.add( FileEntity.Folder( item.title, -item.folderId, item.order, -item.parentFolderId ) ) } for(item in listMap){ listMap[item.key] = item.value.sortedBy { it.order }.toMutableList() } return listMap
이전보다 훨씬 깔끔해졌죠?
이제 데이터 형태의 변경은 완료되었고, 본격적으로 FileList 컴포저블의 수정을 시작하였어요.
지금 구조는 이전 글에서 이야기한 것 처럼 폴더를 누르면 해당 폴더 정보를 저장하고 재귀적으로 FileList 컴포저블을 호출하는 방식이에요.
데이터 형태가 바뀌더라도, 이 구조에 적용하는 것에는 문제가 없지만,
장기적으로 보았을때 디버깅이 어렵고, 재귀적인 구조가 성능에 영향을 줄 수 있으며, 상태, 이벤트의 관리가 어려운 문제가 있어 단일 층 구조로 변경하기로 결정하였어요.
또 한편으로는 FileList가 본래 목적인 목록을 보여주는 것 외에 목록을 아예 관리하는 등 너무 과한 책임이 부여되어 있다 판단하기도 했습니다.
그래서 저는 목록 관리의 책임을 FileListWrapper로 넘기고, FileList는 말 그대로 목록만 보여주도록 하고자 했어요.

일단 이에 맞게 가장 먼저 페이지 스택과 이름 스택을 생성했어요.
val nameStack = rememberSaveable(
saver = listSaver(
save = {
it.toList()
},
restore = {
mutableStateListOf(*it.toTypedArray())
}
)
) { mutableStateListOf<String>() }
val pageStack = rememberSaveable(
saver = listSaver(
save = {
it.toList()
},
restore = {
mutableStateListOf(*it.toTypedArray())
}
)
) { mutableStateListOf<Int>() }
mutableStateListOf로 리스트의 변경시 리컴포지션이 일어날 수 있도록 하였고, 상태가 저장 될 수 있도록 하기 위해 rememberSaveable로 지정하고 saver를 커스텀해주었어요.
이때 Map으로 바뀐 데이터 형태를 활용하기 위해, 페이지 스택은 리스트를 직접 지니는 대신 folderId를 지니도록 하였어요.
다음으로 기존의 FileList에서 이루어지던 폴더 진입과 상위 폴더 이동 또한 이 stack을 다루는 것으로 수정하였습니다.
val onPopBack:()->Unit = {
if(nameStack.size>0) nameStack.removeAt(nameStack.size-1)
if(pageStack.size>0) pageStack.removeAt(pageStack.size-1)
}
val onEnter: (FileEntity.Folder)->Unit = {
nameStack.add(it.folderTitle)
pageStack.add(it.folderId)
}
그리고 가장 중요한 현재 보여지는 List는 아래와 같이 페이지 스택에서 가장 최근에 들어온 폴더, 없다면 0번 폴더를 보여지도록 하였어요.
val currentList = fileList[pageStack.lastOrNull()?:0]?: listOf()
이제 FileList 컴포저블에서 인자를 정리하고, 기존 로직을 제거하면 List의 리팩토링이 완료됩니다!

이제 FileList는 파일 목록에 대한 저장을 하지 않고 그냥 표시만 해주게 되었어요!
다 완료된 것 같지만, 마지막으로 하나 더 걸리는 부분이 있었어요.
기존의 FileList는 드래그가 완료될시 삽입지점인 Order를 계산하여 onDragEnd를 호출해요.
이 과정에서 상위폴더로, 또는 하위폴더로 이동될 수 있기에 상위 폴더의 목록, 하위 폴더의 목록이 필요했어요.
하지만 저희는 이제 트리 구조가 아니기에 상위 폴더는 넘길 수 있지만, 하위 폴더 목록을 넘기기가 어려웠어요.
그래서 이 order의 계산을 모든 List를 지니는 FileListWrapper로 이전했어요.
onDragEnd = { parent: Int?, insertIndex: Int ->
if (parent == null) {
draggedItem = null
return@FileList
}
//---------이 부분!
val (order, isReallocate) = calculateOrder(
currentList,
parentFolderId?.let { fileList[it] } ?: listOf(),
(draggedItem as? FileEntity.Folder)?.let { fileList[-it.folderId] } ?: listOf(),
insertIndex,
draggedItem!!
)
val isFolder = draggedItem!! is FileEntity.Folder
onMovedFile(
draggedItem!!.documentId * if (isFolder) -1 else 1,
isFolder,
-parent,
order,
isReallocate
)
draggedItem = null
}
드래그 앤 드롭이 완료될시 삽입 지점과 삽입 대상 폴더를 가져와 계산하도록 한거죠! 오히려 FileList 안에 있을때보다 더 계산이 간단하고 쉬워졌어요.
이렇게해서 폴더 구조의 리팩토링도 완료되었어요!
그렇게 어려운 리팩토링은 아니였지만, 특이한 구조에서 고민할 부분이 많았었던 것 같아요.
또한 이전에 힘들게 구현했던 부분이더라도 더 나은 코드와 가독성, 개선을 위해 과감하게 제거하는게 맞다는 것도 배운 것 같구요!