솜이의 데브로그

[React Native] 채팅 애플리케이션 만들기 본문

dev/React native

[React Native] 채팅 애플리케이션 만들기

somsoming 2021. 12. 26. 18:20

Reference : 처음 배우는 리액트네이티브 (김범준)

 

 

네비게이션 및 추가 라이브러리들을 먼저 설치한다.

npm install @react-navigation/native
expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view
npm install @react-navigation/stack @react-navigation/bottom-tabs

 

 

스타일드 컴포넌트 라이브러리와 prop-types 라이브러리도 추가적으로 설치한다.

 

 

src/theme.js

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

export const theme = {
    background: colors.white,
    text: colors.black,
};

공통으로 사용할 색을 정의하고, 배경색과 글자의 색을 미리 정의한다.

 

 

파이어베이스

  • 파이버에시는 인증, 데이터베이스 등의 다양한 기능을 제공하는 개발 플랫폼이다. 
  • 대부분의 서비스에서 필요한 서버와 데이터베이스를 직접 구축하지 않아도 개발이 가능하다.

https://console.firebase.google.com/ 

 

로그인 - Google 계정

하나의 계정으로 모든 Google 서비스를 Google 계정으로 로그인

accounts.google.com

파이어베이스 콘솔에서 프로젝트를 생성한다.

파이어베이스에서 앱을 추가 후, 해당 앱의 구성을 복사해서 firebase.json 파일로 생성하여 추가한다.

또 이 파일이 커밋되지 않도록 .gitignore 파일에 firebase.json 을 추가한다.

 

위와 같이 이메일과 비밀번호를 이용하여 인증하는 기능을 만들기 위해 활성화한다.

 

파이어베이스 데이터베이스를 생성하고, 본인이 위치하는 지역을 선택한다.

또 사용자의 사진을 저장하고 가져오는 기능을 만들기 위해 스토리지를 생성한다.

 

 

라이브러리 설치

expo install firebase

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

https://www.npmjs.com/package/firebase 

 

firebase

Firebase JavaScript library for web and Node.js

www.npmjs.com

라이브러리 설치 후 다음과 같이 파일을 작성한다.

 

src/utils/firebase.js

import * as firebase from 'firebase';
import config from '../../firebase.json';

const app = firebase.initializeApp(config);

이렇게 하면 파이어베이스 사용 준비가 완료된다.

 

 

앱 아이콘과 로딩 화면

앱의 아이콘으로 사용할 1024*1024 크기의 icon.png 파일과 로딩화면으로 사용할 1242*2436 크기의 spalsh.png 파일을 생성해서 assets 폴더에 자동으로 생성된 파일들과 교체하고 App 컴포넌트를 수정한다.

 

const cacheImages = images => {
    return images.map(image => {
        if (typeof image === 'string') {
            return Image.prefetch(image);
        } else{
            return Asset.fromModule(image).downloadAsync();
        }
    });
};
const cacheFonts = fonts => {
    return fonts.map(font => Font.loadAsync(font));
};
  • 프로젝트에서 사용할 이미지와 폰트를 미리 불러와서 사용할 수 있도록 cacheImages와 cacheFonts 함수를 작성한다.

 

const App = () => {
    const [isReady, setIsReady] = useState(false);

    const _loadAssets = async () => {
        const imageAssets = cacheImages([require('../assets/splash.png')]);
        const fontAssets = cacheFonts([]);

        await Promise.all([...imageAssets, ...fontAssets]);
    };

    return isReady ? (
        <ThemeProvider theme={theme}>
            <StatusBar barStyle='dark-content'/>
        </ThemeProvider>
    ) : (
        <AppLoading
            startAsync={_loadAssets}
            onFinish={()=> setIsReady(true)}
            onError={console.warn}
        />
    );
};
  • 미리 불러와야하는 항목들을 모두 불러오고 화면이 렌더링되도록 Apploading 컴포넌트의 startAsync와 _loadAssets 함수를 지정한다.
  • AppLoading import 해오는 항목이 expo-app-loading 으로 변경되었다.

 

인증화면

인증을 위해 이메일과 비밀번호를 로그인 및 회원가입화면에서 이메일과 비밀번호를 입력받도록 한다.

 

src/screens/Login.js

import React from "react";
import styled from 'styled-components/native';
import { Text, Button } from 'react-native';

const Container = styled.View`
    flex: 1;
    justify-content: center;
    align-items: center;
    background-color: ${({theme}) => theme.background};
`;

const Login = ({ navigation }) => {
    return(
        <Container>
            <Text style={{ fontsize: 20 }}>Login Screen</Text>
            <Button title="Signup" onPress={() => navigation.navigate('Signup')} />
        </Container>
    );
};

export default Login;
  • 회원가입 화면으로 이동할 수 있는 버튼을 넣은 로그인 화면 작성

 

src/screens/Signup.js

import React from "react";
import styled from "styled-components/native";
import { Text } from 'react-native';

const Container = styled.View`
    flex: 1;
    justify-content: center;
    align-items: center;
    background-color: ${({ theme }) => theme.background};
`;

