KnockKnock: 회원가입 로직

MODAC·2023년 7월 30일
0

knockknock

목록 보기
6/9

Before

맨 처음 작성한 signUp은 header의 progressbar UI와 불필요한 컴포넌트의 재사용 방지를 위해 StackNavigation으로 signUp 스크린에 진입한 후 약관 동의, 이메일 입력, 비밀번호 입력, 성공 총 네개의 컴포넌트를 번갈아가며 랜더링하는 방식으로 작성했다.

import React, {useState} from 'react';
import {SafeAreaView, View, StyleSheet} from 'react-native';
import ProgressBar from '../../components/ProgressBar';
import Agree from './screen/Agree';
import Email from './screen/Email';
import Password from './screen/Password';
import Success from './screen/Success';
import Config from 'react-native-config';
import {SignData} from './screen/Password';

interface SignUpProps {
  locate: string;
  deps: {[key: string]: any}[];
  onLogin: (loginState: boolean) => void;
}

const SignUp: React.FC<SignUpProps> = ({locate, deps, onLogin}) => {
  const [signData, setSignData] = useState<SignData>({email: '', password: ''});
  const url = Config.API_APP_KEY;
  return (
    <SafeAreaView style={signUpStyles.container}>
      <ProgressBar deps={deps} />
      <View style={signUpStyles.contents}>
        {locate === 'Agree' && <Agree />}
        {locate === 'Password' && <Password />}
        {locate === 'Email' && <Email url={url} />}
        {locate === 'Success' && <Success onLogin={onLogin} />}
      </View>
    </SafeAreaView>
  );
};

export const signUpStyles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    width: '100%',
    height: '100%',
  },
  contents: {
    paddingLeft: 24,
    paddingRight: 24,
    paddingTop: 20,
    paddingBottom: 20,
  },
});

export default SignUp;
// ... 중략

const Stack = createStackNavigator();

const StackSign: React.FC<onLoginProps> = ({onLogin}) => {
  const [deps, setDeps] = useState([
    {Agree: true},
    {Password: false},
    {Email: false},
    {Success: false},
  ]);

  const next = () => {
    const nextDepIndex = deps.findIndex(dep => Object.values(dep)[0] === false);
    if (nextDepIndex !== -1) {
      const updatedDeps = [...deps];
      updatedDeps[nextDepIndex] = {
        ...updatedDeps[nextDepIndex],
        [Object.keys(deps[nextDepIndex])[0]]: true,
      };
      setDeps(updatedDeps);
    }
  };

  const goBack = (navigation: any) => {
    // signup 컴포넌트 제어
    const lastTrueIndex = deps
      .map((dep, index) => ({...dep, index}))
      .filter(dep => Object.values(dep)[0] === true);

    if (lastTrueIndex.length > 1) {
      const updatedDeps = [...deps];
      const lastTrueIndexValue = lastTrueIndex[lastTrueIndex.length - 1].index;
      const key = Object.keys(deps[lastTrueIndexValue])[0];

      updatedDeps[lastTrueIndexValue] = {
        ...updatedDeps[lastTrueIndexValue],
        [key]: false,
      };
      setDeps(updatedDeps);
    } else (navigation.navigate as any)('Login');
  };

  const [locate, setLocate] = useState<string>('Agree');

  useEffect(() => {
    const trueDeps = deps.filter(dep => Object.values(dep)[0] === true);
    const nextDeps = trueDeps[trueDeps.length - 1] || 1;
    const taskKeys = Object.keys(nextDeps);
    const lastTaskKey = taskKeys[taskKeys.length - 1];
    setLocate(lastTaskKey);
  }, [deps]);

  return (
    <Stack.Navigator initialRouteName="Login">
      <Stack.Screen name="Login" options={{headerShown: false}}>
        {props => <Login {...props} onLogin={onLogin} />}
      </Stack.Screen>
      <Stack.Screen
        name="SignUp"
        options={({navigation}) => ({
          title: '회원가입',
          headerBackTitleVisible: false,
          headerBackImage: () => <BackBtn goBack={() => goBack(navigation)} />,
          headerRight: () =>
            locate !== 'Success' ? (
              <TouchableOpacity onPress={next}>
                <Image
                  source={require('front/assets/image/check_btn.png')}
                  style={{marginRight: 24, width: 24, height: 24}}
                />
              </TouchableOpacity>
            ) : null,
          headerStyle: styles.headerStyle,
          headerTitleAlign: 'left',
        })}>
        {props => <SignUp {...props} deps={deps} locate={locate} onLogin={onLogin} />}
      </Stack.Screen>
    </Stack.Navigator>
  );
};

