Post

firebase, react-native를 활용한 todos 앱

개요

1. 프로젝트 개요

  • 목적: 사용자가 글을 쉽게 작성하고 관리할 수 있는 메모 앱 개발
  • 플랫폼: 웹 및 모바일 (크로스 플랫폼 고려)
  • 기술 스택: Firebase (Firebase Authentication, Firebase Realtime Database), expo, react-native

2. 요구사항 분석

기능 요구사항

  • 사용자 인증: 이메일 및 소셜 미디어 로그인 지원
  • 글 작성: 사용자가 새로운 메모를 작성할 수 있어야 함
  • 글 수정: 작성된 메모를 수정할 수 있어야 함
  • 글 삭제: 사용자가 메모를 삭제할 수 있어야 함
  • 데이터 저장 및 관리: 모든 메모 데이터는 Firebase Realtime Database에 저장되며 실시간으로 동기화됨

비기능 요구사항

  • 보안: 사용자 데이터는 암호화되어 저장되어야 함
  • 접근성: 모든 사용자가 쉽게 접근하고 사용할 수 있어야 함
  • 반응형 디자인: 다양한 기기에서 원활하게 작동해야 함

3. 기능 명세

인증 관리

  • 로그인: 이메일/비밀번호 및 구글 계정을 이용한 로그인 기능
  • 회원가입: 이메일을 이용한 사용자 등록 기능
  • 로그아웃: 현재 로그인한 사용자의 로그아웃 기능

메모 관리

  • 메모 작성: 제목과 본문을 포함하는 메모 생성
  • 메모 조회: 사용자 별로 저장된 메모 리스트 보기
  • 메모 수정: 기존 메모의 제목 및 본문 수정
  • 메모 삭제: 선택한 메모 삭제

UI/UX

  • 메인 페이지: 사용자의 모든 메모가 리스트 형태로 표시
  • 메모 작성/수정 페이지: 메모 작성 및 수정을 위한 단순하고 직관적인 인터페이스
  • 로그인/회원가입 페이지: 사용자 인증을 위한 인터페이스

4. 데이터 모델

Firebase Realtime Database 구조

Firebase Realtime Database는 계층적 구조를 사용합니다. 데이터는 JSON 트리로 저장되며, 노드를 통해 접근.

  • Users: userID, email, passwordHash, creationDate
  • Notes: noteID, userID, title, content, creationDate, lastModifiedDate

파이어베이스 Realtime Database 모델링의 중요성

  • 구조화: 데이터는 JSON 트리 형태로 저장되며, 효율적이고 직관적인 계층적 구조를 유지해야 함.
  • 스키마리스(schemaless): 정해진 스키마가 없으므로 데이터 구조를 유연하게 설계할 수 있지만, 일관성을 유지해야 함.
  • 데이터 중복 방지: 관계형 데이터베이스와 달리, Realtime Database에서는 데이터 중복을 최소화하도록 구조를 설계해야 함.
  • 실시간 업데이트: 데이터의 실시간 동기화 및 업데이트를 염두에 두고 설계해야 함.

개발

초기화

1
expo init 프로젝트명
  • cmd에 입력해서 프로젝트 생성
  • choose a template는 blank로 선택
  • vscode에 들어가서 cmd에 npx expo start
  • 폰의 expo앱에서 같은 와이파이에 있을시 접속가능. qr코드 스캔해도 됨

개발시 오류

이미지(1)

1
<Image source= style={styles.image} />

이렇게는 안되고

1
<Image source= style={[styles.image, StyleSheet.absoluteFill]} />

이렇게는 됨

기본 이해

  • <Image> 컴포넌트: React Native에서 이미지를 표시하는 기본적인 컴포넌트. source 속성에 uri를 지정하여 네트워크 상의 이미지를 불러올 수 있다.
  • 스타일: 이미지의 크기 및 위치는 스타일에 의해 결정. 스타일을 지정하지 않으면 이미지는 기본적으로 크기 0인 박스로 표시되지 않는다.

문제 설명

<styles.image만 사용하는 경우>