const Signup = () => {
    return(
        <Container>
            <Text style = {{ fontSize: 30 }}>Signup Screen</Text>
        </Container>
    );
};

export default Signup;
  • 화면을 확인할 수 있는 텍스트가 있는 간단한 화면 구성

index.js 파일에 위의 두 파일을 import, export 하도록 작성한다.

 

스택 내비게이션을 이용해 작성

src/navigations/AuthStack.js

import React, { useContext } from "react";
import { ThemeContext } from "styled-components/native";
import { createStackNavigator } from '@react-navigation/stack';
import { Login, Signup } from '../screens';

const Stack = createStackNavigator();

const AuthStack = () => {
    const theme = useContext(ThemeContext);
    return(
        <Stack.Navigator
            initialRouteName="Login"
            screenOptions={{
                headerTitleAlign: 'center',
                cardStyle: { backgroundColor: theme.background },
            }}
        >
            <Stack.Screen name="Login" component={Login} />
            <Stack.Screen name="Signup" component={Signup} />
        </Stack.Navigator>
    );
};

export default AuthStack;
  • 첫 화면을 로그인화면으로 설정하고, 로그인과 회원가입 화면을 가진 내비게이션 생성
  • 스타일드 컴포넌트에서 제공하는 ThemeContext 컴포넌트와 useContext hook 함수를 이용해 theme을 받아온다.
  • 내비게이션 화면의 배경 색 설정, 헤더 타이틀 위치를 안드로이드와 iOS에서 동일한 위치에 렌더링 하기 위해 center에 지정.

 

src/navigations/index.js

import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import AuthStack from "./AuthStack";

const Navigation = () => {
    return(
        <NavigationContainer>
            <AuthStack />
        </NavigationContainer>
    );
};

export default Navigation;
  • NavgationContainer 컴포넌트 사용
  • 자식 컴포넌트로 AuthStack 내비게이션을 사용

이제 작성한 파일들을 렌더링되도록 App.js 파일을 수정한다.

 

src/components/Image.js

import React from "react";
import styled from "styled-components/native";
import PropTypes from 'prop-types';

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.PropTypes = {
    uri: PropTypes.string,
    imageStyle: PropTypes.object,
};

export default Image;
  • props로 전달되는 url을 렌더링하고 imageStyle을 전달받아 컴포넌트의 스타일을 수정할 수 있는 Image 컴포넌트를 생성.
  • Image 컴포넌트 작성이 완료되면 components 폴더에 index.js 파일을 생성
  • firebase 스토리지에 로고를 업로드하고, url을 받아와 src/utils/images.js 파일 밑에 입력한다. 이 때 token값은 제외하고 입력한다.
  • 후 로고 이미지도 로딩과정에서 미리 불러오도록 App 컴포넌트를 수정한다.

 

src/components/Input.js