const stackSignUp = () => {
  return (
    <Stack.Navigator>
      <Stack.Screen name="SignAgree" component={SignAgree} options={{headerShown: false}} />
      <Stack.Screen name="SignAgree" component={SignAgree} options={{headerShown: false}} />
      <Stack.Screen name="SignAgree" component={SignAgree} options={{headerShown: false}} />
      <Stack.Screen name="SignAgree" component={SignAgree} options={{headerShown: false}} />
    </Stack.Navigator>
  );
};

// ... 중략

그러나 백앤드에 이메일을 인증받는 과정에서 이메일 주소와 비밀번호로 이루어진 테이블을 body로 전달하여 해당 이메일로 인증번호를 받고 기존 body에 인증번호를 추가하여 최종적으로 회원가입 요청을 보내는 구조로 api가 설계되었다.

After

이러한 백앤드의 테이블 설계에 맞도록 파일 구조와 회원가입 순서를 리펙토링했다. 먼저 약관 동의 이후 이메일 입력, 비밀번호 입력 순서를 변경해서 비밀전호가 먼저 입력된 후 저장된 비밀번호 state를 이메일과 함께 api에 전달하도록 수정했다. 또한 입력된 데이터를 signUp 컴포넌트에서 useState로 관리하면 데이터 입력 후 다음 컴포넌트로 랜더링 될 떄 입력된 데이터와 컴포넌트가 함께 언마운트되는 문제가 있어서 signUp의 랜더링 구조를 material-top-tabs을 활용하여 tab navigation으로 수정하고 navigation.params를 활용해 입력되는 state를 저장하려고 했다.

// signUp의 스크린을 감싸는 material-top-tabs 구조

import React, {useState, useEffect} from 'react';
import {createMaterialTopTabNavigator} from '@react-navigation/material-top-tabs';
import {onLoginProps} from '../../navigations/StackNavigator';
import SignAgree from './SignAgree';
import SignEmail from './SignEmail';
import SignPassword from './SignPassword';
import SignSuccess from './SignSuccess';
import Header from '../../components/Header';
import {SafeAreaView} from 'react-native';
import {View, StyleSheet} from 'react-native';
import * as Progress from 'react-native-progress';
import Config from 'react-native-config';

const Tab = createMaterialTopTabNavigator();

const SignUpTab: React.FC<onLoginProps> = ({onLogin}) => {
  const [deps, setDeps] = useState(0.25);
  const url = Config.API_APP_KEY;

  return (
    <SafeAreaView style={{flex: 1}}>
      <Header title="회원가입" type="signUp" />
      <View style={styles.progressBar}>
        <Progress.Bar
          progress={deps}
          width={null}
          height={2}
          color={'#1b1b1b'}
          unfilledColor={'#eee'}
          borderColor={'#fff'}
        />
      </View>
      <Tab.Navigator tabBar={props => <></>}>
        <Tab.Screen name="SignAgree">
          {props => <SignAgree {...props} setDeps={setDeps} />}
        </Tab.Screen>
        <Tab.Screen name="SignPassword" initialParams={{password: ''}}>
          {props => <SignPassword {...props} setDeps={setDeps} url={url} />}
        </Tab.Screen>
        <Tab.Screen name="SignEmail">
          {props => <SignEmail {...props} setDeps={setDeps} url={url} />}
        </Tab.Screen>
        <Tab.Screen name="SignSuccess">
          {props => <SignSuccess {...props} onLogin={onLogin} />}
        </Tab.Screen>
      </Tab.Navigator>
    </SafeAreaView>
  );
};

export default SignUpTab;

const styles = StyleSheet.create({
  progressBar: {
    width: '100%',
    backgroundColor: '#ffffff',
    height: 2,
  },
});

tab으로 전환되는 각 컴포넌트에서는 navigation.setParams를 사용해 { 데이터 명: 값 } 의 형태로 params에 저장한다.

const SignUpComponent: React.FC<SignUpProps> = ({navigation, setDeps, route, url}) => {
  const [passwordInput, setPasswordInput] = React.useState('');
  const [passwordCheck, setPasswordCheck] = React.useState('');
  const {password} = route.params;

  const handleNextPage = () => {
    if (passwordInput === passwordCheck) {
      navigation.jumpTo('SignEmail');
      setDeps(0.75);
      navigation.setParams({password: passwordCheck});
    } else Alert.alert('정보가 일치하지 않습니다', '다시 확인해 주세요');
  };

  return ...
};

우선 레이아웃과 로직은 이상없이 동작하며 뒤로가기 동작 시 route의 위치가 tab navigation이 기준이 되지 않고 더 상위의 signUpTab이 위치한 stack navigation 기준으로 확인된다. 현재 header는 tab컴포넌트와 동등한 위치의 컴포넌트인데 여기서 tab의 뒤로가기는 어떻게 처리해야 할지 고민 중이다.

0개의 댓글