인증 화면
이번에는 파이어베이스 인증 기능을 이용해
로그인/회원가입 화면을 만들어 보려 한다.
스택 내비게이션을 활용해 간단하게 로그인, 회원가입 스크린을 만들어주었다.
내비게이션이 정상적으로 작동하는 것을 확인하고,
로그인 화면부터 이메일, 비밀번호를 입력받을 수 있도록 만들어보겠다.
1. Image 컴포넌트
먼저 로고 이미지를 적용하기 위해 이미지 설정을 해주겠다.
파이어베이스 스토리지를 사용하지 않기 때문에 직접 웹 사이트 링크를 넣어주었다.
👇이미지 적용 코드 보기
components/Image.jsx
import PropTypes from 'prop-types';
import React from 'react';
import styled from 'styled-components/native';
const Container = styled.View`
align-self: center;
margin-bottom: 30px;
`;
const StyledImage = styled.Image`
background-color: ${({ theme }) => theme.imageBackground};
width: 100px;
height: 100px;
`;
const Image = ({ url, imageStyle }) => {
return (
<Container>
<StyledImage source={{ uri: url }} style={imageStyle} />
</Container>
);
};
Image.proprTypes = {
url: PropTypes.string,
imageStyle: PropTypes.object,
}
export default Image
utils/images.js
export const images = {
logo: 'https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTW5lR%2FbtsL6au2pze%2FJgZabX2UyB6eXgYteEjAOK%2Fimg.png',
};
screens/Login.jsx
import React from 'react';
import { Button } from 'react-native';
import styled from 'styled-components/native';
import { images } from '../utils/images';
import { Image } from '../components';
const Container = styled.View`
flex: 1;
justify-content: center;
align-items: center;
background-color: ${({theme}) => theme.background};
`;
const Login = ({ navigation }) => {
return (
<Container>
<Image url={images.logo} />
<Button title='Signup' onPress={() => navigation.navigate('Signup')} />
</Container>
)
}
export default Login
로고 이미지 또한 로딩 과정에서 미리 불러오도록 App컴포넌트도 수정했다.
const imageAssets = cacheImages([
require('../assets/splash.png'),
...Object.values(images),
]);
2. Input 컴포넌트
다음으로 아이디와 비밀번호를 입력 받을 input 컴포넌트를 설정해주겠다.
교재에 나와있는 대로 Input 컴포넌트를 구현하던 중
다음과 같은 오류로 코드를 약간 수정해주었다.
⚠️ Warning: TypeError: Component is not a function (it is Object) 에러 발생
styled.TextInput을 직접 사용할 경우, styled-components/native에서 Object로 인식되어 오류가 발생할 수 있다.
이는 styled-components/native가 React Native의 TextInput 을 제대로 감싸지 못할 때 발생한다.
// Warning: TypeError: Component is not a function (it is Object)
// `styled.TextInput` 대신 `styled(TextInput)` 사용
const StyledTextInput = styled(TextInput).attrs(({ theme }) => ({
placeholderTextColor: theme.inputPlaceholder,
}))`
background-color: ${({ theme }) => theme.background};
color: ${({ theme }) => theme.text};
padding: 20px 10px;
font-size: 16px;
border: 1px solid ${({ theme, isFocused }) => (isFocused ? theme.text : theme.inputBorder)};
border-radius: 4px;
`;
이메일 input에서 returnKeyType을 next로 설정하고
useRef를 이용해 이메일 Input 에서 next 버튼을 클릭 시 비밀번호 Input 으로 포커스가 이동되도록 코드를 작성했다.
<Input
onSubmitEditing={() => passwordRef.current.focus()}
returnKeyType="next"
/>
<Input
ref={passwordRef}
/>
이제 Input 컴포넌트에 전달된 ref를 TextInput 컴포넌트에 지정해야한다.
💡
ref는 key처럼 리액트에서 특별히 관리되기 때문에 자식 컴포넌트의 props로 전달되지 않는다.
따라서 forwardRef 함수를 이용해 ref를 전달받을 수 있다.
forwardRef로 TextInput에 ref를 전달해주니 컴포넌트 간 포커스 이동이 잘 이루어지는 것을 확인할 수 있었다.
👇forwardRef 적용 코드 보기
import React, { forwardRef, useState } from 'react';
import { TextInput } from 'react-native'; // TextInput 직접 import
import styled from 'styled-components/native';
import PropTypes from 'prop-types';
const Container = styled.View`
flex-direction: column;
width: 100%;
margin: 10px 0;
`;
const Label = styled.Text`
font-size: 14px;
font-weight: 600;
margin-bottom: 6px;
color: ${({ theme, isFocused }) => (isFocused ? theme.text : theme.label)};
`;
// Warning: TypeError: Component is not a function (it is Object)
// `styled.TextInput` 대신 `styled(TextInput)` 사용
const StyledTextInput = styled(TextInput).attrs(({ theme }) => ({
placeholderTextColor: theme.inputPlaceholder,
}))`
background-color: ${({ theme }) => theme.background};
color: ${({ theme }) => theme.text};
padding: 20px 10px;
font-size: 16px;
border: 1px solid ${({ theme, isFocused }) => (isFocused ? theme.text : theme.inputBorder)};
border-radius: 4px;
`;
const Input = forwardRef(
(
{
label,
value,
onChangeText,
onSubmitEditing,
onBlur,
placeholder,
isPassword,
returnKeyType,
maxLength,
},
ref
) => {
const [isFocused, setIsFocused] = useState(false);
return (
<Container>
<Label isFocused={isFocused}>{label}</Label>
<StyledTextInput
ref={ref}
isFocused={isFocused}
value={value}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
onFocus={() => setIsFocused(true)}
onBlur={() => {
setIsFocused(false);
if (onBlur) onBlur();
}}
placeholder={placeholder}
secureTextEntry={isPassword}
returnKeyType={returnKeyType}
maxLength={maxLength}
autoCapitalize="none"
autoCorrect={false}
textContentType="none" // iOS only
underlineColorAndroid="transparent" // Android only
/>
</Container>
);
}
);
Input.propTypes = {
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
onChangeText: PropTypes.func.isRequired,
onSubmitEditing: PropTypes.func.isRequired,
onBlur: PropTypes.func,
placeholder: PropTypes.string,
isPassword: PropTypes.bool,
returnKeyType: PropTypes.oneOf(['done', 'next']),
maxLength: PropTypes.number,
};
Input.defaultProps = {
onBlur: () => {},
};
export default Input;
3. 키보드 감추기
이번에는 입력 도중 다른 곳을 터치하면 키보드가 사라지는 기능과
키보드가 입력 컴포넌트를 가리지 않도록 하는 기능을 넣어보겠다.
- TouchableWithoutFeedback + Keyboard API : 다른 영역을 터치했을 때 키보드 감추는 기능
- TouchableWithoutFeedback → 클릭에 대해 상호작용은 하지만 스타일 속성 X, 반드시 하나의 자식 컴포넌트를 가져야하는 특징 존재
- Keyboard API → 리액트 네이티브에서 제공하는 키보드 관련 API, 키보드 상태에 따른 이벤트 등록에 많이 사용
- dismiss 함수 : Keyboard API 에서 제공하는 활성화된 키보드를 닫는 기능
위의 요소들을 활용해 다른 영역을 클릭하면 키보드가 사라지게 만들었지만
위치에 따라 키보드가 Input 컴포넌트를 가리는 문제를 해결하지는 못한다.
이를 해결하기 위해 추가적인 라이브러리를 설치해 사용할 수 있다.
- react-native-keyboard-aware-scroll-view : 포커스된 TextInput 컴포넌트의 위치로 자동 스크롤되는 기능 등 Input 컴포넌트에 필요한 기능들 제공
npm install react-native-keyboard-aware-scroll-view
따라서 react-native-keyboard-aware-scroll-view 라이브러리에서 제공하는
KeyboardAwareScrollView 컴포넌트를 적용하면,
입력 도중 다른 영역을 터치했을 때 키보드가 사라질 뿐만 아니라
포커스를 얻은 컴포넌트의 위치에 맞춰 스크롤이 이동하는 것을 확인할 수 있다.
👇 KeyboardAwareScrollView 적용 코드 보기
import React, { useRef, useState } from 'react';
import { Button } from 'react-native';
import styled from 'styled-components/native';
import { images } from '../utils/images';
import { Image, Input } from '../components';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
const Container = styled.View`
flex: 1;
justify-content: center;
align-items: center;
background-color: ${({ theme }) => theme.background};
padding: 20px;
`;
const Login = ({ navigation }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const passwordRef = useRef();
return (
<KeyboardAwareScrollView
contentContainerStyle={{ flex: 1 }}
extraScrollHeight={20}
>
<Container>
<Image url={images.logo} imageStyle={{ borderRadius: 8 }} />
<Input
label="Email"
value={email}
onChangeText={text => setEmail(text)}
onSubmitEditing={() => passwordRef.current.focus()}
placeholder="Email"
returnKeyType="next"
/>
<Input
ref={passwordRef}
label="Password"
value={password}
onChangeText={text => setPassword(text)}
onSubmitEditing={() => { }}
placeholder="Password"
returnKeyType="done"
isPassword
/>
<Button title='Signup' onPress={() => navigation.navigate('Signup')} />
</Container>
</KeyboardAwareScrollView>
)
}
export default Login
4. 오류메세지
이번에는 Input 컴포넌트에 입력되는 값이 올바른 형태로 입력되었는지 확인하고
잘못된 값이 입력되면 오류 메시지를 보여주는 기능을 만들어보려 한다.
utils 폴더에 common.js 파일을 생성하고
올바른 이메일 형식인지 확인하는 함수와 입력된 문자열에서 공백을 모두 제거하는 함수를 정의했다.
export const validateEmail = email => {
const regex = /^[0-9?A-z0-9?]+(\.)?[0-9?A-z0-9?]+@[0-9?A-z]+\.[A-z]{2}.?[A-z]{0,3}$/;
return regex.test(email);
};
export const removeWhitespace = text => {
const regex = /\s/g;
return text.replace(regex, '');
};
비밀번호와 이메일에는 공백이 존재하지 않기 때문에 password, email의 값이 변경될 때마다 공백을 제거하고
validateEmail 함수를 이용해 공백이 제거된 이메일이 올바른 형식인지 검사하는 함수를 정의해
결과에 따라 오류 메세지가 나타나도록 해주었다.
const _handleEmailChange = email => {
const changeEmail = removeWhitespace(email);
setEmail(changeEmail);
setErrorMessage(
validateEmail(changeEmail) ? '' : 'Please verify your email.'
);
};
const _handlePasswordChange = password => {
setPassword(removeWhitespace(password));
};
5. Button 컴포넌트
이메일과 비밀번호가 입력되지 않으면 Button 컴포넌트가 동작하지 않도록 해서
사용자가 버튼 동작 여부를 시각적으로 명확하게 알수 있도록 해보겠다.
Button 컴포넌트에서 props를 통해 전달되는 disabled의 값에 따라 버튼 스타일이 변경되도록해서
TouchableOpacity 컴포넌트에 disabled 속성을 전달하면 값에 따라 버튼이 비활성화되는 기능을 추가했다.
useEffect를 이용해 email, password, errorMessage의 상태가 변할 때마다
조건에 맞게 disabled의 상태가 변경되도록 작성했다.
로그인 버튼은 이메일과 비밀번호가 입력되어 있고, 오류 메시지가 없는 상태에서만 활성화된다.
👇 Button 코드 보기
import React from 'react';
import styled from 'styled-components/native';
import PropTypes from 'prop-types';
const TRANSPARENT = 'transparent';
const Container = styled.TouchableOpacity`
background-color: ${({ theme, isFilled }) => isFilled ? theme.buttonBackground : TRANSPARENT};
align-items: center;
border-radius: 4px;
width: 100%;
padding: 10px;
opacity: ${({ disabled }) => (disabled ? 0.5 : 1)};
`;
const Title = styled.Text`
height: 30px;
line-height: 30px;
font-size: 16px;
color: ${({ theme, isFilled }) => isFilled ? theme.buttonTitle : theme.buttonUnfilledTitle};
`;
const Button = ({ containerStyle, title, onPress, isFilled, disabled }) => {
return (
<Container
style={containerStyle}
onPress={onPress}
isFilled={isFilled}
disabled={disabled}
>
<Title isFilled={isFilled}>{title}</Title>
</Container>
);
};
Button.defaultProps = {
isFilled: true,
};
Button.propTypes = {
containerStyle: PropTypes.object,
title: PropTypes.string,
onPress: PropTypes.func.isRequired,
isFilled: PropTypes.bool,
disabled: PropTypes.bool,
};
export default Button
6. 헤더 수정하기
로그인 화면에는 헤더가 굳이 필요하지 않기 때문에 헤더를 감추는 작업을 해보려 한다.
헤더가 렌더링되지 않도록 headerShown을 이용해 헤더를 숨겼다.
<Stack.Screen
name='Login'
component={Login}
options={{ headerShown: false }}
/>
7. 노치 디자인 대응
이전까지 알고 있던 SafeAreaView 컴포넌트를 이용하는 방법 외에도
노치 디자인에 대응하기 위한 방법이 있다.
- react-native-safe-area-context 라이브러리가 제공하는 useSafeAreaInsets Hook 함수 이용
- 장점 : iOS뿐만 아니라 안드로이드에서도 적용 가능한 padding 값을 전달한다. / 세밀하게 원하는 곳에 원하는 만큼만 padding 을 설정해서 노치 디자인 문제를 해결할 수 있다
위의 방법을 사용해서 로그인 화면을 수정해주었다.
👇 useSafeAreaInsets 적용 코드 보기
import React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components/native';
import { images } from '../utils/images';
import { Button, Image, Input } from '../components';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { removeWhitespace, validateEmail } from '../utils/common';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const Container = styled.View`
flex: 1;
justify-content: center;
align-items: center;
background-color: ${({ theme }) => theme.background};
padding: 0 20px;
padding-top: ${({ insets: { top } }) => top}px;
padding-bottom: ${({ insets: { bottom } }) => bottom}px;
`;
const ErrorText = styled.Text`
align-items: flex-start;
width: 100%;
height: 20px;
margin-bottom: 10px;
line-height: 20px;
color: ${({ theme }) => theme.errorText};
`;
const Login = ({ navigation }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [disabled, setDisabled] = useState(true);
const passwordRef = useRef();
const insets = useSafeAreaInsets();
useEffect(() => {
setDisabled(!(email && password && !errorMessage));
}, [email, password, errorMessage]);
const _handleEmailChange = email => {
const changeEmail = removeWhitespace(email);
setEmail(changeEmail);
setErrorMessage(
validateEmail(changeEmail) ? '' : 'Please verify your email.'
);
};
const _handlePasswordChange = password => {
setPassword(removeWhitespace(password));
};
const _handleLoginButtonPress = () => { };
return (
<KeyboardAwareScrollView
contentContainerStyle={{ flex: 1 }}
extraScrollHeight={20}
>
<Container insets={insets}>
<Image url={images.logo} imageStyle={{ borderRadius: 8 }} />
<Input
label="Email"
value={email}
onChangeText={_handleEmailChange}
onSubmitEditing={_handleLoginButtonPress}
placeholder="Email"
returnKeyType="next"
/>
<Input
ref={passwordRef}
label="Password"
value={password}
onChangeText={_handlePasswordChange}
onSubmitEditing={() => { }}
placeholder="Password"
returnKeyType="done"
isPassword
/>
<ErrorText>{errorMessage}</ErrorText>
<Button
title="Login"
onPress={_handleLoginButtonPress}
disabled={disabled}
/>
<Button
title="Sign up with email"
onPress={() => navigation.navigate('Signup')}
isFilled={false}
/>
</Container>
</KeyboardAwareScrollView>
)
}
export default Login
8. 화면 스크롤
회원 가입 페이지는 입력값이 많아진 것을 제외하고는 로그인 페이지와 비슷하다.
현재로서는 기기의 크기에 따라 화면의 위아래가 잘리는 문제와
어떤 값도 입력하지 않았는데 오류 메세지가 렌더링 되는 문제가 존재한다.
화면 크기에 따라 내용이 화면을 넘어가는 문제는 KeyboardAwareScrollView 컴포넌트에 contentContainerStyle에 “flex:1”을 스타일에 적용시키면서 발생한 것이다.
따라서 flex: 1을 삭제한 후 padding 값을 조정해서 문제를 해결했다.
👇회원가입 코드 보기
import React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components/native';
import { Button, Image, Input } from '../components';
import { removeWhitespace, validateEmail } from '../utils/common';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
const Container = styled.View`
flex: 1;
justify-content: center;
align-items: center;
background-color: ${({ theme }) => theme.background};
padding: 40px 20px;
`;
const ErrorText = styled.Text`
align-items: flex-start;
width: 100%;
height: 20px;
margin-bottom: 10px;
line-height: 20px;
color: ${({ theme }) => theme.errorText};
`;
const Signup = () => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [passwordConfirm, setPasswordConfirm] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [disabled, setDisabled] = useState(true);
const emailRef = useRef();
const passwordRef = useRef();
const passwordConfirmRef = useRef();
useEffect(() => {
let _errorMessage = '';
if (!name) {
_errorMessage = 'Please enter your name.';
} else if (!validateEmail(email)) {
_errorMessage = 'Please verify your email.';
} else if (password.length < 6) {
_errorMessage = 'The password must contain 6 characters at least.';
} else if (password !== passwordConfirm) {
_errorMessage = 'Passwords need to match.';
} else {
_errorMessage = '';
}
setErrorMessage(_errorMessage);
}, [name, email, password, passwordConfirm]);
useEffect(() => {
setDisabled(
!(name && email && password && passwordConfirm && !errorMessage)
);
}, [name, email, password, passwordConfirm, errorMessage]);
const _handleSignupButtonPress = () => { };
return (
<KeyboardAwareScrollView extraScrollHeight={20}>
<Container>
<Image rounded />
<Input
label="Name"
value={name}
onChangeText={text => setName(text)}
onSubmitEditing={() => {
setName(name.trim());
emailRef.current.focus();
}}
onBlur={() => setName(name.trim())}
placeholder="Name"
returnKeyType="next"
/>
<Input
ref={emailRef}
label="Email"
value={email}
onChangeText={text => setEmail(removeWhitespace(text))}
onSubmitEditing={() => passwordRef.current.focus()}
placeholder="Email"
returnKeyType="next"
/>
<Input
ref={passwordRef}
label="Password"
value={password}
onChangeText={text => setPassword(removeWhitespace(text))}
onSubmitEditing={() => passwordConfirmRef.current.focus()}
placeholder="Password"
returnKeyType="done"
isPassword
/>
<Input
ref={passwordConfirmRef}
label="Password Confirm"
value={passwordConfirm}
onChangeText={text => setPasswordConfirm(removeWhitespace(text))}
onSubmitEditing={_handleSignupButtonPress}
placeholder="Password"
returnKeyType="done"
isPassword
/>
<ErrorText>{errorMessage}</ErrorText>
<Button
title="Signup"
onPress={_handleSignupButtonPress}
disabled={disabled}
/>
</Container>
</KeyboardAwareScrollView>
)
}
export default Signup
이어서
회원가입 화면이 처음 렌더링될 때 아무 값이 입력되지 않았는데 오류 메시지가 나타나는 문제를 해결하기 위해
useRef를 활용하려 한다.
didMountRef에 어떤 값도 대입하지 않다가
컴포넌트가 마운트되었을 때 useEffect 함수에서 didMountRef에 값을 대입하는 방법으로
컴포넌트의 마운트 여부를 확인하도록 코드를 작성했다.
useEffect(() => {
if (didMountRef.current) {
let _errorMessage = '';
if (!name) {
_errorMessage = 'Please enter your name.';
} else if (!validateEmail(email)) {
_errorMessage = 'Please verify your email.';
} else if (password.length < 6) {
_errorMessage = 'The password must contain 6 characters at least.';
} else if (password !== passwordConfirm) {
_errorMessage = 'Passwords need to match.';
} else {
_errorMessage = '';
}
setErrorMessage(_errorMessage);
} else {
didMountRef.current = true;
}
}, [name, email, password, passwordConfirm]);
9. 사진 입력 받기
먼저 회원가입 페이지에 이미지를 편집할 수 있도록 할 버튼을 생성해주었고
사진 접근 기능을 위해 expo-image-picker 라이브러리를 추가로 설치해주었다.
또한,
아래와 같은 이유로 교재와는 다르게 expo-permissions가 필요하지 않다.
💡
expo-permissions는 최신 Expo SDK에서 제거됨
expo-image-picker를 사용할 때 expo-permissions이 더 이상 필요하지 않다.
expo-media-library로 권한 요청 필요
npm install expo-image-picker
👇권한 설정 코드 보기
const requestPermissions = async () => {
const { status: mediaStatus } = await ImagePicker.requestMediaLibraryPermissionsAsync();
const { status: cameraStatus } = await ImagePicker.requestCameraPermissionsAsync();
if (mediaStatus !== 'granted' || cameraStatus !== 'granted') {
Alert.alert(
'Permission Required',
'Please allow access to your gallery and camera.'
);
}
};
useEffect(() => {
requestPermissions();
}, []);
const _handleEditButton = async () => {
try {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
Alert.alert(
'Photo Permission',
'Please turn on the camera roll permissions.'
);
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 1,
});
if (!result.canceled && result.assets.length > 0) {
onChangeImage(result.assets[0].uri);
}
} catch (e) {
Alert.alert('Photo Error', e.message);
}
};
- mediaTypes: 조회하는 자료의 타입
- allowsEditing: 이미지 선택 후 편집 단계 진행 여부
- aspect: 안드로이드 전용 옵션으로 이미지 편집 시 사각형의 비율([x, y])
- quality: 0~1 사이의 값을 받으며 압축품질을 의미(1: 최대 품질)
또한,
교재에 적힌대로 result.uri를 사용하면 undefined일 가능성이 있기 때문에
Expo SDK 최신 버전에서는 result.assets[0].uri를 사용해야 한다.
💡
이전에는 cancelled(L 두 개) 라는 속성이 사용되었지만,
Expo 48부터 canceled(L 하나) 로 변경되었다.
10. 로그인/회원가입
먼저 로그인 기능을 위해 파이어베이스에서
이메일과 비밀번호를 이용해서 인증받는 함수는 signlnWithEmailAndPassword 이다.
로그인 기능을 추가하기 위해 사용자를 파이어베이스에서 만들어주겠다.
사용자 추가를 완료한 후
파이어베이스 설정 파일에 로그인 함수를 만들어 주었다.
👇로그인 함수 코드 보기
import { initializeApp } from 'firebase/app';
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth';
import config from '../../firebase.json';
const app = initializeApp(config);
const Auth = getAuth(app);
export const login = async ({ email, password }) => {
const userCredential = await signInWithEmailAndPassword(Auth, email, password);
return userCredential.user;
};
const _handleLoginButtonPress = async () => {
try {
const user = await login({email, password});
Alert.alert('Login Success', user.email);
} catch (e) {
Alert.alert('Login Error', e.message);
}
};
이번에는 회원가입 기능을 만들어보겠다.
파이어베이스에서 제공하는 함수 중 이메일과 비밀번호를 이용해서 사용자를 생성하는 함수는 createUserWithEmailAndPassword 이다.
👇회원가입 함수 코드 보기
export const signup = async ({ email, password }) => {
const userCredential = await createUserWithEmailAndPassword(Auth, email, password);
return userCredential.user;
}
const _handleSignupButtonPress = async () => {
try {
const user = await signup({email, password});
Alert.alert('Signup Success', user.email);
} catch (e) {
Alert.alert('Signup Error', e.message);
}
};
현재 사용자는 정상적으로 추가되지만,
사용자의 사진과 이름을 이용하지 않고 이메일과 비밀번호만으로 사용자를 생성했다.
따라서
이제 이미지를 업로드 하고 이름을 설정하도록 코드를 수정해보려 한다.
⭐⭐그러나⭐⭐
스토리지가 유료인 관계로
스토리지를 사용하지 않고 이미지를 base64 인코딩하여 이름과 함께 firestore에 사용자 정보를 저장해보겠다.
firestore의 규칙을 로그인한 사용자만 읽기 및 쓰기가 가능하도록 업데이트해주고
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null;
}
}
}
이미지 파일을 Base64로 변환하기 위해 추가적인 라이브러리를 설치해주었다.
npm install expo-file-system
라이브러리의 FileSystem을 활용하여
인코딩한 이미지 url을 firestore에 저장하는 코드를 작성했다.
👇이미지 인코딩 저장 함수 코드 보기
import { initializeApp } from 'firebase/app';
import { createUserWithEmailAndPassword, getAuth, signInWithEmailAndPassword, updateProfile } from 'firebase/auth';
import { getFirestore, collection, doc, setDoc } from 'firebase/firestore';
import * as FileSystem from 'expo-file-system';
import config from '../../firebase.json';
const app = initializeApp(config);
const db = getFirestore(app);
const Auth = getAuth(app);
// 이미지 URI를 Base64로 변환하는 함수
const convertImageToBase64 = async (uri) => {
const base64 = await FileSystem.readAsStringAsync(uri, { encoding: FileSystem.EncodingType.Base64 });
return `data:image/png;base64,${base64}`; // Base64 데이터 URL 형식
};
export const login = async ({ email, password }) => {
const userCredential = await signInWithEmailAndPassword(Auth, email, password);
return userCredential.user;
};
export const signup = async ({ email, password, name, photoUrl }) => {
const { user } = await createUserWithEmailAndPassword(Auth, email, password);
let imageBase64 = '';
if (photoUrl) {
imageBase64 = await convertImageToBase64(photoUrl);
}
// Firestore의 "users" 컬렉션에 사용자 데이터 저장
const usersCollection = collection(db, "users"); // "users" 컬렉션 가져오기
const userRef = doc(usersCollection, user.uid);
await setDoc(userRef, {
uid: user.uid,
email: email,
name: name,
photo: imageBase64, // Firestore에 Base64 이미지 저장
});
// Firebase Auth의 사용자 프로필 업데이트
await updateProfile(user, {
displayName: name,
photoURL: '', // Storage를 사용하지 않으므로 빈 값
});
return user;
};
12. Spinner 컴포넌트
: 로그인/회원가입이 진행되는 동안 잘못된 입력이나 클릭을 방지하는 기능
: Spinner 컴포넌트는 리액트 네이티브에서 제공하는 Activityindicator 컴포넌트를 이용해서 쉽게 만들 수 있다.
Spinner 컴포넌트를 AuthStack 내비게이션의 하위 컴포넌트로 사용하면
내비게이션을 포함한 화면 전체를 차지할 수 없다.
따라서 내비게이션을 포함한 화면 전체를 감싸기 위해서는 AuthStack 내비게이션과 같은 위치에 Spinner 컴포넌트를 사용해야한다.
Spinner 컴포넌트를 사용하면 화면 전체를 감싸서 어떤 행동을 할 수 없게 되는데 이는 특정 상황에서만 렌더링 되어야한다.
따라서 여러 화면에서 하나의 상태를 이용하기 위해 전역적으로 상태를 관리해야한다.
Spinner 컴포넌트의 렌더링 상태를 전역적으로 관리하기 위해 Context API를 사용하려 한다.
- createContext 함수 이용 ▷ Context 생성
- Provider 컴포넌트의 value에 Spinner 컴포넌트의 렌더링 상태를 관리할 inProgress 상태 변수와 상태를 변경할 수 있는 함수 전달
- 상태를 변경하는 함수는 사용자가 명확하게 렌더링 여부를 관리할 수 있도록 start 함수와 stop 함수를 만들어서 전달
👇Context 코드 보기
import React, { createContext, useState } from "react";
const ProgressContext = createContext({
inProgress: false,
spinner: () => { },
});
const ProgressProvider = ({ children }) => {
const [inProgress, setInProgress] = useState(false);
const spinner = {
start: () => setInProgress(true),
stop: () => setInProgress(false),
};
const value = { inProgress, spinner };
return (
<ProgressContext.Provider value={value}>
{children}
</ProgressContext.Provider>
);
};
export { ProgressContext, ProgressProvider };
import React, { useContext } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import AuthStack from './AuthStack';
import { Spinner } from '../components';
import { ProgressContext } from '../contexts';
const Navigation = () => {
const { inProgress } = useContext(ProgressContext);
return (
<NavigationContainer>
<AuthStack />
{inProgress && <Spinner />}
</NavigationContainer>
)
}
export default Navigation
현재 inProgress의 초깃값이 false이므로 Spinner 컴포넌트는 나타나지 않는다.
이제 로그인 버튼을 클릭했을 때 inProgress 상태를 변경하여
Spinner 컴포넌트가 렌더링되도록 해보겠다.
👇Context 로그인 버튼에 적용 코드 보기
const { spinner } = useContext(ProgressContext);
const _handleLoginButtonPress = async () => {
try {
spinner.start();
const user = await login({ email, password });
Alert.alert('Login Success', user.email);
} catch (e) {
Alert.alert('Login Error', e.message);
} finally {
spinner.stop();
}
};
회원가입 버튼에도 동일한 방식으로 Spinner 렌더링을 적용해주었다.
생각보다 글이 길어져서
메인 화면 구현은 다음 글에서 적어보겠다.
'공부 > ReactNative' 카테고리의 다른 글
[ReactNative] 채팅 애플리케이션(Firebase) 3 | 처음 배우는 리액트네이티브 9장 (0) | 2025.02.05 |
---|---|
[ReactNative] 채팅 애플리케이션(Firebase) 1 | 처음 배우는 리액트네이티브 9장 (0) | 2025.02.03 |
[ReactNative] FlatList, 무한 스크롤 | 처음 배우는 리액트네이티브 8장 (0) | 2025.01.29 |
[ReactNative] Navigation | 처음 배우는 리액트네이티브 8장 (0) | 2025.01.28 |
[ReactNative] 전역 상태 관리, Context API, useContext | 처음 배우는 리액트네이티브 7장 (0) | 2025.01.23 |