import React, { useState } from "react";
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)};
`;
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 = ({
    label,
    value,
    onChangeText,
    onSubmitEditing,
    onBlur,
    placeholder,
    isPassword,
    returnKeyType,
    maxLength,
}) => {
    const [isFocused, setIsFocused] = useState(false);

    return(
        <Container>
            <Label isFocused={isFocused}>{label}</Label>
            <StyledTextInput
                isFocused={isFocused}
                value={value}
                onChangeText={onChangeText}
                onSubmitEditing={onSubmitEditing}
                onFocus={() => setIsFocused(true)}
                onBlur={() => {
                    setIsFocused(false);
                    onBlur();
                }}
                placeholder={placeholder}
                secureTextEntry={isPassword}
                returnKeyType={returnKeyType}
                maxLength={maxLength}
                autoCapitalize="none"
                autoCorrect={false}
                textContentType="none"
            />
        </Container>
    );
};

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

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,
};

export default Input;
  • 라벨을 TextInput 컴포넌트 위에 렌더링하고, 포커스 여부에 따라 스타일이 변경되도록 컴포넌트를 생성.
  • secureTextEntry 속성은 입력되는 문자를 감추는 기능이다.
  • Input 컴포넌트 작성이 완료되면 components 폴더의 index.js 파일을 수정한다.

 

src/screens/Login.js

const Login = ({ navigation }) => {
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    return(
        <Container>
            <Image url={images.logo} imageStyle={{ borderRadius: 8 }} />
            <Input
                label="Email"
                value={email}
                onChangeText={text => setEmail(text)}
                onSubmitEditing={() => {}}
                placeholder="Email"
                returnKeyType="next"
            />
            <Input
                label="Password"
                value={password}
                onChangeText={text => setPassword(text)}
                onSubmitEditing={() => {}}
                placeholder="Password"
                returnKeyType="done"
                isPassword
            />
            <Button title="Signup" onPress={() => navigation.navigate('Signup')} />
        </Container>
    );
};
  • 입력되는 이메일과 비밀번호를 관리할 email과 password를 useState 함수로 생성
  • 각각 이메일과 비밀번호를 입력받는 Input 컴포넌트의 value로 지정.
  • 비밀번호를 입력받는 Input 컴포넌트는 입력되는 값이 보이지 않도록 isPassword 속성을 추가한다.
  • 이메일 입력받는 returnKeyType은 next로 설정해 바로 비밀번호를 입력하도록한다.

 

 

  • useRef를 이용해 이메일을 입력받는 Input 컴포넌트에서 키보드의 next 버튼을 클릭하면 비밀번호를 입력하는 Input 컴포넌트 포커스가 이동.
  • passwordRef를 만들고 비밀번호를 입력하는 Input 컴포넌트의 ref로 지정
  • onSubmitEditing 함수를 passwordRef를 이용해 비밀번호를 입력하는 Input 컴포넌트로 포커스가 이동되도록 수정.

 

 

키보드 감추기

  • TouchableWithoutFeedback : 클릭에 대해 상호작용은 하지만 스타일 속성이 없고 반드시 하나의 자식 컴포넌트를 가져야한다ㅏ.
  • Keyboard API : 리액트 네이티브에서 제공하는 키보드 관련 API로 키보드 상태에 따른 이벤트 등록에 많이 사용하며, dismiss 함수는 활성화된 키보드를 닫는 기능이다.
npm install react-native-keyboard-aware-scroll-view

react-native-keyboard-aware-scroll-view 라이브러리를 이용해 포커스가 있는 TextInput 컴포넌트의 위치로 자동 스크롤하는 기능 제공.

 

  • 입력 도중 다른 영역 터치 시 키보드가 사라짐
  • 포커스를 얻은 TextInput 컴포넌트의 위치에 맞춰 스크롤이 이동하는 것을 확인할 수 있다.
  • 스크롤 되는 위치를 조정하고 싶은 경우 extraScrollHeight 의 값을 조절해서 원하는 위치로 스크롤되도록 설정할 수 있다.

 

오류메시지

src/screens/Login.js

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 [errorMessage, setErrorMessage] = useState('');

    const _handleEmailChange = email => {
        const changedEmail = removeWhitespace(email);
        setEmail(changedEmail);
        setErrorMessage(
            validateEmail(changedEmail) ? '' : 'Please verify your email.'
        );
    };
    const _handlePasswordChange = password => {
        setPassword(removeWhitespace(password));
    };   
}
  • 이메일에는 공백이 존재하지 않으므로 email의 값이 변경될 때마다 공백을 제거하도록 수정
  • validateEmail 함수를 이용해 공백이 제거된 이메일이 올바른 형식인지 검사
  • 비밀번호도 공백허용하지 않는다.

 

src/components/Button.js

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;
`;
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 }) => {
    return(
        <Container style={containerStyle} onPress={onPress} isFilled={isFilled}>
            <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,
};

export default Button;
  • isFilled의 값에 따라 버튼 내부를 채우거나 투명하게 처리하는 Button 컴포넌트 생성
  • 버튼 내부가 채워지지 않았을 경우 props로 전달된 title의 색이 변경되도록 작성
  • 사용되는 곳에 따라 버튼의 스타일을 수정하기 위해 containerStyle을 props로 전달받아 적용하도록 작성.

 

<Button title="Login" onPress={_handleLoginButtonPress} />
  • Button 컴포넌트를 이용해 로그인과 회원가입 화면으로 이동하는 버튼 생성
  • 로그인 버튼 클릭시와 비밀번호 입력받는 Input 컴포넌트의 onSubmitEditing 함수의 역할이 같으므로 동일한 작업 수행.

 

  • Button 컴포넌트에서 props를 통해 전달되는 disabled의 값에 따라 버튼 스타일이 변경되도록 수정.
  • TouchableOpacity 컴포넌트에 disabled 속성을 전달하면 값에 따라 클릭 등의 상호작용이 동작하지 않는다.
  • disabled 값을 props로 전달하는 것으로 버튼 비활성화 기능 추가.

 

src/screens/Login.js

const Login = ({ navigation }) => {
    const [disabled, setDisabled] = useState(true);

    useEffect(() => {
        setDisabled(!(email && password && !errorMessage));
    }, [email, password, errorMessage]);
}

 

src/components/Button.js

const Button = ({ containerStyle, title, onPress, isFilled, disabled }) => {
    return(
        <Container 
            style={containerStyle} 
            onPress={onPress}
            isFilled={isFilled}
            disabled={disabled}
        >
            <Title isFilled={isFilled}>{title}</Title>
        </Container>
    );
};
  • useState를 사용해 버튼의 활성화 상태를 관리하는 disabled를 생성하고 useEffect를 이용해 email, password, errorMessage의 상태가 변할 때마다 조건에 맞게 disabled 상태가 변경되도록 작성.
  • 로그인 버튼은 이메일과 비밀번호가 입력되어 있고, 오류 메시지가 없는 상태에서만 활성화.
  • 로그인 버튼의 Button 컴포넌트에 disabled를 전달해서 값에 따라 버튼의 활성화여부 결정

 

현재 상태...

'dev > React native' 카테고리의 다른 글

[React Native] 채팅 애플리케이션(2)  (0) 2022.01.06
[React Native] Navigation  (0) 2021.11.21
[React Native] Context API  (0) 2021.11.14
[React Native] Hooks  (0) 2021.11.12
[React Native] 할 일 관리 애플리케이션(2)  (0) 2021.11.06