본문 바로가기

Study/React

[React Native] axios를 사용한 비동기 통신과 장면 전환

1. 컨트롤러


axios를 통해 JSON 형태로 데이터를 주고 받는다. 이 때 axios가 url 패턴을 찾아서 값을 전달해줘야 하는데 다음과 같은 컨트롤러를 통해 값을 전달하고 결과를 받아온다.


import com.hwangduil.springbootbackend.dto.BbsDto;
import com.hwangduil.springbootbackend.service.BbsService;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class BbsController {

    public final Logger logger = LoggerFactory.getLogger(BbsController.class);
    private final BbsService service;

    @GetMapping("/getBbsList")
    public List<BbsDto> getBbsList() {
        logger.info("BbsController getBbsList");
        return service.getBbsList();
    }

    @GetMapping("/insertBbs")
    public String insertBbs(BbsDto dto) {
        logger.info("BbsController insertBbs() ");
        boolean isInsert = service.insertBbs(dto);
        if (isInsert) {
            return "ok";
        }
        return "no";
    }

    @GetMapping("/getBbsDetail")
    public BbsDto getBbsDetail(int seq) {
        logger.info("BbsController getBbsDetail()");
        service.readcount(seq);
        return service.getBbsDetail(seq);
    }
}

2. App.tsx에 네비게이션 메뉴 준비



name으로 전달받은 값을 통해 특정 화면으로 이동하도록 하기 위해서는 NavigationContainerStack이 필요하다.


import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import Login from "./src/screens/members/Login";
import Account from "./src/screens/members/Account";
import Bbs from "./src/screens/bbs/Bbs";

/*
    필수 설치 패키지 목록
    npm install @react-navigation/native
    npm install @react-navigation/native-stack
    npm install react-native-safe-area-context
    npm install react-native-gesture-handler
    npm install react-native-screens
    npm install watcher

    npm i @react-native-async-storage/async-storage
*/

const Stack = createNativeStackNavigator();

export default function App() {

  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="login">
        <Stack.Screen name="login" component={Login} options={{title: "로그인"}} />
        <Stack.Screen name="account" component={Account} options={{title: "회원가입"}} />
        <Stack.Screen name="bbs" component={Bbs} options={{title: "게시판"}} />
      </Stack.Navigator>
    </NavigationContainer>
  )
}

title은 해당 메뉴로 이동했을 때 화면 맨 위에 표시해 줄 제목같은 것이다.



3. 게시글을 보여줄 페이지


import React, { useState } from "react";
import { Button, StyleSheet, Text, View } from "react-native";
import BbsDetail from "./BbsDetail";
import Bbslist from "./Bbslist";
import BbsWrite from "./BbsWrite";

const Bbs = (props:any) => {

    const [bbslist, setBbslist] = useState("bbslist");
    const [bbs, setBbs] = useState([]);        // json 형태로 받기 때문에 배열 괄호로 선언해둔다.

    let child:any       // 장면 전환을 위한 변수

    if (bbslist === "bbslist") {
        // child = (<Text>Bbs List View</Text>)
        child = (<Bbslist setBbslist={setBbslist} setBbs={setBbs} />)
    } else if (bbslist === "bbswrite") {
        // child = (<Text>Bbs List Write</Text>)
        child = (<BbsWrite setBbslist={setBbslist} />)
    } else if (bbslist === "bbsdetail") {
        // child = (<Text>Bbs List Detail {JSON.stringify(bbs)}</Text>)
        child = (<BbsDetail bbs={bbs} />)
    }

child는 장면의 전환을 위해 선언한 변수로 각 컴포넌트의 값으로 저정해둔다. 이 때 setter의 값을 그대로 전달하는데, 해당 컴포넌트에서 전달 값을 받기 위해 props라는 매개변수가 사용될 것이다.


    return (
        <View>
            <View style={styles.menu}>
                <View style={styles.button}>
                    <Button title="글목록" onPress={(bbslist) => setBbslist("bbslist")} />
                </View>
                <View style={styles.button}>
                    <Button title="글추가" onPress={(bbslist) => setBbslist("bbswrite")} />
                </View>
            </View>

            <View>{child}</View>
        </View>
    )
}

const styles = StyleSheet.create({
    menu: {
        flexDirection: "row",
        flexWrap: "wrap",
    },
    button: {
        flex: 1,
        height: 50,
        margin: 1
    },
    childView: {
        height: 760
    }
})

export default Bbs;

페이지 상단에는 두개의 버튼이 존재하는데, 이 버튼을 클릭하면 setBbslist를 각 페이지의 이름으로 설정해준다.



4. 게시글 목록 가져오기


DB로 부터 게시글 목록을 가져오기 위해 서버로부터 리스트 형식으로 가져와야한다.

axios를 사용하여 getBbsList에 값을 보내주어 반환 받는다.


import axios from "axios";
import React, { useEffect, useState } from "react";
import { FlatList, Image, StyleSheet, Text, View, TouchableOpacity } from "react-native";
// import { TouchableOpacity } from "react-native-gesture-handler";

