[리액트 네이티브] 채팅 애플리케이션 만들기 1.파이어베이스 세팅, 로그인, 회원가입
my code archive
article thumbnail
반응형

💡구현 목표

  • 로그인/회원가입 : 이메일, 비밀번호를 이용한 로그인 및 회원가입
  • 프로필 : 내 정보 확인 및 변경
  • 채널 생성, 목록 조회
  • 채널 : 실시간으로 메시지를 주고받는 독립된 공간

1. 프로젝트 준비

프로젝트 생성

expo init react-native-simple-chat	//프로젝트 생성

npm install @react-navigation/native	//리액트 내비게이션 설치

설치한 라이브러리

expo-image-picker : 기기 사진, 영상을 가져오는 기능
moment : 시간을 다양한 형태로 변경하는 등 시간과 관련된 많은 기능 제공
react-native-keyboard-aware-scroll-view : 키보드가 화면을 가리며 생기는 불편함 해결
react-native-gifted-chat : 메시지를 주고받는 채팅 화면을 구현할 수 있도록 돕는 라이브러리

프로젝트 구조

  • components : 컴포넌트 파일 관리
  • contexts: Context API 파일 관리
  • navigations : 내비게이션 파일 관리
  • screens : 화면 파일 관리
  • utils : 프로젝트에서 이용할 기타 기능 관리

2. 파이어베이스

파이어베이스는 인증(Authentication), 데이터베이스(Database) 등 다양한 기능을 제공하는 개발 플랫폼. 파이어베이스를 이용하면 대부분 서비스에서 필요한 서버, 데이터베이스를 직접 구축하지 않아도 개발이 가능하다는 장점이 있다.

 

먼저 파이어베이스 콘솔에서 프로젝트를 생성해 준다.

https://firebase.google.com/?hl=ko&gclid=Cj0KCQjwz96WBhC8ARIsAATR252zN52tdqbCVnd-Zno0S-Og2Nnx0axtZGHC7335jv3QL15dZplgyREaArhIEALw_wcB&gclsrc=aw.ds 

 

Firebase

Firebase는 고품질 앱을 빠르게 개발하고 비즈니스를 성장시키는 데 도움이 되는 Google의 모바일 플랫폼입니다.

firebase.google.com

파이어베이스 프로젝트 설정 / 일반 / 내 앱에서 파이어베이스 설정값을 확인 후

firebase.json 파일에 생성 후 노출되지 않도록 .gitignore 파일에 추가한다.

2-1. 인증

파이어베이스에서 제공하는 다양한 인증 기능 중 '이메일/비밀번호' 선택

2-2. 프로젝트와 연동

리액트 네이티브에서 파이어베이스를 사용하기 위해 라이브러리를 설치해 준다.

expo install firebase	//파이어베이스 라이브러리 설치

그런데 여기서 내가 고생했던 부분... 위의 명령어로 설치 시 가장 최근 버전이 설치되는데, 책에서 사용하는 버전과 달라서 문법이 바뀌고 여러 가지 에러를 겪었다😢 이 방법이 맞는지 모르겠지만 버전을 낮추니 에러를 잡는 것은 해결했다. 파이어베이스 버전때문에 약 이틀 간 에러로 고생함 ㅠㅠㅠ

npm uninstall firebase	//삭제
npm install firebase@9.6.11	//낮은 버전으로 설치

3. 로고 변경

파이어베이스 storage에 아이콘, 배경화면 이미지 업로드 후 파일명을 클릭하면 링크가 나온다.

image.js 파일 생성 후 각 이미지 링크를 가져와서 사용한다.

const prefix = 
    "https://firebasestorage.googleapis.com/v0/b/react-native-simple-chat-3ded8.appspot.com/o";

export const images = {
    logo: `${prefix}/logo.png?alt=media`,
    photo: `${prefix}/photo.png?alt=media`,
};

변경 완료

4. 로그인

먼저 프로젝트에서 사용할 색들을 theme.js에 따로 정의해 주었다.

const colors = {
    white: '#ffffff',
    black: '#000000',
    grey_0: '#d5d5d5',
    grey_1: '#a6a6a6',
    red: '#e84118',
    blue: '#3679fe',
};

export const theme= {
    background: colors.white,
    text: colors.black,
    errorText: colors.red,
    
    //Image Component
    imageBackground: colors.grey_0,
    imageButtonBackground: colors.grey_1,
    imageButtonIcon: colors.white,

    //Input Component
    label: colors.grey_1,
    inputPlaceholder: colors.grey_1,
    inputBorder: colors.grey_1,

    //Button Component
    buttonBackground: colors.blue,
    buttonTitle: colors.white,
    buttonUnfilledTitle: colors.blue,
    bottonLogout: colors.red,

     // Navigation
    headerTintColor: colors.black,
    tabActiveColor: colors.blue,
    tabInactiveColor: colors.grey_1,

    //Spinner
    spinnerBackground: colors.black,
    spinnerIndicator: colors.white,
};

