Auth(로그인/회원가입) 구현 - React Native

Junghyun Park·2021년 8월 9일
0

https://www.obytes.com/blog/authentication-in-react-native-easy-secure-and-reusable-solution

1. Create The Authentification Context

  • loading, signOut, signIn - 3가지 status
  • redux 등 상태 관리할 수 있는 다른 라이브러리 사용해도 되지만 context API이 간단하고 충분
/// Auth.tsx
import * as React from 'react'
import { getToken, setToken, removeToken } from './utils.tsx'

interface AuthState {
  userToken: string | undefined | null
  status: 'idle' | 'signOut' | 'signIn'
}
type AuthAction = { type: 'SIGN_IN'; token: string } | { type: 'SIGN_OUT' }

type AuthPayload = string

interface AuthContextActions {
  signIn: (data: AuthPayload) => void
  signOut: () => void
}

interface AuthContextType extends AuthState, AuthContextActions {}

const AuthContext = React.createContext<AuthContextType>({
  status: 'idle',
  userToken: null,
  signIn: () => {},
  signOut: () => {},
})

export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
  const [state, dispatch] = React.useReducer(AuthReducer, {
    status: 'idle',
    userToken: null,
  })

  React.useEffect(() => {
    const initState = async () => {
      try {
        const userToken = await getToken()
        if (userToken !== null) {
          dispatch({ type: 'SIGN_IN', token: userToken })
        } else {
          dispatch({ type: 'SIGN_OUT' })
        }
      } catch (e) {
        // catch error here
        // Maybe sign_out user!
      }
    }

    initState()
  }, [])

  const authActions: AuthContextActions = React.useMemo(
    () => ({
      signIn: async (token: string) => {
        dispatch({ type: 'SIGN_IN', token })
        await setToken(token)
      },
      signOut: async () => {
        await removeToken() // TODO: use Vars
        dispatch({ type: 'SIGN_OUT' })
      },
    }),
    []
  )

  return (
    <AuthContext.Provider value={{ ...state, ...authActions }}>
      {children}
    </AuthContext.Provider>
  )
}

const AuthReducer = (prevState: AuthState, action: AuthAction): AuthState => {
  switch (action.type) {
    case 'SIGN_IN':
      return {
        ...prevState,
        status: 'signIn',
        userToken: action.token,
      }
    case 'SIGN_OUT':
      return {
        ...prevState,
        status: 'signOut',
        userToken: null,
      }
  }
}

export const useAuth = (): AuthContextType => {
  const context = React.useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be inside an AuthProvider with a value')
  }
  /*
    you can add more drived state here
    const isLoggedIn  = context.status ==== 'signIn'
    return ({ ...context, isloggedIn})
  */
  return context
}

2. Store Tokens:The secure way

  • 보통 localStorage에 token을 저장하지만, 이것은 매우 보안상 좋지 않음
  • KeyChain과 같은 방법을 사용함이 바람직/ react-native-sensitive-data package 이용
    yarn add react-native-sensitive-info@next
//utils.tsx
import SInfo from 'react-native-sensitive-info';

const TOKEN = 'token';
const SHARED_PERFS = 'ObytesSharedPerfs';
const KEYCHAIN_SERVICE = 'ObytesKeychain';
const keyChainOptions = {
  sharedPreferencesName: SHARED_PERFS,
  keychainService: KEYCHAIN_SERVICE,
};

export async function getItem<T>(key: string): Promise<T | null> {
  const value = await SInfo.getItem(key, keyChainOptions);
  return value ? JSON.parse(value)?.[key] || null : null;
}

export async function setItem<T>(key: string, value: T): Promise<void> {
  SInfo.setItem(key, JSON.stringify({[key]: value}), keyChainOptions);
}
export async function removeItem(key: string): Promise<void> {
  SInfo.deleteItem(key, keyChainOptions);
}

export const getToken = () => getItem<string>(TOKEN);
export const removeToken = () => removeItem(TOKEN);
export const setToken = (value: string) => setItem<string>(TOKEN, value);

3. Access Auth Actions outside component tree

// Auth.tsx

// In case you want to use Auth functions outside React tree
export const AuthRef = React.createRef<AuthContextActions>();


export const AuthProvider = ({children}: {children: React.ReactNode}) => {

   ....
  // we add all Auth Action to ref
  React.useImperativeHandle(AuthRef, () => authActions);

  const authActions: AuthContextActions = React.useMemo(
    () => ({
      signIn: async (token: string) => {
        dispatch({type: 'SIGN_IN', token});
        await setToken(token);
      },
      signOut: async () => {
        await removeToken(); // TODO: use Vars
        dispatch({type: 'SIGN_OUT'});
      },
    }),
    [],
  );

  return (
    <AuthContext.Provider value={{...state, ...authActions}}>
      {children}
    </AuthContext.Provider>
  );
};

/*
you can eaisly import  AuthRef and start using Auth actions
AuthRef.current.signOut()
*/