/*
    일반적으로 화면단을 설정할 때 컨텐츠가 화면을 벗어나면 스크롤뷰를 설정해야 화면을 내려서 볼 수 있지만,
    플랫 리스트를 사용하면 스크롤뷰가 내장되어 있기 때문에 별도로 설정하지 않아도 된다.
*/

// test data
// const data = [
//     {
//         "userId": "admin",
//         "title": "테스트 데이터 제목입니다.",
//         "readCount": 10
//     },
//     {
//         "userId": "guest",
//         "title": "게스트입니다.",
//         "readCount": 1
//     },
//     {
//         "userId": "hJunPark",
//         "title": "박군입니다.",
//         "readCount": 100
//     }
// ]

function Item ({id, title, readCount, seq, props}:any) {

    function itemClick(seq:number) {
        console.log('itemClick');
        console.log(seq);

        axios.get("http://192.168.35.149:3000/getBbsDetail", {params: {seq: seq}}).then(function(resp) {
            console.log(resp.data);

            // 페이지 이동 시 가져갈 정보
            props.setBbs(resp.data);
        }).catch(function(err) {
            console.log(err);
        })

        // detail 페이지로 이동
        props.setBbslist("bbsdetail")
    }

이 때 props를 통해 다른 컴포넌트에서 값을 전달하게 해준다. 하단의 Bbslist 컴포넌트에 전달해준다.


    return (
        <View style={styles.item}>
            <TouchableOpacity onPress={() => itemClick(seq)}>
                <View style={styles.idRow}>
                    <Image style={styles.image} 
                           source={require("../../assets/dog.png")}
                    />
                    <Text style={styles.title}>{title}</Text>
                </View>
                <View style={styles.idRow}>
                    <Text style={styles.userId}>{id}</Text>
                    <Text style={styles.readCount}>{readCount}</Text>
                </View>
            </TouchableOpacity>
        </View>
    )
}

TouchableOpacity는 HTML의 <a>태그 같은 것이다.


const Bbslist = (props:any) => {

    function renderItem({item}:any) {

        // 제목이 긴 경우 줄여서 표현해 줄 함수
        function strLength(str:String) {
            if(str.length > 18) {
                return str.substring(0, 17) + "...";
            } else {
                return str;
            }
        }

        return (
            <Item
                id={`작성자: ${item.userId}`} 
                title={strLength(item.title)} 
                readCount={`조회수: ${item.readCount}`} 
                seq={item.seq}
                props={props}
            />
        )
    }

플랫 리스트를 사용하기 위해 renderItem 함수를 만들어주었다.


    const [data, setData] = useState([]);

    useEffect(() => {

        axios.get("http://192.168.35.149:3000/getBbsList", {})
        .then(function(resp) {
            console.log(resp);
            setData(resp.data);
        })
        .catch(function(err) {
            console.log(err);
        });
    }, [])

    return (
        <View style={styles.scrollView}>
            <FlatList data={data} renderItem={renderItem} />
        </View>
    )    
}

const styles = StyleSheet.create({

    item: {
         backgroundColor: "#d7d9f4",
         padding: 10,
         marginVertical: 2,
         marginHorizontal: 8,
         borderColor: "#ff0000",
         borderRadius: 15,
         borderStyle: "solid",
         borderWidth: 2,
    },
    image: {
        width: 50,
        height: 50,
        marginTop: 15
    },
    title: {
        fontFamily: "roboto-regular",
        paddingLeft: 20,
        color: "#400040",
        height: 60,
        width: 500,
        fontSize: 26,
    },
    userId: {
        fontFamily: "roboto-regular",
        //backgroundColor: "#ffff00",
        color: "#121212",
        height: 30,
        width: 166,
        fontSize: 20,
        marginLeft: 80,
        marginTop: 8
    },
    readCount: {
        fontFamily: "roboto-regular",
        //backgroundColor: "#00ff00",
        color: "#121212",
        height: 30,
        width: 211,
        fontSize: 20,
        marginLeft: 51,
        marginTop: 8,
        textAlign: "center",
    },
    idRow: {
        height: 40,
        flexDirection: "row",
        marginLeft: 5,
    },
    scrollView: {
        height: 760
    }
});

export default Bbslist;


5. 글을 추가하기 위한 컴포넌트


import AsyncStorage from "@react-native-async-storage/async-storage";
import axios from "axios";
import React, { useState } from "react";
import { Alert, StyleSheet, Text, View } from "react-native";
import { Button, TextInput } from "react-native-paper";

