React Native 프로젝트에서 토스트 메시지 직접 구현하기
React Native 프로젝트에서 토스트 메시지 기능을 직접 구현하는 방법에 대해 정리한 페이지입니다.
Tags
Mobile, React Native, TypeScript
Environment
OS: Windows 11
react v19.0.0
react-native v0.79.2
개요
React Native 프로젝트에서 토스트 메시지 기능을 직접 구현하는 방법에 대해 정리한 페이지입니다.
토스트 메시지 기능을 사용하는 방법
React Native에서 토스트 메시지를 구현하는 방법은 여러 가지가 있습니다.
1. 기본 Android Toast 사용 (Android only)
React Native에서 ToastAndroid
모듈을 제공합니다. 모듈 이름을 보면 알 수 있듯이 Android 플랫폼 내에서만 사용할 수 있다는 단점이 있습니다.
1
2
3
4
5
6
7
import { ToastAndroid } from "react-native";
ToastAndroid.showWithGravity(
"토스트 메시지",
ToastAndroid.SHORT,
ToastAndroid.BOTTOM
);
2. 외부 라이브러리 사용
react-native-toast-message
라이브러리와 같이 외부 라이브러리를 사용하는 방법이 있습니다.
1
npm install react-native-toast-message
1
2
3
4
5
6
7
8
9
10
import Toast from "react-native-toast-message";
export const App = () => {
return (
<>
<YourComponent>
<Toast />
</>
);
};
1
2
3
4
5
6
7
import Toast from "react-native-toast-message";
Toast.show({
type: "success", // 'success' | 'error' | 'info'
text1: "성공!",
text2: "저장에 성공했습니다."
});
3. 커스텀 토스트 컴포넌트 사용
외부 라이브러리를 사용하기 않고 간단한 커스텀 토스트 컴포넌트를 구현하여 원하는 위치와 스타일로 토스트 메시지를 출력할 수 있습니다.
토스트 메시지 기능 구현하기
이 문단에서는 간단한 커스텀 토스트 컴포넌트를 구현하는 방식을 설명합니다.
Step 1 - Context 생성하기
토스트 메시지를 앱 어디서든 쉽게 사용할 수 있도록 하려면, 토스트 컴포넌트를 루트에 두고 전역 상태(Context)를 통해 제어하는 방식이 좋습니다. 따라서 먼저 다음과 같이 토스트 메시지 설정을 담당하는 Context를 생성합니다.
1
2
3
4
5
import { createContext } from "react";
export const ToastDispatcherContext = createContext({
setToastMessage: (_toastMessage: string) => {}
});
Step 2 - ToastProvider 컴포넌트 구현하기
생성된 Context를 이용하여 다음과 같이 ToastProvider 컴포넌트를 구현합니다. 해당 컴포넌트는 토스트 메시지 기능을 사용할 수 있는 컴포넌트들을 children으로 전달받습니다.
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
import { tw } from "@src/shared/lib/utils";
import { ToastDispatcherContext } from "@src/shared/model";
import { useCallback, useMemo, useState } from "react";
import { Animated, useAnimatedValue, View } from "react-native";
interface ToastProviderProps {
children: React.ReactNode;
}
export const ToastProvider = ({ children }: ToastProviderProps) => {
const [message, setMessage] = useState("");
const opacity = useAnimatedValue(0);
const setToastMessage = useCallback(
(toastMessage: string) => {
setMessage(toastMessage);
Animated.timing(opacity, {
toValue: 1,
duration: 500,
useNativeDriver: true
}).start(() => {
setTimeout(() => {
Animated.timing(opacity, {
toValue: 0,
duration: 500,
useNativeDriver: true
}).start();
}, 2000);
});
},
[opacity]
);
const memoizedDispatcher = useMemo(
() => ({ setToastMessage }),
[setToastMessage]
);
return (
<ToastDispatcherContext value={memoizedDispatcher}>
{children}
<View
style={tw`absolute bottom-8 left-4 right-4 flex items-center justify-center px-4`}
>
<Animated.Text
style={tw.style(
"min-w-40 rounded-lg bg-black px-4 py-2 text-sm text-white",
{ opacity }
)}
>
{message}
</Animated.Text>
</View>
</ToastDispatcherContext>
);
};
위의 코드를 설명하자면 다음과 같습니다.
setToastMessage 함수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const opacity = useAnimatedValue(0);
const setToastMessage = useCallback(
(toastMessage: string) => {
setMessage(toastMessage);
Animated.timing(opacity, {
toValue: 1,
duration: 500,
useNativeDriver: true
}).start(() => {
setTimeout(() => {
Animated.timing(opacity, {
toValue: 0,
duration: 500,
useNativeDriver: true
}).start();
}, 2000);
});
},
[opacity]
);
토스트 메시지를 애니메이션을 활용하여 출력할 예정이므로 다음과 같이 토스트 메시지의 opacity를 useAnimatedValue
훅을 사용하여 설정합니다. 토스트 메시지를 출력할 때 처음에는 보이지 않다가 시간이 흐르면서 화면에 출력되어야 하므로 초깃값은 0(={ opacity: 0 })으로 설정합니다. 이후 setToastMessage 함수를 구현합니다. 해당 함수는 토스트 메시지를 출력하는 함수로, 토스트 메시지가 화면에 출력된 후 약 2초 동안 보이다가, 이후에 사라지도록 구현되었습니다. 또한 useState
로 선언한 message 상태가 변경되면 ToastProvider 컴포넌트가 리렌더링되고 setToastMessage 함수도 새로 생성됩니다. setToastMessage 함수는 props로 전달하므로, 불필요한 리렌더링 유발을 방지하기 위해 useCallback
을 사용하였습니다.
memoizedDispatcher 함수
1
2
3
4
const memoizedDispatcher = useMemo(
() => ({ setToastMessage }),
[setToastMessage]
);
구현한 ToastProvider 컴포넌트에서 Context의 value에 setToastMessage 함수를 넘길 때 객체 형태({ setToastMessage: setToastMessage }
)로 넘기고 있습니다. ToastProvider 컴포넌트가 리렌더링될 떄마다 value에 지정한 객체도 새로 생성되므로 해당 Context를 구독하고 있는 모든 컴포넌트에 불필요한 리렌더링을 유발하게 됩니다. 따라서 이를 방지하기 위해 useMemo
를 사용하여 Context의 value에 넘길 객체가 새로 생성되는 것을 방지하였습니다.
return
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
return (
<ToastDispatcherContext value={memoizedDispatcher}>
{children}
<View
style={tw`absolute bottom-8 left-4 right-4 flex items-center justify-center px-4`}
>
<Animated.Text
style={tw.style(
"min-w-40 rounded-lg bg-black px-4 py-2 text-sm text-white",
{ opacity }
)}
>
{message}
</Animated.Text>
</View>
</ToastDispatcherContext>
);
토스트 메시지를 사용할 컴포넌트를 children으로 감싸는 부분입니다. 토스트 메시지는 화면 하단에 표시되도록 구현하였습니다.
Step 3 - React.memo 사용하기
위에서 언급한 ToastProvider 컴포넌트는 토스트 메시지를 사용할 컴포넌트들을 children으로 전달받습니다. setToastMessage 함수를 사용하면 ToastProvider 컴포넌트가 리렌더링되고, 자식 컴포넌트에 해당하는 children도 리렌더링됩니다. 따라서 토스트 메시지를 출력할 때 불필요한 리렌더링을 방지하기 위해 다음과 같이 children에 해당하는 컴포넌트를 React.memo
로 감쌉니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* ... */
import { memo } from "react";
const Stack = createNativeStackNavigator<RootStackParamList>();
export const NavigationComponent = () => {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="BottomTabs">
{/* ... */}
</Stack.Navigator>
</NavigationContainer>
);
};
export const Navigation = memo(NavigationComponent);
Step 4 - app.tsx 설정
다음과 같이 토스트 메시지를 사용할 컴포넌트를, 위에서 구현한 ToastProvider 컴포넌트로 감쌉니다. 이렇게 하면 Navigation 컴포넌트를 포함한 자식 컴포넌트들은 useContext
훅을 사용하여 토스트 메시지를 출력할 수 있게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useEffect } from "react";
import { Navigation } from "./routes";
import SplashScreen from "react-native-splash-screen";
import { ToastProvider } from "@src/shared/ui/toast";
export const App = () => {
useEffect(() => {
SplashScreen.hide();
}, []);
return (
<ToastProvider>
<Navigation />
</ToastProvider>
);
};
Step 5 - 구현 예시
구현한 토스트 메시지 기능을 사용한 예시는 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
import { useContext } from "react";
import { ToastDispatcherContext } from "@src/shared/model";
/* ... */
const { setToastMessage } = useContext(ToastDispatcherContext);
const handleSaveButtonPress = () => {
saveHangulToBrailleHistory(state.recognizedText, state.translatedText);
setToastMessage("번역 기록을 저장하였습니다.");
};
/* ... */