React Native에서 인앱 알림 시스템 구축
본문
알림은 모든 응용 프로그램에서 가장 중요한 부분 중 하나입니다. 이 기사에서는 고급 컴포넌트, 후크 및 애니메이션과 같은 여러 가지 강력한 도구와 기술을 제공하는 React Native 프레임 워크를 활용하여 안정적이고 보기 좋은 알림 시스템을 구현한 경험을 공유하고자 합니다.
https://medium.com/@vadimkorr/building-in-app-notification-system-in-react-native-96efd478ef31
소개
우리가 사용할 몇 가지 기술의 장점을 강조하겠습니다.
1. 후크는 기능적 구성 요소에서 상태 및 기타 React 기능을 사용할 수 있는 강력한 메커니즘으로 React 16.8에서 처음 도입되었습니다.
그들의 장점은 다음과 같습니다.
- 구성 요소 간 상태 저장 논리 재사용
- 구성 요소 복잡성 감소
- 더 나은 우려 분리
- 최적화 (예 : 더 나은 최소화 및 더 안정적인 핫 로딩
2. 고차 컴포넌트 (HOC)는 예제에서 사용할 또 다른 유용한 기술입니다. React 문서에 따르면 HOC는 구성 요소를 가져 와서 새로운 구성 요소를 반환하는 함수입니다.
HOC는 다음에 유용 할 수 있습니다.
- 의존성 주입
- 상속 반전
- 구성 요소 공장
3. 애니메이션은 훌륭한 사용자 경험을 만들기 위해 매우 중요합니다. 앱과 상호 작용할 때 사용자에게 피드백을 제공합니다.
로드맵
안정적인 알림 시스템을 구축하기 위해 다음 단계를 수행합니다.
- 다양한 유형의 알림에 대한 구성 요소 구현
- 컨테이너 구성 요소가 표시 될 컨테이너 구성 요소를 구현하여 알림 개체를 저장하는 상태 관리 시스템 추가
- 더 나은 사용자 경험을 위해 애니메이션 적용
- React의 최신 기능 중 하나를 적용하십시오.
최종 결과는 다음과 같습니다.
이행
1. 통지 구성 요소
성공, 정보, 경고, 오류 등 두 가지 유형의 이벤트를 지원하겠습니다. 약간 다르게 보일 것이지만 동일한 핵심 구현을 공유합니다.
import React from "react"; | |
import PropTypes from "prop-types"; | |
import { StyleSheet, TouchableOpacity, Text } from "react-native"; | |
import { FontAwesome } from "@expo/vector-icons"; | |
const ICON_SQUARE_SIZE = 100; | |
// 'makeNotification' is a HOC | |
export const makeNotification = ( | |
// these values depend on notification type | |
iconName, | |
colorPrimary, | |
colorAccent | |
) => { | |
const NotificationBase = props => { | |
const { title, message, onClosePress } = props; | |
return ( | |
<TouchableOpacity | |
style={[styles.mainContainer, { backgroundColor: colorPrimary }]} | |
onPress={onClosePress} | |
> | |
<FontAwesome | |
style={styles.icon} | |
name={iconName} | |
size={ICON_SQUARE_SIZE} | |
color={colorAccent} | |
/> | |
<Text style={[styles.title, { color: colorAccent }]}>{title}</Text> | |
<Text style={[styles.message, { color: colorAccent }]}>{message}</Text> | |
</TouchableOpacity> | |
); | |
}; | |
NotificationBase.propTypes = { | |
title: PropTypes.string.isRequired, | |
message: PropTypes.string.isRequired, | |
onClosePress: PropTypes.func | |
}; | |
// 'NotificationBase' is returned from the HOC | |
return NotificationBase; | |
}; |
여기서 makeNotification은 HOC입니다. 이 경우 makeNotification은 클래스 팩토리로 작동하며 코드 중복을 피하는 데 도움이 됩니다.
이제 이 함수를 사용하여 위에서 언급 한 유형의 구성 요소를 만들 수 있습니다.
아이콘 이름과 색상을 나타내는 두 개의 문자열만 전달하면 됩니다. 성공 알림 구성 요소를 만드는 것이 얼마나 쉬운 지…
import { makeNotification } from "./makeNotification"; | |
// 'SuccessNotification' component is used for rendering success notifications | |
export const SuccessNotification = makeNotification( | |
"check-circle", // icon name | |
"#dff0d8", // primary color | |
"#3c763d" // accent color | |
); |
… 정보 알림. 다른 알림 유형의 기능적 구성 요소도 비슷한 방식으로 작성됩니다.
import { makeNotification } from "./makeNotification"; | |
export const InfoNotification = makeNotification( | |
"info-circle", | |
"#d9edf7", | |
"#31708f" | |
); |
구성 요소 내에서 notificationsStore를 사용하고 구성 요소를 테스트 할 수 있게 하려면 HOC를 적용해야 합니다.
여기서 HOC는 NotificationControlsInner 구성 요소에 notificationsStore를 삽입하는 데 사용됩니다.
NotificationControls 구성 요소는 폐쇄적이며 어휘 환경에서는 notificationsStore를 유지하고 NotificationControlsInner는 함수 팩토리입니다.
앱 상태를 저장하는 스토어 부분은 다음과 같습니다.
import React from "react"; | |
import PropTypes from "prop-types"; | |
import { View } from "react-native"; | |
import { Button, NotificationsStore, withNotifications } from "../../components"; | |
// description of buttons which add notifications | |
const controls = [{ | |
key: "create-success-notification-button", | |
iconName: "check", | |
colorPrimary: "#cbf0c4", | |
colorAccent: "#3c763d", | |
onPress: store => { | |
store.add( | |
createSuccessNotification( | |
"Success", | |
"This message tells that everything goes fine." | |
) | |
); | |
} | |
}, | |
// ... | |
]; | |
const NotificationControlsInner = props => { | |
// 'store' is passed via 'withNotifications' function | |
const { store } = props; | |
return ( | |
<React.Fragment> | |
{controls.map((b, i) => ( | |
<View | |
key={b.key} | |
style={{ marginBottom: i !== controls.length - 1 ? 10 : 0 }} | |
> | |
<Button | |
iconName={b.iconName} | |
colorPrimary={b.colorPrimary} | |
colorAccent={b.colorAccent} | |
onPress={() => b.onPress(store)} | |
/> | |
</View> | |
))} | |
</React.Fragment> | |
); | |
}; | |
NotificationControlsInner.propTypes = { | |
store: PropTypes.instanceOf(NotificationsStore).isRequired | |
}; | |
// 'NotificationControls' has store injected | |
// it is used in the app (not 'NotificationControlsInner' component) | |
export const NotificationControls = withNotifications( | |
NotificationControlsInner | |
); |
2. 상태 관리
이제 필요한 모든 구성 요소가 구현되었습니다. 예를 들어 다른 알림 유형의 구성 요소 SuccessNotification 및 NotificationControls는 알림 소스입니다.
이제 알림 상태를 관리하기 위한 코드를 추가해야 합니다. 컨텍스트 API 또는 Redux, Unstated 등과 같은 타사 솔루션을 사용하여 상태 관리를 구현하는 방법에는 여러 가지가 있습니다. 이 기사에서는 MobX를 사용합니다. MobX는 사용하기 쉬운 라이브러리이며 상용구 코드가 적습니다. MobX는 상태, 파생 및 작업의 세 가지 개념을 기반으로 합니다.
import { observable, action, computed } from "mobx"; | |
export class NotificationsStore { | |
constructor(notifications) { | |
this.notifications = [...notifications]; | |
} | |
// Observers will be notified and react to changes | |
// in properties which have @observable decorator. | |
@observable | |
notifications = []; | |
// @action decorator should be used on functions | |
// that modify state. In a real app, this function could be called | |
// e.g. on SignalR event. | |
// Observers will be notified when new notification is created | |
@action | |
add(notification) { | |
this.notifications.push(notification); | |
} | |
// 'remove' function also modifies the state, | |
// that's why it should has @action decorator. | |
@action | |
remove(removedNotification) { | |
this.notifications = this.notifications.filter( | |
notification => notification.id !== removedNotification.id | |
); | |
} | |
} |
위의 코드 스니펫 중 하나에서 NotificationControlsInner 구성 요소에 저장소를 삽입했습니다.
해당 구성 요소에는 해당 유형에 대한 새 알림이 추가되는 4 개의 버튼이 있습니다.
각 버튼에는 onPress 속성이 있습니다. NotificationsStore의 add 함수를 호출하는 함수입니다.
관찰자에게 항상 상태 변경을 알리려면 조치 만 사용하여 상태를 변경하십시오.
실수로 공개 알림 필드가 변경되는 것을 방지하기 위해 다음과 같은 방식으로 MobX를 구성합니다.
import React from "react"; | |
import { Routing } from "./src/Routing"; | |
import { configure } from "mobx"; | |
configure({ | |
// 'observed' means that the state needs to be changed through actions | |
// otherwise it throws an error | |
enforceActions: "observed" | |
}); | |
const App = () => <Routing />; | |
export default App; |
observed은 조치를 통해 상태를 변경해야 함을 의미합니다. 더 많은 옵션이 있습니다. 엄격 모드가 활성화되면 상태를 직접 수정하려는 모든 시도…
// trying to push notification object directly to the array | |
store.notifications.push( | |
createSuccessNotification( | |
"Success", | |
"This message tells that everything goes fine." | |
) | |
) |
… 오류가 발생합니다.
우리의 경우 NotificationsInner는 상점을 관찰합니다. 저장소의 어레이에서 알림을 표시합니다. 알림을 제거하거나 추가 할 때마다 NotificationsInner가 다시 렌더링 됩니다.
import React from "react"; | |
import PropTypes from "prop-types"; | |
import { View } from "react-native"; | |
import { observer } from "mobx-react"; | |
import { Notification } from "../Notification"; | |
import { NotificationsStore, withNotifications } from "../store"; | |
// 'observer' function turns component into reactive component | |
// component will be rerendered upon 'notifications' array change | |
const NotificationsInner = observer(props => { | |
const { store } = props; | |
return ( | |
<React.Fragment> | |
{store.notifications.map((notification, index) => ( | |
<View | |
key={notification.id} | |
style={{ | |
marginTop: index !== 0 ? 15 : 0 | |
}} | |
> | |
<Notification | |
type={notification.type} | |
title={notification.title} | |
message={notification.message} | |
onClosePress={() => { | |
store.remove(notification); | |
}} | |
/> | |
</View> | |
))} | |
</React.Fragment> | |
); | |
}); | |
NotificationsInner.propTypes = { | |
store: PropTypes.instanceOf(NotificationsStore) | |
}; | |
export const Notifications = withNotifications(NotificationsInner); |
3. 애니메이션
비즈니스 로직에 대한 작업이 완료되었습니다. UX를 향상 시키자. 여기서는 React Native의 애니메이션 API를 사용하고 있습니다. Animated 라이브러리를 사용하면 강력하고 제작하기 쉬운 애니메이션을 만들 수 있습니다.
알림이 표시되고 숨겨지는 방식에 애니메이션을 적용합니다. 모든 구성 요소는 NotificationBase 구성 요소에서 파생되므로 애니메이션의 초기화 및 구성을 받아야 합니다.
애니메이션은 애니메이션에서 start()를 호출하여 시작됩니다. start ()는 애니메이션이 완료 될 때 호출되는 선택적 콜백을 취할 수 있습니다.
컴포넌트를 애니메이션화하려면 코드 스니펫의 Animated.View와 같은 특수 컴포넌트를 사용해야 합니다. 이러한 구성 요소는 애니메이션 값을 속성에 바인딩하고 애니메이션을 최적화 합니다.
import React from "react"; | |
import PropTypes from "prop-types"; | |
import { StyleSheet, TouchableOpacity, Text, Animated } from "react-native"; | |
import { FontAwesome } from "@expo/vector-icons"; | |
const ICON_SQUARE_SIZE_PX = 100; | |
const ANIMATION_DURATION_MS = 150; | |
const NOTIFICATION_HEIGHT_PX = 120; | |
export const makeNotification = (iconName, colorPrimary, colorAccent) => { | |
class NotificationBase extends React.Component { | |
// initial value is 0 | |
animated = new Animated.Value(0); | |
componentDidMount() { | |
// start opening animation when component inserted into the tree | |
Animated.timing(this.animated, { | |
// animation ends with value 1 | |
toValue: 1, | |
duration: ANIMATION_DURATION_MS | |
}).start(); | |
} | |
onClosePress = () => { | |
const { onClosePress } = this.props; | |
if (onClosePress) { | |
// start closing animation | |
// and call 'onClosePress' after animation ends | |
Animated.timing(this.animated, { | |
// during closing we will animate to initial value | |
toValue: 0, | |
duration: ANIMATION_DURATION_MS | |
}).start(onClosePress); | |
} | |
}; | |
render() { | |
const { title, message } = this.props; | |
const animatedStyles = [ | |
{ | |
opacity: this.animated, | |
height: this.animated.interpolate({ | |
inputRange: [0, 1], | |
// it means that | |
// at 0 ms height will be 0 px | |
// at 75 ms height will be 60 px | |
// at 150 ms height will be 120 px | |
outputRange: [0, NOTIFICATION_HEIGHT_PX], | |
extrapolate: "clamp" | |
}), | |
transform: [ | |
{ | |
translateX: this.animated.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [30, 0], // px | |
extrapolate: "clamp" | |
}) | |
} | |
] | |
} | |
]; | |
// only animatable components can be animated. e.g. Animated.View | |
return ( | |
<TouchableOpacity onPress={this.onClosePress}> | |
<Animated.View | |
style={[ | |
animatedStyles, | |
styles.mainContainer, | |
{ backgroundColor: colorPrimary } | |
]} | |
> | |
<FontAwesome | |
style={styles.icon} | |
name={iconName} | |
size={ICON_SQUARE_SIZE_PX} | |
color={colorAccent} | |
/> | |
<Text style={[styles.title, { color: colorAccent }]}>{title}</Text> | |
<Text style={[styles.message, { color: colorAccent }]}> | |
{message} | |
</Text> | |
</Animated.View> | |
</TouchableOpacity> | |
); | |
} | |
} | |
NotificationBase.propTypes = { | |
title: PropTypes.string.isRequired, | |
message: PropTypes.string.isRequired, | |
onClosePress: PropTypes.func | |
}; | |
return NotificationBase; | |
}; |
이 예는 시간이 지남에 따라 발생하는 애니메이션을 보여줍니다. 그렇기 때문에 Animated.timing()이 사용되었습니다. 부패와 스프링의 두 가지 애니메이션 유형이 더 있습니다.
각각의 값이 시작 값에서 최종 값으로 애니메이션 되는 방식을 제어합니다. 알림의 높이, 불투명도 및 수평 오프셋에 애니메이션을 적용합니다.
우리의 애니메이션은 150ms 이상 발생하며 시간을 애니메이션 값으로 매핑 해야 합니다.
React Native는 interpolate() 함수를 사용하여 허용합니다. 이 기능은 입력 범위를 출력 범위에 매핑합니다.
4. 후크
이 예에서는 NotificationBase 1을 제외한 모든 구성 요소가 작동합니다. 문제는 클래스 구성 요소 만 수명 주기 메서드를 사용할 수 있다는 것입니다. 거기서 애니메이션을 시작한 ComponentDidMount 라이프 사이클 메소드를 사용했습니다. 반응 고리는 수명 주기 처리 방식을 변화 시킵니다. 이제 기능 컴포넌트만으로 전체 앱을 빌드 할 수 있습니다.
import React from "react"; | |
import PropTypes from "prop-types"; | |
import { StyleSheet, TouchableOpacity, Text, Animated } from "react-native"; | |
import { FontAwesome } from "@expo/vector-icons"; | |
const ICON_SQUARE_SIZE_PX = 100; | |
const ANIMATION_DURATION_MS = 150; | |
const NOTIFICATION_HEIGHT_PX = 120; | |
export const makeNotification = (iconName, colorPrimary, colorAccent) => { | |
// now it is a functional component | |
function NotificationBase(props) { | |
const { title, message, onClosePress } = props; | |
const [animated] = React.useState(new Animated.Value(0)); | |
// useEffect is a hook | |
React.useEffect(() => { | |
Animated.timing(animated, { | |
toValue: 1, | |
duration: ANIMATION_DURATION_MS | |
}).start(); | |
}, []); // the empty array is to tell React that effect doesn't depend on props or state | |
return ( | |
<TouchableOpacity | |
onPress={() => { | |
if (onClosePress) { | |
Animated.timing(animated, { | |
toValue: 0, | |
duration: ANIMATION_DURATION_MS | |
}).start(onClosePress); | |
} | |
}} | |
> | |
<Animated.View | |
style={[ | |
{ | |
opacity: animated, | |
height: animated.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [0, NOTIFICATION_HEIGHT_PX], | |
extrapolate: "clamp" | |
}), | |
transform: [ | |
{ | |
translateX: animated.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [30, 0], | |
extrapolate: "clamp" | |
}) | |
} | |
] | |
}, | |
styles.mainContainer, | |
{ backgroundColor: colorPrimary } | |
]} | |
> | |
<FontAwesome | |
style={styles.icon} | |
name={iconName} | |
size={ICON_SQUARE_SIZE_PX} | |
color={colorAccent} | |
/> | |
<Text style={[styles.title, { color: colorAccent }]}>{title}</Text> | |
<Text style={[styles.message, { color: colorAccent }]}> | |
{message} | |
</Text> | |
</Animated.View> | |
</TouchableOpacity> | |
); | |
} | |
NotificationBase.propTypes = { | |
title: PropTypes.string.isRequired, | |
message: PropTypes.string.isRequired, | |
onClosePress: PropTypes.func | |
}; | |
return NotificationBase; | |
}; |
useEffect는 React 후크 중 하나입니다. 렌더링 후에 컴포넌트가 무언가를 해야 한다고 React에 알려줍니다. 경우에 따라 렌더링 할 때마다 효과를 실행하면 성능 문제가 발생할 수 있습니다. 이 경우 메모리 누수를 방지하기 위해 구성 요소 마운트에만 이 효과를 적용해야 합니다. 따라서 []는 React에게 속성이나 상태의 값에 의존하지 않음을 React에 알리기 위한 두 번째 인수로 전달됩니다.
마무리
알림은 앱에서 가장 중요한 부분 중 하나입니다.
새 메시지를 사용자에게 알리고 이벤트를 상기 시키는 등의 작업을 합니다. 이 프로세스를 안전하고 완벽하게 사용자 정의하고 지원할 수 있도록 하기 위한 최상의 솔루션은 모든 비즈니스 요구 사항을 충족하는 기능을 갖춘 사용자 정의 시스템을 구현하는 것입니다.
이러한 시스템을 구현하기 위해 우리는 상위 컴포넌트, MobX에서 제공하는 상태 관리, Animated API 및 적용된 후크 API와 같은 강력한 도구와 기술을 사용했습니다.
소스 코드는 GitHub 리포지토리에서 사용할 수 있습니다. 또한 expo.io 또는 장치에서 앱을 실행할 수 있습니다.
- 이전글푸시 React 네이티브 (코드)에 올바른 방법 19.09.29
- 다음글초보자를 위한 React 네이티브 자습서 반응-Crash Course 2019 19.09.29