export default function BbsWrite(props:any) {

    const [id, setId] = useState("");
    const [title, setTitle] = useState("");
    const [content, setContent] = useState("");

    const loginData = async () => {
        try {
            let user = await AsyncStorage.getItem("login");

            if (user !== null) {
                setId((JSON.parse(user)).id);
            }
        } catch (err) {
            console.log(err);
        }
    }

    loginData();

AsyncStoragesetIte\을 통해 로그인 화면에서 로그인한 사용자의 정보를 저장해주었고, 여기에서 그 값을 조회하여 사용하기 위해 getItem으로 불러왔다. 이 객체가 null이 아닐 때 idsetter를 사용해서 불러온 값을 JSON으로 파싱하고 아이디 값으로 바꿔준다.


값이 제대로 넘어오지 않을 수 있기 때문에 try catch를 사용했다.


    const bbsWriteBtn = () => {
        console.log(id);
        console.log(title);
        console.log(content);

        if (title.trim() === "") {
            Alert.alert("제목 입력 확인", "제목이 입력되지 않았습니다!");
        } else if (content.trim() === "") {
            Alert.alert("내용 입력 확인", "내용이 입력되지 않았습니다!")
        } else {
            axios.get("http://192.168.35.149:3000/insertBbs", 
            { params: {
                id: id,
                title: title,
                content: content
            }}
            ).then(function(resp){
                console.log(resp.data);

                if (resp.data === "ok") {
                    Alert.alert("등록 완료", "글이 등록되었습니다!");
                    // Bbs.tsx에서 bbslist가 bbswrite일 때 setBbslist를 이 컴포넌트 함수의 매개변수인 prop로 접근해서 글 등록이 성공하면 bbslist로 돌아감.
                    props.setBbslist("bbslist");
                } else {
                    Alert.alert("글이 등록되지 않았습니다.", "글이 정상적으로 등록되지 않았습니다. 확인해주세요.");
                }

글의 제목, 내용, 작성자 정보를 서버로 전달해서 ok 문자열을 받으면 글이 등록된 것이므로 등록되었다는 알림을 띄워준다. 그리고 Bbslist.tsx에서 bbslist 즉 보여줄 메뉴의 값이 bbswrite일 때 setBbslist를 이 컴포넌트 함수의 매개변수인 prop로 접근해서 글 등록을 성공적으로 수행하였다면 bbslist로 돌아간다.


            }).catch(function(err) {
                console.log(err);
                Alert.alert("에러 발생", "글을 등록하는 도중에 에러가 발생했습니다!");
            })
        }


    }

    return (
        <View>
            <Text style={styles.text}>새 글 등록하기</Text>
            <View style={{alignItems: "center"}}>
                <TextInput
                    style={styles.textInput}
                    mode="outlined"
                    label="작성자"
                    value={id}
                    editable={false} 
                />

                <TextInput
                    style={styles.textInput}
                    mode="outlined"
                    label="제목"
                    value={title}
                    onChangeText={(title) => setTitle(title)}
                />

                <TextInput
                    style={styles.textArea}
                    placeholder="내용"
                    multiline={true}
                    numberOfLines={20}
                    value={content}
                    onChangeText={(content) => setContent(content)}
                />

                <Button
                    mode="outlined"
                    style={styles.btn}
                    onPress={bbsWriteBtn}
                >
                    작성 완료
                </Button>
            </View>
        </View>
    )
}

const styles = StyleSheet.create({
    text: {
        marginTop: 10,
        fontSize: 30,
        textAlign: "center"
    },
    textInput: {
        marginTop: 20,
        fontSize: 16,
        width: 500,
        height: 40,
        backgroundColor: "#e3e3e3"
    },
    textArea: {
        fontSize: 16,
        borderWidth: 1,
        marginTop: 20,
        backgroundColor: "#e3e3e3",
        textAlignVertical: "top",
        width: 500
    },
    btn: {
        marginTop: 20,
        marginVertical: 8
    }
})


6. 상세 글보기 페이지


import React from "react";
import { SafeAreaView, StyleSheet, ScrollView, Text } from "react-native";
import { Avatar, Card, Headline, Paragraph } from "react-native-paper";

function BbsDetail(props:any) {

    let userName = props.bbs.userId.substring(0, 2);

    return (
        <SafeAreaView>
            <ScrollView 
                contentContainerStyle={styles.contentContainer}
            >
                <Headline>글 내용</Headline>
                <Card style={styles.card}>
                    <Card.Title 
                        title={props.bbs.title} 
                        subtitle={`작성자: ${props.bbs.userId}`}
                        left={props => 
                            <Avatar.Text{...props} 
                                label={userName}
                            /> 
                        } 
                    />
                    <Card.Content>
                        <Paragraph style={styles.content}>
                            {props.bbs.content}
                        </Paragraph>

                        <Text style={{textAlign: "right"}}>
                            조회수: {props.bbs.readCount}
                        </Text>

                        <Text style={{textAlign: "right"}}>
                            작성일: {props.bbs.wdate}
                        </Text>
                    </Card.Content>
                </Card>
            </ScrollView>
        </SafeAreaView>
    );
}

Card를 통해서 글을 나열해준다.

Bbs 컴포넌트에서 <BbsDetail />을 불러와 사용할 때, 속성으로 전달해줄 값이 필요한데, useState로 선언된 bbs를 전달해준다.

bbsJSON 형태이기 때문에 직접 key값으로 접근하여 가져올 수 있다.


const styles = StyleSheet.create({
    contentContainer: {
        padding: 16,
    },
    card: {
        marginTop: 20,
    },
    content: {
        fontSize: 20
    }
})

export default BbsDetail;