개발경험

[React-Native] 전역적으로 사용할 수 있는 모달 만들기

Mactto 2024. 2. 6. 16:25
728x90

모달을 구현하다 보니 모달 자체에 대한 코드가 너무 길어 일회성으로 사용하지 않고 전역적으로 사용하는 방법을 고민하게 되었습니다.

이를 위해 여러 레퍼런스들을 찾아보니 일반적으로 Redux나 Recoil과 같은 전역 상태 관리 라이브러리에 모달의 상태를 저장하여 구현하는 방법을 소개하고 있었습니다.

 

저는 전역 상태 관리 라이브러리를 사용하지 않고 Context API를 사용하고 있기에 Context API를 사용해 전역 모달을 구현한 과정을 기록하고 구현 도중 만났던 이슈와 해결 방법들을 기록하려 합니다.

 

1. 일반적인 Modal 구현 방법

일반적으로 react native에서 모달은 아래와 같이 구현할 수 있습니다.

import React, { useState } from 'react';
import { View, Text, Modal, Button } from 'react-native';

const App = () => {
  const [isModalVisible, setModalVisible] = useState(false);

  const toggleModal = () => {
    setModalVisible(!isModalVisible);
  };

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Button title="Open Modal" onPress={toggleModal} />

      <Modal
        animationType="slide"
        transparent={true}
        visible={isModalVisible}
        onRequestClose={toggleModal}
      >
        <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
          <View style={{ backgroundColor: 'white', padding: 20, borderRadius: 10 }}>
            <Text>Modal Content</Text>
            <Button title="Close Modal" onPress={toggleModal} />
          </View>
        </View>
      </Modal>
    </View>
  );
};

export default App;

 

하지만 모달이 여러 화면에서 필요할 경우 이런 긴 코드가 화면마다 작성되어야 한다는 문제가 있고 모달과 화면 사이에 강한 의존성이 생긴다는 문제가 있었습니다.

즉, 모달이 일회성으로만 사용이 가능하고 화면과 1:1로 강한 의존성이 생긴다는 단점이 있었습니다.

그래서 이 의존성을 끊고 전역적으로 선언해 모달이 필요한 곳에서 호출하는 것만으로 모달을 띄울 수 있도록 구현하고 싶었습니다.

 

2. 전역적인 모달 구현하기

 

필요한 모달은 첫번째 사진 같이 단순히 처리 상황들을 전달하기 위한 Info 모달과 두 번째 사진과 같이 중요한 사항들에 대해 다시 한번 사용자 행동을 묻는 confirm 모달이었습니다.

 

모달 내의 컨텐츠들을 레이아웃은 동일하되 문구 정도가 달라진다는 특징이 있었고 confirm 모달의 경우 confirm 버튼을 누르면 이전 시나리오에 따라 다음 시나리오가 결정된다는 까다로움이 있었습니다.

 

이를 구현하기 위해 Context API에 전역적으로 모달의 상태를 저장하였는데 모달은 2개의 모달이 같이 띄워지는 경우가 없다는 특징이 있기 때문에 전역 상태로 그 상태를 관리할 목적이었습니다.

 

2-1. Info 모달 구현 과정

먼저 Info 모달 컴포넌트를 구현한 방법은 아래와 같습니다.

먼저 전역적으로 상태를 저장하기 전에 Info 모달 컴포넌트를 아래와 같이 작성하였습니다.

import React, {FC} from 'react'
import {Modal,  TouchableOpacity, View, StyleSheet} from 'react-native'
import CustomText from '../Utils/CustomText'

interface InfoModalProps {
  title: string
  content: string
  visible: boolean
  onClose: () => void
}

