ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 음식점 추천 챗봇 만들기3 - frontend + Backend 로그인 (Flask)
    졸업작품 2021. 8. 24. 21:30

    Notion 으로 이동했습니다

    https://hill-bovid-56d.notion.site/6f84f2d1d53342f5b425351f710a754b?v=f290bbaba9ed460ab7bfbeaac0bf9eb4

     

    졸업작품 - 음식점추천 챗봇앱

    2021.06.25 ~ Github : https://github.com/su1715/food-select-chatbot React Native, Dialogflow, Flask, AWS EC2

    hill-bovid-56d.notion.site


    지난 글에서는 회원가입, 로그인하는데 필요한 frontend 페이지를 만들었다

     

    음식점 추천 챗봇 만들기2 - Frontend 회원가입, 로그인

    이전 글에서 작성한 바와 같이 userToken이 없는 경우 AuthScreen, SignInScreen, SignUpScreen 에만 접근할 수 있다. 2021.08.23 - [졸업작품] - 음식점 추천 챗봇 만들기1 - Frontend 구조 음식점 추천 챗봇 만..

    my-chair.tistory.com

     

    이번 글에서는 로그인페이지에서 정보를 받아서 backend에서 처리하고

    userToken을 보내주는 기능을 구현하려고한다.

     

    서버는 Flask, DB는 sqlite를 사용하며, 현재는 ngrok을 통해 로컬서버를 돌리고 있다.

    추후 AWS를 이용할 예정이다.

     


    fetch (보내는) 구조 한눈에 보기

    App.js의 authContext에는 signIn 함수가 있다.

    useContext 를 통해 signInScreen에 전해지며, signInScreen에서 로그인버튼을 누르면 signIn 함수가 실행된다.

    이때 파라미터로 userId와 password를 받는다.

    signIn 함수는 받은 데이터(userId, password)를 변수 signIn_info에 담아 서버에 보낸다 (fetch) 

     

    Fetch

    fetch(url + "/signin", signin_info)
                .then((response) => response.json())
                .then((response) => {
                  if (response.result === "success") {
                    SecureStore.setItemAsync("userToken", response.token);
                    dispatch({ type: "SIGN_IN", token: response.token });
                  } else alert(response.error);
                });

    url 은 서버주소, "/signIn" 은 서버에서 내가 정한 singIn 처리루트 이름이다. 각자 마음대로 지정할 수 있다.

    ngrok을 사용해서 서버 주소가 계속 바뀌기 때문에, env.js 파일을 만들어 그곳에서 url을 가져오고 있다.

    (.gitignore에 url을 담은 env.js을 추가했음)

    fetch 함수는 url/signIn 이라는 주소에 signin_info 를 보내고 리턴값을 받아온다.

    리턴값의 형식은 서버에서 자신이 임의로 정하는 것이라 response의 코드를 깊게 이해할 필요가 없고

    flask 코드를 읽어보면 더 이해가 쉬울 것이다.

     

    app.py (flask) - /signin 

    로컬서버를 사용하려면

    1. 콘솔창에서 해당 코드가 있는 폴더에 들어가 python app.py 를 수행해야함

    2. ngrok 을 켜서 ngrok 주소를 frontend 의 url에 넣어주어야함

    @app.route('/signin', methods=['GET', 'POST'])
    def signin():
        conn = sqlite3.connect("foodDic.db")
        cur = conn.cursor()
        data = request.get_json(force=True)
        userId = data['userId']
        password = data['password']
        # db에 같은 정보 있는지 확인
        cnt = cur.execute(
            "SELECT count(*) From User Where userid=? AND password=?", (userId, password,))
        conn.commit()
        conn.close()
        if cnt:
            return jsonify(result="success", token=userId)
        else:
            return jsonify(result="fail", error="계정정보가 일치하지 않습니다.")

    signin_info에서 body에 담아 보낸 부분을 data = request.get_json(force=True) 를 통해 받는다.

    그리고 구축해놓은 db의 User Table에 해당 아이디와 비밀번호를 가진 데이터가 있는지 확인한다. 

    있다면 result값과 token을, 없다면 result값과 error 메세지를 보낸다.

     

    원래 token을 만들어주는 함수(access_token)가 있고 그걸 사용해주었는데

    dialogflow를 통해 webhook에 아이디를 전달해야하는 문제 때문에 token을 userId로 사용하게 되었다.

    나중에 자세하게 글을 작성할 예정이며 일반적인 로그인의 경우 token을 따로 만들어 주는 것이 맞다.

    token에 대해서는 아래 페이지 참조

    https://m.blog.naver.com/shino1025/221954027152

     

    [Flask 입문] JWT 토큰을 이용해서 로그인/인증 기능을 만들어 보자

    이번 포스팅에서는 Flask에서 사용자 인증을 비롯하여 간단한 로그인 API를 만들어 보자. 해당 포스팅...

    blog.naver.com

     

    Fetch 다시보기

    fetch(url + "/signin", signin_info)
                .then((response) => response.json())
                .then((response) => {
                  if (response.result === "success") {
                    SecureStore.setItemAsync("userToken", response.token);
                    dispatch({ type: "SIGN_IN", token: response.token });
                  } else alert(response.error);
                });

    fetch는 /signin 에서 보낸 결과를 Promise 객체로 반환한다.

    더보기

    Promise란 javascript에서 비동기처리를 하기 위한 객체이다. 

    서버에 요청을 보내고 받아오는 데에는 시간이 걸린다. 근데 javascript는 요청을 보내자마자 결과를 받은 것 처럼 다음 코드를 바로 수행하려고한다. 받지도 않은 결과를 이용해 무언가를 하려고 하면 분명히 에러가 날것이다. 이때 Promise 객체라는 것을 사용하는데 Promise 객체에.then((결과값)=>{코드})를 붙이고 그 안에 코드를 적으면 결과값을 받은 후에 작업을 이어서 진행할 수 있다.

    더 자세한 설명은 해당 블로그 참조 https://joshua1988.github.io/web-development/javascript/promise-for-beginners/

    결과는 {result="success", token=userid} 혹은 {result="fail, error="계정정보가 일치하지 않습니다"} 일 것이다.

    result 가 success라면 SecureStore에 "userToken" 이라는 이름으로 token을 저장한다. 또한 "SIGN_IN" 액션을 dispacth 한다.

    result 가 fail 이라면 error 메세지를 alert로 출력한다. 

     

    dispatch

    //App.js
    //가독성을 위해 stack의 option, initailParams 등은 지웠습니다.
    ...
    const [state, dispatch] = React.useReducer(
        (prevState, action) => {
          switch (action.type) {
          	...
            case "SIGN_IN":
              return {
                ...prevState,
                isSignout: false,
                userToken: action.token,
              };
           	...
          }
        },
        {
          isLoading: true,
          isSignout: false,
          userToken: null,
        }
      );
      ...
      return (
        ...
              {state.userToken == null ? (
                <>
                  <Stack.Screen name="Auth" component={AuthScreen}/>
                  <Stack.Screen name="SignIn" component={SignInScreen}/>
                  <Stack.Screen name="SignUp" component={SignUpScreen}/>
                </>
              ) : (
                <>
                  <Stack.Screen name="Main" component={MainScreen}/>
                  <Stack.Screen name="Chat" component={ChatScreen}/>
                </>
              )}
         ...
      );

    서버로부터 토큰을 받아 SIGN_IN 액션이 수행된다면 useReducer를 이용한 state는 {
                ...prevState,
                isSignout: false,
                userToken: action.token,
              }; 가 될것이다.

    userToken이 생겼으므로 App.js 의 리턴값은 MainScreen 과 ChatScreen만 보여줄 것이다.

    MainScreen이 ChatScreen 보다 앞에 작성되었으므로 성공적으로 로그인이 되었다면 MainScreen으로 이동할 것이다.

    정리

    1. SignInScreen에서 로그인버튼을 누른다.

    2. authContext에서 받아와 로그인버튼의 onPress에 넣어두었던 signIn 함수가 실행된다.

    3. signIn 함수는 userId 와 password를 받아 서버의 /signin 루트에 보낸다

    4. 서버에서 db에 해당유저가 있는지 확인하고 토큰을 보내거나 에러메세지를 보낸다.

    5. success를 받으면 저장하고 SIGN_IN 액션 수행, fail을 받으면 에러메세지 출력

    6. SIGN_IN 액션이 수행되면 MainScreen으로 이동한다.

     

    전체코드

    더보기
    //SignInScreen.js
    import React, { useState, useContext } from "react";
    import {
      StyleSheet,
      Text,
      View,
      TouchableOpacity,
      SafeAreaView,
      TextInput,
    } from "react-native";
    import { AuthContext } from "../App";
    
    const SignInScreen = ({ navigation }) => {
      const { signIn } = useContext(AuthContext);
      const [userId, setUserId] = useState("");
      const [password, setPassword] = useState("");
      return (
        <SafeAreaView style={styles.container}>
          <View style={styles.title}>
            <Text style={styles.titleText}>로그인</Text>
          </View>
          <View style={styles.form}>
            <View style={styles.inputWrapper}>
              <Text style={styles.label}>아이디</Text>
              <TextInput
                placeholder="아이디"
                value={userId}
                onChangeText={setUserId}
                style={styles.textInput}
              />
            </View>
            <View style={styles.inputWrapper}>
              <Text style={styles.label}>비밀번호</Text>
              <TextInput
                placeholder="비밀번호"
                value={password}
                onChangeText={setPassword}
                secureTextEntry
                style={styles.textInput}
              />
            </View>
            <View style={styles.buttons}>
              <TouchableOpacity
                style={styles.button}
                onPress={() => signIn({ userId, password })}
              >
                <Text style={styles.buttonText}>로그인</Text>
              </TouchableOpacity>
              <TouchableOpacity
                style={styles.button}
                onPress={() => navigation.navigate("Auth")}
              >
                <Text style={styles.buttonText}>취소</Text>
              </TouchableOpacity>
            </View>
          </View>
        </SafeAreaView>
      );
    };
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        marginLeft: 20,
        marginRight: 20,
      },
      title: {
        width: "100%",
        height: "15%",
        justifyContent: "center",
      },
      titleText: {
        fontSize: 30,
      },
      form: {
        width: "100%",
        alignItems: "center",
        justifyContent: "center",
      },
      inputWrapper: {
        width: "100%",
        paddingBottom: 20,
      },
      label: {
        fontSize: 20,
        paddingBottom: 6,
      },
      textInput: {
        width: "100%",
        height: 35,
        backgroundColor: "#d9d9d9",
        borderRadius: 5,
      },
      buttons: {
        width: "100%",
        height: 45,
        flexDirection: "row",
        justifyContent: "space-around",
        //backgroundColor: "pink",
      },
      button: {
        width: "30%",
        borderRadius: 10,
        borderWidth: 1,
        justifyContent: "center",
        alignItems: "center",
      },
      buttonText: {
        fontSize: 20,
      },
    });
    
    export default SignInScreen;

     

    //App.js
    import * as React from "react";
    import { NavigationContainer } from "@react-navigation/native";
    import { createStackNavigator } from "@react-navigation/stack";
    import * as SecureStore from "expo-secure-store";
    import MainScreen from "./src/MainScreen";
    import ChatScreen from "./src/ChatScreen";
    import AuthScreen from "./src/AuthScreen";
    import SignInScreen from "./src/SignInScreen";
    import SignUpScreen from "./src/SignUpScreen";
    import { url } from "./env";
    
    const Stack = createStackNavigator();
    export const AuthContext = React.createContext();
    
    export default function App() {
      const [state, dispatch] = React.useReducer(
        (prevState, action) => {
          switch (action.type) {
            case "RESTORE_TOKEN":
              return {
                ...prevState,
                userToken: action.token,
                isLoading: false,
              };
            case "SIGN_IN":
              return {
                ...prevState,
                isSignout: false,
                userToken: action.token,
              };
            case "SIGN_OUT":
              return {
                ...prevState,
                isSignout: true,
                userToken: null,
              };
          }
        },
        {
          isLoading: true,
          isSignout: false,
          userToken: null,
        }
      );
    
      React.useEffect(() => {
        const bootstrapAsync = async () => {
          let userToken;
          try {
            userToken = await SecureStore.getItemAsync("userToken");
          } catch (e) {
            // Restoring token failed
          }
          dispatch({ type: "RESTORE_TOKEN", token: userToken });
        };
        bootstrapAsync();
      }, []);
    
      const authContext = React.useMemo(
        () => ({
          signIn: async (data) => {
            const { userId, password } = data;
            const signin_info = {
              method: "POST",
              body: JSON.stringify(data),
              headers: {
                "Content-Type": "application/json",
              },
            };
            if (userId && password) {
              fetch(url + "/signin", signin_info)
                .then((response) => response.json())
                .then((response) => {
                  if (response.result === "success") {
                    SecureStore.setItemAsync("userToken", response.token);
                    dispatch({ type: "SIGN_IN", token: response.token });
                  } else alert(response.error);
                });
            } else {
              alert("입력 양식을 확인해주세요");
            }
          },
          signOut: async () => {
            await SecureStore.deleteItemAsync("userToken");
            dispatch({ type: "SIGN_OUT" });
          },
          signUp: async (data) => {
            const { userId, username, password, repassword } = data;
            const signup_info = {
              method: "POST",
              body: JSON.stringify(data),
              headers: {
                "Content-Type": "application/json",
              },
            };
            let result;
            if (
              userId &&
              username &&
              password &&
              repassword &&
              password === repassword
            ) {
              fetch(url + "/signup", signup_info)
                .then((response) => response.json())
                .then((response) => {
                  if (response.result === "success") {
                    SecureStore.setItemAsync("userToken", response.token);
                    dispatch({ type: "SIGN_IN", token: response.token });
                  } else alert(response.error);
                });
            } else {
              alert("입력 양식을 확인해주세요");
            }
          },
        }),
        []
      );
      return (
        <NavigationContainer>
          <AuthContext.Provider value={authContext}>
            <Stack.Navigator>
              {state.userToken == null ? (
                <>
                  <Stack.Screen
                    name="Auth"
                    component={AuthScreen}
                    options={{
                      headerShown: false,
                    }}
                  />
                  <Stack.Screen
                    name="SignIn"
                    component={SignInScreen}
                    options={{
                      headerShown: false,
                    }}
                  />
                  <Stack.Screen
                    name="SignUp"
                    component={SignUpScreen}
                    options={{
                      headerShown: false,
                    }}
                  />
                </>
              ) : (
                <>
                  <Stack.Screen
                    name="Main"
                    component={MainScreen}
                    options={{
                      headerShown: false,
                    }}
                    initialParams={{ token: state.userToken }}
                  />
                  <Stack.Screen
                    name="Chat"
                    component={ChatScreen}
                    options={{
                      headerShown: false,
                    }}
                    initialParams={{ token: state.userToken }}
                  />
                </>
              )}
            </Stack.Navigator>
          </AuthContext.Provider>
        </NavigationContainer>
      );
    }

    댓글

Designed by Tistory.