고윤태의 개발 블로그

누구나 접근하기 쉽지만 얻어 가는 것이 있는 글을 쓰기 위해 노력 중입니다.

개발

React Native ToastMessage 구현하기

고윤태 2023. 7. 27. 11:37
안녕하세요 이번에 작성하게 될 내용은 제가 react-native에서 ToastMessage을 어떤 계기로 구현하게 되었고 어떤 식으로 구현했는지에 대해 전달을 하고자 작성해 봅니다

💻 개발 환경

React-native(expo) + TypeScript로 진행되었습니다.

🧐 구현하게 된 계기

진행 중인 토이 프로젝트에서 디자인 시스템에 ToastMessage가 존재하였기에 개발을 하게 되었습니다.

 

처음에는 디자이너 분과 얘기해서 라이브러리를 사용하려고 생각을 해서 여러 다양한 ToastMessage를 사용할 수 있는 라이브러리를 찾아보고 좋으신 분들이 작성해 주신 글도 읽어봤습니다.

 

하지만 아쉽게도 제 기준에 미관상 현재 서비스에 맞지 않거나 style에 대한 커스텀이 불가능하거나 사용법이 불편하거나(지극히 주관적인 의견입니다.)

 

예전 회사에서 바닐라 자바스크립트 환경에서 구현해 본 경험도 존재했고
React 환경에서는 구현을 해본 적이 없기에 이번 기회에 해보면 좋을 것 같다고 판단하여 직접 구현해 보기로 했습니다.

만약 제 생각에 개발 비용이 크다 판단이 됐고 이 프로젝트에 대한 기간에 대한 압박이 있었다면 라이브러리를 사용했을 것 같습니다.

🤔 기능 산출 및 개발 계획

개발을 들어가기 전에 이전에 바닐라 자바스크립트 환경에서는 어떤 식으로 구현을 했었는지 기억을 되짚어봤습니다.

const showToast = (message) => {
  const existingToast = document.querySelector('.toast');
  if (existingToast) {
    existingToast.innerText = message;
  } else {
    const toast = document.createElement('div');
    toast.innerText = message;
    toast.classList.add('toast');

    document.body.appendChild(toast);
  }

  setTimeout(() => {
    const toast = document.querySelector('.toast');
    if (toast) {
      toast.remove();
    }
  }, 3000);
};

지금 작성한 이 코드가 그때와 정말 똑같다고 볼 수 없지만 얼추 느낌은 비슷할 거 같습니다.
toast라는 class를 가진 div를 생성하여 body에 appendChild를 통해 엘리먼트를 추가했다가
setTimeout을 통해 일정 시간이 지난 뒤 삭제하는 방식으로 구현했었습니다.

예전에 구현했던 방법을 React에 맞게 변환하여 옮기자고 마음을 먹게 되었습니다.

피그마를 이용해 간단하게 설계도로 옮겨봤습니다.

 

핵심은 ToastContainer라는 것을 만들어 전체를 감싸주고 showToast라는 함수를 Context를 통해 dispatch로 자식 컴포넌트에서 사용을 할 수 있게 하여 showToast가 호출된다면 Toast라는 컴포넌트가 렌더링 될 수 있도록 만들면 되겠다고 생각을 끝냈습니다.

👀 컴포넌트 개발(CODE)

ToastMessage

import { useRef, useEffect } from 'react';
import { Animated } from 'react-native';
import { Text, StyleSheet } from 'react-native';
import Color from '../../../constants/color';
import TYPOS from '../typo';
import SHADOWS from '../shadow';

const ToastMessage = ({ message }: { message: string }) => {
  const fadeAnim = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    Animated.timing(fadeAnim, {
      toValue: 1,
      duration: 300,
      useNativeDriver: true,
    }).start();

    const timer = setTimeout(() => {
      Animated.timing(fadeAnim, {
        toValue: 0,
        duration: 300,
        useNativeDriver: true,
      }).start();
    }, 2000);

    return () => clearTimeout(timer);
  }, [fadeAnim]);

  return (
    <Animated.View
      style={[styles.toastContainer, { opacity: fadeAnim }, SHADOWS.shadow4]}
    >
      <Text style={[TYPOS.body2, { color: Color.white }]}>{message}</Text>
    </Animated.View>
  );
};

const styles = StyleSheet.create({
  toastContainer: {
    justifyContent: 'center',
    backgroundColor: Color.black,
    height: 48,
    paddingHorizontal: 16,
    position: 'absolute',
    bottom: 88,
    left: 8,
    right: 8,
    zIndex: 999,
  },
});

export default ToastMessage;

코드 설명