const InfoModal: FC<InfoModalProps> = ({title, content, visible, onClose}) => {
  return (
    <Modal transparent={true} visible={visible} onRequestClose={onClose}>
      <View style={styles.container}>
        <View style={styles.modalView}>
          {title && (
            <CustomText style={{fontSize: 18, marginTop: '5%'}}>
              {title}
            </CustomText>
          )}
          {content && (
            <CustomText style={{fontSize: 15, marginTop: '5%'}}>
              {content}
            </CustomText>
          )}
          <TouchableOpacity
            style={{
              width: '100%',
              height: 30,
              backgroundColor: '#000000',
              justifyContent: 'center',
              alignItems: 'center',
              marginTop: '10%',
            }}
            onPress={onClose}>
            <CustomText style={{color: '#ffffff'}}>확인</CustomText>
          </TouchableOpacity>
        </View>
      </View>
    </Modal>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'gray',
  },

  modalView: {
    width: '80%',
    backgroundColor: 'white',
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
})

export default InfoModal

 

props를 보시면 title과 content, visible, onClose 함수를 전달받는데 각각의 역할은 아래와 같습니다.

  • title : 모달의 타이틀 부분
  • content : 모달의 컨텐츠 부분
  • visible : 모달이 open 상태인지 close 상태인지 구분 (전역 상태 값으로 관리)
  • onClose : 닫기 버튼이 눌릴 경우 모달을 닫는 함수

이후 위에서 만든 Info 컴포넌트를 아래 코드에서와 같이 Context API에서 상태를 선언하고 컴포넌트를 호출하였습니다

interface ConfirmModalState {
  title: string
  content: string
  isOpen: boolean
}

 

위와 같이 Info Modal의 상태를 저장하였는데 title과 content를 저장하고 모달의 오픈 여부를 판단할 수 있도록 isOpen이라는 boolean 값을 두었습니다.

이후 아래와 같이 각 전역 함수들을 선언하고 Info 컴포넌트를 호출하였습니다.

const AppContext = createContext<ContextProps | undefined>(undefined)

export const AppProvider = ({children}) => {
  const [infoModalState, setInfoModalState] = useState<InfoModalState>({
    title: '',
    content: '',
    isOpen: false,
  })

  const infoModalOpen = (data: InfoModalParameter) => {
    const {title, content} = data

    setInfoModalState({
      title: title,
      content: content,
      isOpen: true,
    })
  }

  const infoModalClose = () => {
    setInfoModalState({
      title: '',
      content: '',
      isOpen: false,
    })
  }

  return (
    <AppContext.Provider
      value={{
        infoModalOpen,
        infoModalClose,
      }}>
      {infoModalState.isOpen && (
        <InfoModal
          title={infoModalState.title}
          content={infoModalState.content}
          visible={infoModalState.isOpen}
          onClose={infoModalClose}
        />
      )}
      {children}
    </AppContext.Provider>
  )
}

 

각 전역 함수들의 역할은 다음과 같습니다.

  • infoModalOpen : Info 모달을 띄우는 함수입니다. Info 모달이 필요한 곳에 해당 함수를 호출하여 전역적으로 모달을 띄울 수 있습니다.
  • infoModalClose : 확인 버튼을 눌렀을 때 모달을 닫는 함수입니다. Info 모달 컴포넌트에 close 버튼에 onPress 함수로 등록해 사용하는 전역 함수입니다.

위처럼 하면 Info 모달을 띄우기 위해 infoModalOpen 함수를 title과 content 파라미터 값을 설정해 호출하기만 하면 화면 어느 곳에서도 필요한 title과 content 값으로 Info 모달을 띄울 수 있습니다.

 

아래는 사용 예시입니다.

infoModalOpen({title: '', content: '회원가입을 완료해주세요.'})

 

이렇게 Info 모달을 전역적으로 선언하였습니다.

그럼 이제 confirm 모달을 만든 과정을 설명드리겠습니다

 

2-2. Confirm 모달 구현 과정

Info 모달을 확인 버튼만 누르면 모달을 끄면 되는 간단한 동작이었지만

confirm 모달의 경우 confirm 버튼을 누르면 이후 동작이 제각각 다르다는 이슈가 있어서

