ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 음식점 추천 챗봇 만들기1 - Frontend 구조
    졸업작품 2021. 8. 23. 12:49

    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 는 React Native (EXPO)

    Backend 는 Flask를 사용한다


    이번 게시물에서는 Frontend 코드를 정리해보겠다. 구조는 다음과 같다

    App.js

    (로그인하지 않은 경우)

    └AuthScreen

    └SignUpScreen

    └SignInScreen

    (로그인한 경우)

    └MainScreen

    └ChatScreen

    RN에서 화면간 이동을 하려면 React Navigation을 사용해야한다. 

    하지만 우리는 화면간 이동뿐만 아니라 로그인해야만 화면을 넘어갈수 있도록 구현해주어야한다.

    매번 로그인하는 것은 번거로우므로 userToken이라는 것을 사용한다.

    userToken은 서버에서 발행해주는 로그인인증이다. userToken 을 받아 로컬에 저장해놓으면 매번 로그인할 필요 없이 세션이 만료될때만 로그인해주면 된다. (이 프로젝트에서는 세션만료를 구현하지 않아서 로그인하면 쭉 사용할 수 있다.)

     

    UserToken(로그인 정보)가 없으면 AuthScreen..을 보여주어 로그인하게 하고

    UserToken이 있으면 MainScreen을 통해 ChatScreen 을 사용할 수 있게한다.

     

    이를 위해서 React Navigation 공식 문서에서는 Auth-flow 에 대한 예제를 제공한다. 

    https://reactnavigation.org/docs/auth-flow/

     

    https://reactnavigation.org/docs/auth-flow/

     

    reactnavigation.org

     

     

    코드설명

    bootstrapAsync

    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();
      }, []);

    bootstrapAsync : 앱이 실행될때 SecureStore에 userToken이 있는지 확인하고 있으면 받아온다.

     

    useReducer

    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,
              };
          }
        },
        { //default
          isLoading: true,
          isSignout: false,
          userToken: null,
        }
      );

    useReducer를 사용해 action에 따른 state 변화를 구현한다.

    예를 들어 bootstrapAsync 에서 dispatch({ type: "RESTORE_TOKEN", token: userToken })를 했으므로

    state는

    {...prevState,
       userToken: action.token,
       isLoading: false,
    } 로 변경된다.

     

    authContext

    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("입력 양식을 확인해주세요");
            }
          },
        }),
        []
      );

    AuthContext : signIn, signOut, signUp 했을 때 실행할 함수들을 담고 있다. 이는 AuthContext.Provider를 통해 아래 컴포넌트들에게 전해진다. 해당 컴포넌트들에서 각 함수를 사용하게 되면 아래 내용이 실행된다.

    signIn : 서버에 보낼 정보를 signin_info 에 담는다. userId와 password가 모두 채워진 경우에만 fetch를 통해 서버에 정보를 전달한다. 서버에서 user정보를 확인하고 userToken을 보내주면 Secure Store(로컬저장소)에 저장하고 SIGN_IN action을 dispatch하여 state를 바꿔준다.

    signOut : Secure Store에서 userToken 을 삭제하고 SIGN_OUT action을 dispatch한다.

    signUp : 서버에 보낼 정보를 signup_info에 담는다. userId, password, username, repassword가 모두 채워졌고 password와 repassword가 같은 경우 signup_info를 서버에 보내고 userToken을 받는다. 회원가입을 하면 새로 로그인할필요 없이 자동 로그인이 되도록 하기 위해 SIGN_IN action을 dispatch 한다.

     

    렌더링

    const Stack = createStackNavigator();
    export const AuthContext = React.createContext();
    
    export default function App({ navigation }) {
      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>
      );
    }

    state.userToken == null ? ():() 

    : 삼항연산자를 사용하여 userToken 이 없는경우 AuthScreen, SignInScreen, SignUpScreen 에 접근할 수 있고

    있는 경우 MainScreen, ChatScreen에 접근할 수 있다.

     

    AuthContext.Provider : value 값을 아래 컴포넌트들에게 전달한다. 아래 컴포넌트에서 signUp, signOut, signIn함수를 사용하여 state를 바 수 있게 된다.

     

    initialParams : Navigation을 사용할때는 파라미터를 마음대로 전달할수 없고 initialParams을 통해 보내야한다.

     

    전체코드

    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({ navigation }) {
      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.