1
<Image source= style={styles.image} />
  • 이 경우, styles.image가 제대로 정의되지 않거나, 크기나 위치가 올바르게 설정되지 않으면 이미지가 보이지 않을 수 있다.
  • 이미지를 표시하기 위해서는 스타일 속성에서 반드시 크기(width와 height)를 지정하거나, 부모 컨테이너가 크기를 제공해야 함.

<StyleSheet.absoluteFill을 추가한 경우>

1
2
<Image source= style={[styles.image, StyleSheet.absoluteFill]} />

  • StyleSheet.absoluteFill은 위치와 크기를 부모 컨테이너에 맞게 절대적으로 채우는 스타일 속성.
  • 이 속성을 추가하면, 이미지가 부모 컨테이너의 크기와 위치에 맞춰지기 때문에 화면에 표시될 수 있다.

< StyleSheet.absoluteFill의 역할>

StyleSheet.absoluteFill은 다음과 같은 스타일을 자동으로 적용함:

1
2
3
4
5
6
7
const absoluteFillStyle = {
  position: 'absolute',
  left: 0,
  right: 0,
  top: 0,
  bottom: 0,
};
  • position: 'absolute': 부모 컨테이너를 기준으로 절대 위치를 지정.
  • left: 0, right: 0, top: 0, bottom: 0: 부모 컨테이너의 모든 가장자리에 0 값을 줌으로써, 부모 컨테이너를 꽉 채우도록 설정.

따라서, StyleSheet.absoluteFill을 추가하면 이미지가 부모 컨테이너의 크기와 일치하게 되어 화면에 표시될 수 있다.

해결 방법

방법 1: 명시적으로 스타일 크기 지정

이미지가 표시되지 않는 문제는 styles.image에 명시적으로 크기를 지정하지 않아서 발생할 수 있다. 스타일에 width와 height를 명확하게 지정해주기.

1
2
3
4
5
6
7
const styles = StyleSheet.create({
  image: {
    width: 100, // 원하는 너비
    height: 100, // 원하는 높이
  },
});

1
2
<Image source= style={styles.image} />

방법 2: 부모 컨테이너의 크기에 맞추기

부모 컨테이너의 크기에 맞춰서 이미지가 표시되게 하려면 StyleSheet.absoluteFill을 사용할 수 있다. 이 방법은 이미지가 부모 컨테이너를 꽉 채우도록 함.

1
2
3
4
5
6
const styles = StyleSheet.create({
  image: {
    // width와 height를 생략하고 부모 컨테이너에 맞추기
  },
});

1
2
<Image source= style={[styles.image, StyleSheet.absoluteFill]} />

결론

  • 명시적 크기 지정: styles.image에 width와 height를 명시적으로 지정하면, 이미지가 해당 크기로 표시됨.
  • 부모 크기에 맞추기: StyleSheet.absoluteFill을 사용하면, 이미지는 부모 컨테이너를 완전히 채우게 되어 크기를 자동으로 조정할 수 있다.

어떤 접근 방식을 사용할지는 해당 이미지가 어떻게 표시되기를 원하는지에 따라 다르다. 대부분의 경우, 명시적인 크기 지정을 추천하지만, 부모 컨테이너의 크기를 기준으로 이미지를 표시하려는 경우에는 StyleSheet.absoluteFill이 유용할 수 있다.


이미지(2)

프로필이미지를 기본 아래와 같이 해서 경로만 줬더니 이미지가 업데이트가 안됨

1
2
3
 const profilePicture = 'https://randomuser.me/api/portraits/men/34.jpg';
 
 <Image source={ { uri: profilePicture }} style={styles.profilePicture} />

아래와 같이 업데이트 하니까 이미지가 표시가 됨

1
2
const profilePicture = require('../assets/person.png');
<Image source= style={[styles.image, StyleSheet.absoluteFill]} />

차이점

React Native에서 로컬 이미지를 사용하는 두 가지 주요 방식은 require를 사용하는 방식과 { uri } 형식을 사용하는 방식. 이 두 가지 방식은 다르게 작동하며, 각각의 경우에 가장 적합한 용도가 있다.