4. 원하는 곳에서 Auth hook을 사용

// App.tsx
import * as React from 'react'
import { Text, View, StyleSheet, Button } from 'react-native'
import { AuthProvider, useAuth, AuthRef } from './Auth'

// you can access to Auth action directly from AuthRef
// AuthRef.current.signOut()

const LogOutButton = () => {
  const { signOut } = useAuth()
  return <Button title="log Out" onPress={signOut} />
}

const LogInButton = () => {
  const { signIn } = useAuth()
  return <Button title="log IN" onPress={() => signIn('my_token')} />
}
const Main = () => {
  const { status, userToken } = useAuth()

  return (
    <View style={styles.container}>
      <Text style={styles.text}>status : {status}</Text>
      <Text style={styles.text}>
        userToken : {userToken ? userToken : 'null'}
      </Text>
      <View style={styles.actions}>
        <LogInButton />
        <LogOutButton />
      </View>
    </View>
  )
}

export default function App() {
  return (
    <AuthProvider>
      <Main />
    </AuthProvider>
  )
}

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

1. 화면구성 tip

  • Auth state 변수를 활용하여, 2 가지 그룹으로 ternery operator 구성
isSignedIn ? (
  <>
    <Stack.Screen name="Home" component={HomeScreen} />
    <Stack.Screen name="Profile" component={ProfileScreen} />
    <Stack.Screen name="Settings" component={SettingsScreen} />
  </>
) : (
  <>
    <Stack.Screen name="SignIn" component={SignInScreen} />
    <Stack.Screen name="SignUp" component={SignUpScreen} />
  </>
)

2. 조건부 랜더링 화면 시, menunal하게 navigate 하지말 것

  • Auth state 에 따라 자동으로 선택되도록하고, navigation.navigate('Home')식으로 이동하지 말것

3. 스크린들을 구성하기

  • Splash Screen : 토큰을 받는 동안 보여줄 화면
  • SignIn Screen : 로그인 화면
  • Home Screen : 로긴하면 보여줄 화면
state.userToken == null ? (
  <>
    <Stack.Screen name="SignIn" component={SignInScreen} />
    <Stack.Screen name="SignUp" component={SignUpScreen} />
    <Stack.Screen name="ResetPassword" component={ResetPassword} />
  </>
) : (
  <>
    <Stack.Screen name="Home" component={HomeScreen} />
    <Stack.Screen name="Profile" component={ProfileScreen} />
  </>
);

4. 토큰 가져오는 로직 구현

  • 3가지 상태 변수가 필요 (isLoading, isSignOut, userToken)
  • 2 가지 필요(1. token을 가져오고, signIn, signOut 로직, 2. 다른 컴포넌트들에게 접근 가능하도록 구현)
    =>React.useReducer, React.useContext 활용 가능
import * as React from 'react';

const AuthContext = React.createContext();
import * as React from 'react';
import * as SecureStore from 'expo-secure-store';

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(() => {
    // Fetch the token from storage then navigate to our appropriate place
    const bootstrapAsync = async () => {
      let userToken;

      try {
        userToken = await SecureStore.getItemAsync('userToken');
      } catch (e) {
        // Restoring token failed
      }

      // After restoring token, we may need to validate it in production apps

      // This will switch to the App screen or Auth screen and this loading
      // screen will be unmounted and thrown away.
      dispatch({ type: 'RESTORE_TOKEN', token: userToken });
    };

    bootstrapAsync();
  }, []);

  const authContext = React.useMemo(
    () => ({
      signIn: async data => {
        // In a production app, we need to send some data (usually username, password) to server and get a token
        // We will also need to handle errors if sign in failed
        // After getting token, we need to persist the token using `SecureStore`
        // In the example, we'll use a dummy token

        dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
      },
      signOut: () => dispatch({ type: 'SIGN_OUT' }),
      signUp: async data => {
        // In a production app, we need to send user data to server and get a token
        // We will also need to handle errors if sign up failed
        // After getting token, we need to persist the token using `SecureStore`
        // In the example, we'll use a dummy token

        dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
      },
    }),
    []
  );

  return (
    <AuthContext.Provider value={authContext}>
      <Stack.Navigator>
        {state.userToken == null ? (
          <Stack.Screen name="SignIn" component={SignInScreen} />
        ) : (
          <Stack.Screen name="Home" component={HomeScreen} />
        )}
      </Stack.Navigator>
    </AuthContext.Provider>
  );
}
function SignInScreen() {
  const [username, setUsername] = React.useState('');
  const [password, setPassword] = React.useState('');

  const { signIn } = React.useContext(AuthContext);

  return (
    <View>
      <TextInput
        placeholder="Username"
        value={username}
        onChangeText={setUsername}
      />
      <TextInput
        placeholder="Password"
        value={password}
        onChangeText={setPassword}
        secureTextEntry
      />
      <Button title="Sign in" onPress={() => signIn({ username, password })} />
    </View>
  );
}
profile
21c Carpenter

0개의 댓글