본문 바로가기

프로그램 개발

React Native로 PDF Viewer 앱 만들기; 5. 화면 개발 - 팝업 창 (Modal)

React Native로 PDF Viewer 앱 만들기

1. React Native 개발 환경 설치

2. react-native-pdf 테스트 앱

3. 화면 개발 - JSX 컴포넌트

4. 화면 개발 - Context API

5. 화면 개발- 팝업 창 (Modal)

6. TypeScript 전환, Jest 추가

7. React Navigation - 1

8. React Navigation - 2

 

9. Modal; 팝업 (Popup) 창 만들기

이제 PDF가 잘 보이는 것도 확인했고, 최상위에 Bar를 만들어 앱 이름과 페이지 정보도 표시해 봤으니, 다른 PDF도 읽을 수 있도록 입력을 받을 수 있는 팝업 창을 하나 만들어 보자.

 

와이어 프레임 구조

디자이너는 아니라서... 대충 이런 느낌으로 만들려면, 팝업 창 컴포넌트, Input 컴포넌트, Open과 Close 버튼(Button) 컴포넌트가 필요한 것으로 생각되어, React Native의 컴포넌트를 찾아 쓰거나 필요하면 Custom 컴포넌트를 만들기로 했다.

 

  • Input 컴포넌트

React Native에 문서에는 TextInput를 찾을 수 있는데, 예제를 봤을 때 useState로 입력받는 text를 저장하고, 현재 text값을 value로 설정하면 사용하는데 문제가 없어 보인다. 안드로이드와 iOS의 차이점도 현재 개발하는데 고민할 정도가 아니어서 그대로 사용하기로 했다.

 

  • Open과 Close 버튼(Button) 컴포넌트

Button은 안드로이드와 iOS가 다르게 표현되는 것으로 이미 알려져 있다. 개발자들이 처음에 많이 보는 "핸드온 리액트 네이티브"책을 봐도 버튼은 만들어서 사용하고 있기도 해서 Button을 만들어 보기로 했다.

 

# /src/components/Button.js

import React from "react";
import { StyleSheet, Text, TouchableOpacity } from "react-native";

export const Button = ({ title, onPress, style }) => {
    return (
        <TouchableOpacity onPress={onPress} style={[styles.button, style]} activeOpacity={0.6}>
            <Text style={[styles.text]}>{title}</Text>
        </TouchableOpacity>
    );
}

const styles = StyleSheet.create({
    button: {
        paddingVertical: 5,
        paddingHorizontal: 10,
        backgroundColor:"#007bff",
        borderRadius: 5,
        elevation: 5,
    },
    text: {
        color: '#fff'
    }
})

 

Button의 이름(title), 스타일(style), onPress 핸들러를 Property로 받으면, 내부에 적용되어 있는 Button의 스타일과 합쳐서 모양을 만들고, 누르면 onPress 핸들러를 호출하도록 간단하게 만들었다. 이렇게 만드는 대표적인 이유는 안드로이드와 iOS가 기본적으로 표현하는 Button의 모양이 많이 다르기 때문에 두 기기에서 비슷하게 나타나게 하기 위해 새로 정의하는 것이 편하기 때문이다.

 

이제 팝업 창 (Modal)을 만들어 보자. 컴포넌트의 이름을 GetModal로 하고 { isVisible, setVisible, setSource }의 세 가지 Property를 MainPage에서 받았다. 앞에서 컴포넌트 간 정보전달을 위해 Context를 사용했는데, 여기서는 부모의 상태정보를 변경해서 PDF의 위치를 PDFWrapper에 전달하는 방법을 사용하기로 했다. 그래서 MainPage (/src/index.js)에

 

# /src/index.js
...
export const MainPage = () => {
    const [ source, setSource ] = useState('http://samples.leanpub.com/thereactnativebook-sample.pdf');
    const [ isVisible, setVisible ] = useState(false);
    return (
        <PageContextProvider>
            <View style={styles.container}>
                <TopBar setVisible={setVisible}></TopBar>
                <PDFWrapper style={styles.pdf}
                    source={source}
                ></PDFWrapper>
            </View>
            <GetModal isVisible={isVisible}
                setVisible={setVisible}
                setSource={setSource}
            ></GetModal>
        </PageContextProvider>
    );
...

 

두 개의 상태를 useState로 만들어 놓으면 isVisible이나 source가 변경되면 MainPage가 다시 렌더링 되도록(그려지도록) 하고 있다. Open과 Close Button은 한 줄에 표시하기 위해 <View>로 감싸고 flexDirection을 'row'로 설정했다.

 

# /src/components/GetModal.js

import { Modal, StyleSheet, Text, TextInput, View } from "react-native";
import { Button } from "./Button";
import { useState } from "react";

const handleOpen = (text, setVisible, setSource) => () => {
    setSource(text);
    setVisible(false);
}

export const GetModal = ({ isVisible, setVisible, setSource }) => {
    const [text, setText] = useState('');
    return (
        <Modal animationType="fade"
            transparent={true}
            visible={isVisible}
        >
            <View style={styles.modalview}>
                <TextInput style={styles.input}
                    onChangeText={setText}
                    value={text}
                ></TextInput>
                <View style={styles.inRow}>
                    <Button title="Open" style={styles.modalButton}
                        onPress={handleOpen(text, setVisible, setSource)}
                    ></Button>
                    <Button title="Close" style={styles.modalButton}
                        onPress={() => setVisible(false)}
                    ></Button>
                </View>
            </View>
        </Modal>
    );
}

const styles = StyleSheet.create({
    modalview: {
        margin: 20,
        backgroundColor: 'white',
        borderRadius: 20,
        padding: 35,
        alignItems: 'center',
        shadowColor: '#000',
        shadowOffset: {
            width: 0,
            height: 2,
        },
        shadowOpacity: 0.25,
        shadowRadius: 4,
        elevation: 5,
    },
    centered: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    },
    input: {
        height: 40,
        margin: 12,
        alignSelf: 'stretch',
        borderWidth: 1,
        borderColor: 'grey',
        borderRadius: 5,
        padding: 10,
    },
    inRow: {
        flexDirection: 'row',
        display: 'flex',
        justifyContent: 'space-evenly',
        alignItems: 'center',
    },
    modalButton: {
        margin: 10,
    }
})

 

PDFWrapper를 수정해서 source를 MainPage에서 전달받도록 바꿨고,

 

# /src/components/PDFWrapper.js
...
export const PDFWrapper = ({source}) => {
    const {setPage} = useContext(PageContext);

    const pdfSource = { uri: source, cache: true };
...

 

TopBar에 Open Button을 넣고 MainPage에서 setVisible을 Property로 전달받아 팝업 창(Modal)이 나타날 수 있게 수정했다.

 

# /src/components/TopBar.js
...
export const TopBar = ({setVisible}) => {
    const { page } = useContext(PageContext);
    return (
        <View style={style.container}>
            <Text style={style.title}>PDF Viewer</Text>
            <Button title="open" onPress={() => {setVisible(true)}}>
            </Button>
            <Pagination total={page.total} current={page.current}></Pagination>
        </View>
...

 

 

이제 화면에서 Open Button을 누르고 새로운 URL을 입력하면 화면이 바뀌는 것을 확인할 수 있다.