require 사용 (require() statement for local images)

특징

  • 로컬 파일: 앱에 포함된 로컬 이미지를 가져온다.
  • 정적 리소스: 이미지 경로는 컴파일 타임에 결정.
  • 번들링: 이미지는 빌드 시에 번들에 포함되므로, 번들된 모든 이미지 파일은 앱의 크기에 영향을 미친다.
  • 미리 로드: 이미지를 미리 로드하므로, 로드 성능이 더 빠르다.

사용 예시

1
2
3
const profilePicture = require('../assets/image.png');
<Image source={profilePicture} style={styles.profilePicture} />

{ uri } 형식 사용 ({ uri } format for remote images)

특징

  • 원격 파일: 인터넷에 있는 원격 이미지를 가져옴.
  • 동적 리소스: 이미지는 실행 시점에 결정되며, URL이 필요.
  • 지연 로드: 이미지는 URL을 통해 로드되므로 네트워크 상태에 따라 로드 속도가 달라질 수 있다.
  • 메모리 효율: 원격 이미지를 사용하는 경우, 번들 크기에 영향을 주지 않으며 네트워크 연결을 통해 이미지를 불러온다.

사용 예시

1
2
const uri = 'https://example.com/image.png';
<Image source= style={styles.profilePicture} />

주요 차이점

  • 이미지 출처:
    • require: 로컬 파일 시스템에서 이미지를 가져옵니다. 앱 내부의 파일만 사용할 수 있다.
    • { uri }: 원격 서버에서 이미지를 가져온다. URL을 통해 인터넷에 있는 모든 이미지를 사용할 수 있다.
  • 경로 결정 시점:
    • require: 경로는 컴파일 타임에 결정. 이미지가 번들에 포함.
    • { uri }: 경로는 런타임에 결정. URL을 통해 이미지를 로드.
  • 로드 성능:
    • require: 앱 실행 시 이미지가 미리 로드되므로 성능이 더 빠를 수 있다.
    • { uri }: 네트워크 상태에 따라 이미지 로드 시간이 달라질 수 있다.
  • 유연성:
    • require: 정적 경로만 허용되며 동적 경로는 사용할 수 없다.
    • { uri }: 동적 경로를 사용할 수 있어 유저가 프로필 사진을 변경하거나 서버에서 이미지를 동적으로 불러오는 경우에 유리.

언제 사용해야 하나?

  • 정적 로컬 이미지: 앱과 함께 제공되는 고정된 이미지를 사용할 때 require를 사용. 예를 들어, 앱의 아이콘, 기본 배경 이미지 등이 이에 해당.
  • 동적 원격 이미지: 사용자가 프로필 사진을 업로드하거나, 서버에서 동적으로 이미지를 불러올 때 { uri } 형식을 사용.

Auth 경고(1)

1
2
3
4
5
6
7
8
9
10
11
12
WARN  [2024-06-23T05:07:42.578Z]  @firebase/auth: Auth (10.12.2):
You are initializing Firebase Auth for React Native without providing
AsyncStorage. Auth state will default to memory persistence and will not
persist between sessions. In order to persist auth state, install the package
"@react-native-async-storage/async-storage" and provide it to
initializeAuth:

import { initializeAuth, getReactNativePersistence } from 'firebase/auth';
import ReactNativeAsyncStorage from '@react-native-async-storage/async-storage';
const auth = initializeAuth(app, {
persistence: getReactNativePersistence(ReactNativeAsyncStorage)
});]

이런 경고문이 나옴

해결

React Native 환경에서 Firebase 인증 지속성을 올바르게 처리하려면 경고 메시지에 제공된 권장 사항을 따라야 함. 여기에는 AsyncStorageReact Native용 으로 Firebase 인증을 설정하는 작업이 포함됨.

이 문제를 해결하는 방법 :

AsyncStorageFirebase 인증 지속성을 위한 통합 단계

  • Install@react-native-async-storage/async-storage : 이 패키지는 Firebase에서 인증 상태를 저장하는 데 사용할 수 있는 암호화되지 않은 영구 비동기 저장소를 제공.