이를 어떻게 구현할 지 고민하다가 미리 정의한 콜백 함수를 confirm 모달을 선언할 때 전역 값으로 저장하고

confirm 버튼이 클릭했을 때만 해당 콜백 함수를 호출하는 방법으로 구현하였습니다.

 

코드는 전체적으로 Info 모달과 비슷하지만 콜백 함수를 저장하고 호출했다는 부분에만 집중해서 보시면 됩니다.

전체적인 코드 플로우가 동일하기에 별도 설명은 생략하겠습니다.

 

  • Confirm Modal 컴포넌트
import {FC} from 'react'
import {Modal, StyleSheet, TouchableOpacity, View} from 'react-native'
import CustomText from '../Utils/CustomText'

interface ConfirmModalProps {
  content: string
  visible: boolean
  onConfirm: () => void
  onCancel: () => void
}

const ConfirmModal: FC<ConfirmModalProps> = ({
  content,
  visible,
  onConfirm,
  onCancel,
}) => {
  return (
    <Modal transparent={true} visible={visible}>
      <View style={styles.container}>
        <View style={styles.modalView}>
          {content && (
            <CustomText style={{fontSize: 15, marginTop: '5%'}}>
              {content}
            </CustomText>
          )}
          <View style={styles.btnContainer}>
            <TouchableOpacity
              style={{
                height: 45,
                width: '48%',
                backgroundColor: '#F7F9FA',
                justifyContent: 'center',
                alignItems: 'center',
                marginTop: '10%',
              }}
              onPress={onCancel}>
              <CustomText>취소</CustomText>
            </TouchableOpacity>
            <TouchableOpacity
              style={{
                height: 45,
                width: '48%',
                backgroundColor: '#000000',
                justifyContent: 'center',
                alignItems: 'center',
                marginTop: '10%',
              }}
              onPress={onConfirm}>
              <CustomText style={{color: '#ffffff'}}>확인</CustomText>
            </TouchableOpacity>
          </View>
        </View>
      </View>
    </Modal>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'gray',
  },

  modalView: {
    width: '80%',
    backgroundColor: 'white',
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },

  btnContainer: {
    flexDirection: 'row',
    width: '100%',
    marginHorizontal: '10%',
    justifyContent: 'space-between',
  },
})

export default ConfirmModal

 

  • Context API 코드
interface ConfirmModalState {
  content: string
  isOpen: boolean
  callback?: () => void;
}

const AppContext = createContext<ContextProps | undefined>(undefined)

export const AppProvider = ({children}) => {
  const [confirmModalState, setConfirmModalState] = useState<ConfirmModalState>({
    content: '',
    isOpen: false,
  })
  
  const confirmModalOpen = (data: ConfirmModalParameter) => {
    const {content, callback} = data;

    setConfirmModalState({
      content: content,
      isOpen: true,
      callback: callback,
    })
  }

  const confirmModalConfirm = () => {
    if (confirmModalState.callback) {
      confirmModalState.callback();
    }
  }

  const confirmModalClose = () => {
    setConfirmModalState({
      content: '',
      isOpen: false,
      callback: undefined
    })
  }
  
  return (
    <AppContext.Provider
      value={{
        confirmModalOpen,
        confirmModalConfirm,
        confirmModalClose,
      }}>
      {confirmModalState.isOpen && (
        <ConfirmModal
          content={confirmModalState.content}
          visible={confirmModalState.isOpen}
          onConfirm={confirmModalConfirm}
          onCancel={confirmModalClose}
        />
      )}
      {children}
    </AppContext.Provider>
  )
}

 

  • Confirm 모달 호출 코드
  const confirmCallback = () => {
    signOut()
    confirmModalClose()
  }

  const withdrawalHandler = async () => {
    confirmModalOpen({content: '정말 탈퇴 하시겠습니까?', callback: confirmCallback})
  }
728x90