Input 컴포넌트 작성

  • 라벨을 TextInput 컴포넌트 위에 랜더링, 포커스 여부에 따라 스타일이 변경됨.
  • secureTextEntry : 문자를 감추는 기능, 비밀번호 입력에 많이 사용됨.
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)}
                onBlue={()=>{
                    setIsFocused(false);
                    onBlur()
                }}
                placeholder={placeholder}
                secureTextEntry={isPassword}
                returnKeyType={returnKeyType}
                maxLength={maxLength}
                autoCapitalize="none"
                autoCorrect={false}
                textContentType="none"  //iOS only
                underlineColorAndroid="transparent" //Android only
            />
        </Container>
    );
}
);

Input.defaultProps = {
    onBlue: () => {},
};

Input.propTypes = {
    label: PropTypes.string.isRequired,
    value: PropTypes.string.isRequired,
    onChangeText: PropTypes.func.isRequired,
    onBlue: PropTypes.func,
    placeholder: PropTypes.string,
    isPassword: PropTypes.bool,
    returnKeyType: PropTypes.oneOf(['done','next']),
    maxLength: PropTypes.number,
};

export default Input;

Button 컴포넌트 작성

const Button = ({containerStyle, title, onPress, isFilled, disabled}) => {
    return (
        <Container
            styled={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;

이메일과 비밀번호가 입력되지 않으면 Button 컴포넌트가 동작하지 않도록 수정

opacity: ${({disabled})=>(disabled ? 0.5 : 1)};

로그인 UI Login 컴포넌트

  • 입력되는 이메일, 비밀번호를 관리할 email, password를 useState 함수로 생성 후 각각 이메일, 비밀번호를 입력받는 Input 컴포넌트의 value로 지정함.
  • 비밀번호 입력 컴포넌트는 입력되는 값이 보이지 않도록 isPassword 속성 추가
  • 이메일 입력 Input 컴포넌트의 returnKeyType을 next로 설정, 비밀번호 Input 컴포넌트는 done으로 설정.
  • useRef : 이메일 입력 컴포넌트에서 키보드의 next 버튼 클릭 시 비밀번호 입력 컴포넌트로 포커스 이동
  • KeyboardAwareScrollView : 입력 도중 다른 영역 터치했을 때 키보드가 사라짐, 포커스를 얻은 TextInput 컴포넌트의 위치에 맞춰 스크롤 이동
 return(
        //활성화된 키보드를 닫음.
        // <TouchableWithoutFeedback onPress={Keyboard.didmiss}>
        <KeyboardAwareScrollView
            contentContainerStyle={{ flex: 1 }}
            extraScrollHeight={20}>
        <Container insets={insets}>
            {/* <Text style={{fontSize: 30}}>Login Screen</Text> */}
            <Image url={images.logo} imageStyle={{borderRadius: 8}}/>
            <Input
                label="Email"
                value={email}
                /*onChangeText={text=>setEmail(text)}*/
                onChangeText={_handleEmailChange}
                onSubmitEditing={()=>passwordRef.current.focus()}
                placeholder="Email"
                returnKeyType="next"
            />
            <Input
                ref={passwordRef}
                label="Password"
                value={password}
                /*ChangeText={text=>setPassword(text)}*/
                onChangeText={_handlePasswordChange}
                /*onSubmitEditing={()=>{}}*/
                onSubmitEditing={_handleLoginButtonPress}
                placeholder="Password"
                returnKeyType="done"
                isPassword
            />
            {/* <Button title="Signup" onPress={()=>navigation.navigate('Signup')} /> */}
            <ErrorText>{errorMessage}</ErrorText>
            <Button title="Login" onPress={_handleLoginButtonPress} disabled={disabled} />
            <Button
                title="Sign up with email"
                onPress={()=>navigation.navigate('Signup')}
                isFilled={false}
            />
        </Container>
        {/* </TouchableWithoutFeedback> */}
        </KeyboardAwareScrollView>
    );
};

export default Login;

이제 로그인에 사용할 사용자를 파이어베이스에 추가해 준다.

로그인 함수 구현

  • signInWithEmailAndPassword : 이메일과 비밀번호를 이용해서 인증받는 함수
//로그인
export const login = async ({email, password}) => {
    const {user} = await authService.signInWithEmailAndPassword(email, password);
    return user;
}

이것도 파이어베이스 버전으로 고생을 많이했다. ㅠㅠ 책에서 나오는 문법대로 하면 에러가 발생하고 v9 이상부터 새로 바뀐 문법을 적용해 주어야 한다.

로그인 화면 수정

const _handleLoginButtonPress = async () => {
        try{
            const user = await login({email, password});
            Alert.alert('Login Success', user.email);
            //인증되면 UserContext의 user를 수정하도록
            //dispatch(user);
        }catch(e){
            Alert.alert('Login Error', e.message);
        }
    };

실행 화면

5. 회원가입

입력받아야 하는 내용이 많아졌을 뿐, 로그인 화면과 거의 유사하다.

 return (
        <KeyboardAwareScrollView
            extraScrollHeight={20}
        >
        <Container>
            <Image rounded url={photoUrl} showButton onChangeImage={url => setPhotoUrl(url)}/>
            <Input
                label="Name"
                value={name}
                onChangeText={text=>setName(text)}
                onSubmitEditing={()=>{
                    setName(name.trim());
                    emailRef.current.focus();
                }}
                onBlue={()=>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}
            />
            {/* <Text style={{ fontSize: 30 }}>Signup Screen</Text> */}
        </Container>
        </KeyboardAwareScrollView>
    );
};

export default Signup;

useEffect를 이용해 관련된 값이 변할 때마다 오류 메시지가 렌더링되도록 유효성 검사 추가

사진 입력받기

import React, {useEffect} from "react";
import * as ImagePicker from 'expo-image-picker';
import * as Permission from 'expo-permissions';
import styled from "styled-components";
import PropTypes from 'prop-types';
import { Alert, Platform } from "react-native";
import {MaterialIcons} from '@expo/vector-icons';

...

...

const Image = ({ url, imageStyle, rounded, showButton, onChangeImage }) => {
    useEffect(()=>{
        (async ()=>{
            try{
                if(Platform.OS !== 'web'){
                    const{
                        status,
                    } = await ImagePicker.requestMediaLibraryPermissionsAsync();
                    if (status !== 'granted'){
                        Alert.alert(
                            'Photo Permission',
                            'Please turn on the camera roll permissions.'
                        );
                    }
                }
            }catch(e){
                Alert.alert('Photo Permission Error', e.message);
            }
        })();
    }, []);

    const _handleEditButton = async () => {
        try{
            const result = await ImagePicker.launchImageLibraryAsync({
                mediaTypes : ImagePicker.MediaTypeOptions.Images,
                allowsEditing: true,
                aspect: [1,1],
                quality: 1,
            });

            if(!result.cancelled){
                onChangeImage(result.uri);
            }
        }catch(e){
            Alert.alert('Photo Error', e.message);
        }
    };

    return(
        <Container>
            <StyledImage source={{ url: url }} style={imageStyle} rounded={rounded} />
            {showButton && <PhotoButton onPress={_handleEditButton} />}
        </Container>
    );
};

Image.defaultProps = {
    ...
}

Image.prototype = {
   ...
};

export default Image;

expo-image-picker 라이브러리 설치를 통해 버튼 클릭 시 사진첩에 접근하도록 구현

회원가입 함수 구현

  • createUserWithEmailAndPassword : 파이어베이스에서 제공하는 함수 중 이메일과 비밀번호를 이용해 사용자를 생성하는 함수
//회원가입
export const signup = async ({email, password, name, photoUrl}) => {
    const {user} = await authService.createUserWithEmailAndPassword(email, password);
    const storageUrl = photoUrl.startsWith('https')
        ? photoUrl
        : await uploadImage(photoUrl);
    await user.updateProfile({
        displayName: name,
        photoURL: storageUrl,
    });
    return user;
}

이 부분 역시 파이어베이스 v9 문법 차이로 고생을 했던...

회원가입 화면 수정

const _handleSignupButtonPress = async () => {
        try{
            const user = await signup({email, password, name, photoUrl});
            console.log(user);
            //Alert.alert('Signup Success', user.email);
        }catch(e){
            Alert.alert('Signup Error', e.message);
        }
    };

회원가입 완료

signup 함수에서 반환되는 user 객체를 보면 uid(사용자마자 갖고 있는 유일한 키값)로 사용자를 식별하고 있다.
=> 파이어베이스에서 사용되는 사용자 생성 함수는 사용자의 이름을 이용하지 않고 이메일, 비밀번호만으로 사용자 생성이 가능하다.

반응형
profile

my code archive

@얼레벌레 개발자👩‍💻

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

반응형