1
2
3
npm install @react-native-async-storage/async-storage
# or
yarn add @react-native-async-storage/async-storage
  • firebase-config.js사용하도록 수정AsyncStorage**AsyncStorage : 인증 상태 지속성에 사용할 수 있도록 Firebase 구성을 업데이트.

업데이트된 파일 : firebase-config.js.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// firebase-config.js
import { initializeApp } from 'firebase/app';
import { initializeAuth, getAuth, getReactNativePersistence } from 'firebase/auth';
import { getDatabase } from 'firebase/database';
import { getFirestore } from 'firebase/firestore';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { 
  FIREBASE_API_KEY, 
  FIREBASE_AUTH_DOMAIN, 
  FIREBASE_PROJECT_ID, 
  FIREBASE_STORAGE_BUCKET, 
  FIREBASE_MESSAGING_SENDER_ID, 
  FIREBASE_APP_ID, 
  FIREBASE_MEASUREMENT_ID 
} from '@env';

// Firebase configuration object
export const firebaseConfig = {
  apiKey: FIREBASE_API_KEY,
  authDomain: FIREBASE_AUTH_DOMAIN,
  projectId: FIREBASE_PROJECT_ID,
  storageBucket: FIREBASE_STORAGE_BUCKET,
  messagingSenderId: FIREBASE_MESSAGING_SENDER_ID,
  appId: FIREBASE_APP_ID,
  measurementId: FIREBASE_MEASUREMENT_ID
};

// Initialize Firebase app
const app = initializeApp(firebaseConfig);

// Initialize Firebase Auth with persistence
const auth = initializeAuth(app, {
  persistence: getReactNativePersistence(AsyncStorage)
});

// Initialize other Firebase services
const db = getDatabase(app);
const firestore = getFirestore(app);

// Export initialized services
export { auth, db, firestore };

환경변수

babel.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// babel.config.js
module.exports = function(api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: [
      [
        'module:react-native-dotenv',
        {
          moduleName: '@env',
          path: '.env',
          blocklist: null,
          allowlist: null,
          safe: false,
          allowUndefined: true,
          verbose: false,
        },
      ],
    ],
  };
};

firebase-config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// firebase-config.js
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
import { getDatabase } from 'firebase/database';
import { FIREBASE_API_KEY, FIREBASE_AUTH_DOMAIN, FIREBASE_PROJECT_ID, FIREBASE_STORAGE_BUCKET, FIREBASE_MESSAGING_SENDER_ID, FIREBASE_APP_ID, FIREBASE_MEASUREMENT_ID } from '@env';

// Firebase 구성 정보
export const firebaseConfig = {
  apiKey: FIREBASE_API_KEY,
  authDomain: FIREBASE_AUTH_DOMAIN,
  projectId: FIREBASE_PROJECT_ID,
  storageBucket: FIREBASE_STORAGE_BUCKET,
  messagingSenderId: FIREBASE_MESSAGING_SENDER_ID,
  appId: FIREBASE_APP_ID,
  measurementId: FIREBASE_MEASUREMENT_ID
};

// Firebase 초기화
export const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getDatabase(app);

.env

1
2
3
4
5
6
7
FIREBASE_API_KEY=FIREBASE_API_KEY
FIREBASE_AUTH_DOMAIN=FIREBASE_AUTH_DOMAIN
FIREBASE_PROJECT_ID=FIREBASE_PROJECT_ID
FIREBASE_STORAGE_BUCKET=FIREBASE_STORAGE_BUCKET
FIREBASE_MESSAGING_SENDER_ID=FIREBASE_MESSAGING_SENDER_ID
FIREBASE_APP_ID=FIREBASE_APP_ID
FIREBASE_MEASUREMENT_ID=FIREBASE_MEASUREMENT_ID

expo에서는

import ‘dotenv/config’;

npm uninstall dotenv

npm install react-native-dotenv

이렇게 하고 위에처럼 해야 오류가 안남

완료 이미지

이미지1

이미지2

깃허브 코드

코드 깃허브 링크

This post is licensed under CC BY 4.0 by the author.