fadeAnim 변수는 useRef를 사용하여 Animated.Value(0)으로 초기화됩니다. 이는 애니메이션의 현재 값을 나타내는 변수입니다.

useEffect를 사용하여 컴포넌트가 렌더링 될 때 애니메이션을 시작하고, 일정 시간이 지난 후에 애니메이션을 종료하도록 하였습니다.               
fadeAnim를 opacity 스타일에 적용함으로써 FadeIn과 FadeOut 효과를 구현합니다.


ToastProvider

import React, { createContext, useState } from 'react';
import ToastMessage from './ToastMessage';

type State = {
  isVisible: boolean;
  message: string;
};

export type ToastDispatch = {
  showToastMessage: (message: string) => void;
};

export const ToastDispatchContext = createContext<ToastDispatch | null>(null);

const ToastStateContext = createContext<State | null>(null);

export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
  const [toastState, setToastState] = useState({
    isVisible: false,
    message: '',
  });

  const showToastMessage = (message: string) => {
    setToastState({
      isVisible: true,
      message,
    });

    const timer = setTimeout(() => {
      setToastState({
        isVisible: false,
        message: '',
      });
    }, 3000);

    return () => {
      clearTimeout(timer);
    };
  };

  const dispatch = {
    showToastMessage,
  };

  return (
    <ToastStateContext.Provider value={null}>
      <ToastDispatchContext.Provider value={dispatch}>
        {children}
        {toastState.isVisible && <ToastMessage message={toastState.message} />}
      </ToastDispatchContext.Provider>
    </ToastStateContext.Provider>
  );
};

코드 설명

showToastMessage 함수는 새로운 메시지를 받아와 toastState를 업데이트하고, Toast 메시지를 화면에 보여줍니다. 이후, setTimeout을 사용하여 일정 시간(여기서는 3초)이 지난 후에 toastState를 초기화하여 Toast 메시지를 감춥니다.

이렇게 하면 사용자에게 ToastMessage를 보여주고 사라지게 됩니다.


ToastProvider 컴포넌트를 생성하여 Context를 제공합니다. ToastProvider 컴포넌트는 toastState와 showToastMessage 함수를 자식 컴포넌트들에게 전달합니다. 이를 통해 어떤 컴포넌트던지 ToastDispatchContext와 ToastStateContext를 통해 Toast 메시지를 사용할 수 있습니다.

 완성된 ToastMessage

사용법(Provider 설정)

import AppInner from './AppInner';
import { ToastProvider } from './src/components/ui/toast/ToastProvider';

export default function App() {
  return (
    <ToastProvider>
      <AppInner />
    </ToastProvider>
  );
}

사용법(호출하기)

import Container from '../components/layout/Container';
import { useContext } from 'react';
import { ToastDispatchContext } from '../components/ui/toast/ToastProvider';
import Button from '../components/ui/buttons/Button';

const Test = () => {
  const dispatch = useContext(ToastDispatchContext);

  const onPress = () => {
    dispatch?.showToastMessage('toast message');
  };

  return (
    <Container>
      <Button label='show toast' onPressHandler={onPress} />
    </Container>
  );
};

export default Test;

이런 식으로 사용하여 제가 원하는 대로 ToastMessage를 구현할 수 있었습니다.

 

😁 글 작성 후기

예전에 바닐라 자바스크립트를 구현해본 경험을 토대로 React에 맞게 옮기는 작업은 생각보다 재미있는 작업이였습니다.

같은 기능을 하지만 환경에 맞춰서 변환하여 구현을 해야하는 점이 매력있게 느껴졌습니다.

과거에 바닐라 자바스크립트만 사용하여 개발을 하던 때가 생각이 나기도 하면서 그 때 쌓은 경험치 덕에 지금 이 ToastMessage를 쉽고 현재 제 상황에 맞춰 유연하게 구현한 것 같습니다.

 

이번에 ToastMessage를 구현하면서 가장 어려웠던 점은 애니메이션 구현이였습니다.

css로는 애니메이션 예제도 많이 존재하는 편이고 간단한 애니메이션 정도는 구현해본 경험이 이미 존재했기에 익숙했지만

React Native에서는 애니메이션을 구현해본 경험이 많이 없기에 많이 찾아보고 직접 구현하려했으나 실패를 경험했고

 

Chat GPT를 이용하여 간단한 예제를 받은 후 제 상황에 맞게 값을 변경하여 쉽게 구현할 수 있었습니다.

React Native에서 애니메이션 구현 능력에 더 필요성을 느끼고 매력을 느낀다면 예전에 한참 정규표현식에 빠져서 공부할 때 처럼 공부할 수도 있을 것 같습니다.