[트러블슈팅] React Native + Modal in FlatList
React Native 프로젝트에서 FlatList 내 Modal을 사용하면서 발생한 문제를 해결한 과정에 대해 기록한 페이지입니다.
Tags
Troubleshooting, TypeScript, React Native, Mobile
Environment
OS: Windows 11
react-native v0.76.5
✅ 개요
React Native 프로젝트에서 FlatList 내 Modal을 사용하면서 발생한 문제를 해결한 과정에 대해 기록한 페이지입니다.
❓ 문제
⚠️ 오류
Tips
발생한 버그를 간략히 설명해 주세요.
다음 영상과 같이 Modal 내의 TextInput을 클릭하여 키보드가 올라오는 경우 일부 상황에서 Modal이 화면에서 사라지는 문제가 발생하였습니다.
🖥️ 발생 환경
Tips
운영체제, 브라우저, 의존성 목록 등을 작성해 주세요.
- OS: Android
- Galaxy S8+
- react-native v0.76.5
🕘 발생 일시
Tips
버그가 발생한 날짜와 시간을 입력해 주세요. (Ex. 2024년 10월 1일, 오후 3시 30분)
- 2025년 2월 18일, 오후 2시 30분
📖 해결 과정
먼저 문제가 발생하는 상황에 대해 분석하였습니다. 문제 상황을 분석한 결과 Modal 컴포넌트를 포함하는 상위 컴포넌트가 TextInput을 클릭함으로써 키보드에 의해 가려질 때 Modal이 화면이 사라지는 것을 확인하였습니다. 이는 다음과 같이 React Native
의 FlatList
를 사용함으로써 발생한 문제였습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* TourItemList.tsx */
import React from "react";
import { FlatList, Image, Text, View } from "react-native";
import { tw } from "@src/libs/tailwind";
import { TourItem } from "./TourItem";
import { useTourItemList } from "@src/hooks/tour/useTourItemList";
export const TourItemList = () => {
const { tourItemList } = useTourItemList();
return (
<View
style={tw.style(
tourItemList.length === 0 ? "bg-white" : "bg-[#F3F3F3]",
"flex h-full flex-col justify-center px-4"
)}
>
<FlatList
contentContainerStyle={tw`flex flex-col`}
data={tourItemList}
renderItem={({ item }) => <TourItem data={item} />}
keyExtractor={(item) => item.plan.planId.toString()}
ListEmptyComponent={
<View style={tw`flex flex-col items-center gap-[1.125rem]`}>
<Image
style={tw`h-16 w-16`}
source={require("@src/assets/tour/tour-empty.png")}
/>
<Text>아직 저장된 여행이 없어요</Text>
</View>
}
/>
</View>
);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/* TourItemMenu.tsx */
import { COLOR } from "@src/constants/color";
import { useTourItemDelete } from "@src/hooks/tour/useTourItemDelete";
import { tw } from "@src/libs/tailwind";
import React, { useState } from "react";
import { ActivityIndicator, Image, Pressable, Text, View } from "react-native";
import { TourItemTitleModal } from "./TourItemTitleModal";
interface TourItemMenuProps {
planId: number;
planTitle: string;
}
export const TourItemMenu = ({ planId, planTitle }: TourItemMenuProps) => {
const [menuVisible, setMenuVisible] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const { isPending, handleDeleteButtonClick } = useTourItemDelete(
planId,
planTitle
);
return (
<View style={tw`relative`}>
{isPending ? (
<ActivityIndicator style={tw`h-8 w-8`} color={COLOR.PRIMARY_GREEN} />
) : (
<Pressable
style={({ pressed }) =>
tw.style(pressed && "bg-white", "rounded-lg p-1")
}
onPress={() => setMenuVisible((value) => !value)}
>
<Image
style={tw`h-6 w-6`}
source={require("@src/assets/common/menu-icon.png")}
/>
</Pressable>
)}
{menuVisible && (
<View
style={tw`absolute right-1.5 top-8 z-10 flex w-20 flex-col rounded-lg bg-white shadow`}
>
<Pressable
style={({ pressed }) =>
tw.style(pressed && "bg-slate-100", "w-full")
}
onPress={() => {
setMenuVisible(false);
setModalVisible(true);
}}
>
<Text style={tw`py-2.5 text-center`}>수정</Text>
</Pressable>
<Pressable
style={({ pressed }) =>
tw.style(pressed && "bg-slate-100", "w-full")
}
onPress={() => {
setMenuVisible(false);
handleDeleteButtonClick();
}}
>
<Text style={tw`py-2.5 text-center`}>삭제</Text>
</Pressable>
</View>
)}
<TourItemTitleModal
planId={planId}
title={planTitle}
modalVisible={modalVisible}
closeModal={() => setModalVisible(false)}
/>
</View>
);
};
React Native
의 FlatList
는 가상화된 리스트 컴포넌트로, 화면에 보이는 항목만 렌더링합니다. 이를 고려하였을 때 키보드가 올라오기 전에는 Modal을 포함하는 컴포넌트가 화면에 보이므로 Modal이 제대로 렌더링되지만, TextInput을 클릭함으로써 키보드가 올라왔을 때 키보드에 의해 Modal을 포함하는 컴포넌트가 화면에서 사라지면 해당 컴포넌트가 언마운트(unmount)
되어 메모리에서 제거되고, Modal 역시 사라지는 문제였습니다.
즉, 이 문제를 해결하기 위한 핵심은 키보드가 올라왔을 때 Modal을 포함하는 컴포넌트가 화면에서 사라지더라도 Modal의 렌더링 상태가 유지되어야 하는 것이었습니다. 따라서 화면에 보이는 항목만 렌더링하는 VirtualizedList
, FlatList
, SectionList
를 사용해서 리스트를 렌더링하는 것이 아니라, ScrollView
와 map
메서드를 활용해서 리스트를 렌더링하도록 변경하면 문제를 해결할 수 있습니다.
FlatList
대신 ScrollView
로 변경한 코드는 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* TourItemList.tsx */
import React from "react";
import { Image, ScrollView, Text, View } from "react-native";
import { tw } from "@src/libs/tailwind";
import { TourItem } from "./TourItem";
import { useTourItemList } from "@src/hooks/tour/useTourItemList";
export const TourItemList = () => {
const { tourItemList } = useTourItemList();
return (
<ScrollView
style={tw.style(
tourItemList.length === 0 ? "bg-white" : "bg-[#F3F3F3]",
"flex h-full flex-col px-4"
)}
>
{tourItemList.length === 0 ? (
<View style={tw`flex flex-col items-center gap-[1.125rem]`}>
<Image
style={tw`h-16 w-16`}
source={require("@src/assets/tour/tour-empty.png")}
/>
<Text>아직 저장된 여행이 없어요</Text>
</View>
) : (
tourItemList.map((item) => (
<TourItem key={item.plan.planId} data={item} />
))
)}
</ScrollView>
);
};
위와 같이 문제를 해결한 결과는 다음과 같습니다.