분류 Reactjs

React Native에서 인앱 알림 시스템 구축

컨텐츠 정보

  • 조회 396 (작성일 )

본문

1*g-EinSyehO6B1kHtH0WNew.jpeg 


알림은 모든 응용 프로그램에서 가장 중요한 부분 중 하나입니다. 이 기사에서는 고급 컴포넌트, 후크 및 애니메이션과 같은 여러 가지 강력한 도구와 기술을 제공하는 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. 애니메이션은 훌륭한 사용자 경험을 만들기 위해 매우 중요합니다. 앱과 상호 작용할 때 사용자에게 피드백을 제공합니다.


로드맵 


안정적인 알림 시스템을 구축하기 위해 다음 단계를 수행합니다.

  1. 다양한 유형의 알림에 대한 구성 요소 구현
  2. 컨테이너 구성 요소가 표시 될 컨테이너 구성 요소를 구현하여 알림 개체를 저장하는 상태 관리 시스템 추가
  3. 더 나은 사용자 경험을 위해 애니메이션 적용
  4. React의 최신 기능 중 하나를 적용하십시오.

최종 결과는 다음과 같습니다.

1*Esz707Zfoh2chG38vqljVQ.jpeg 


이행 


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."
)
)


… 오류가 발생합니다.


1*ns3flNu_9KzO2NA2xpj-OA.png 


우리의 경우 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 또는 장치에서 앱을 실행할 수 있습니다.