React Native/React Native_study

[리액트 네이티브를 다루는 기술 #22] 9장 Firebase로 사진 공유 앱 만들기2 (p .529 ~ 558)

bocoder
728x90
반응형

@ List

* 9.5 사용자 프로필 화면 구현하기

** 9.5.1 Firestore 데이터 조회할 때 조건 추가하기

** 9.5.2 포스트 조회 함수 리팩토링하기

** 9.5.3 Firestore에서 색인 추가하기

** 9.5.4 Profile 컴포넌트 만들기

** 9.5.5 그리드 뷰 만들기

** 9.5.6 페이지네이션 구현하기

** 9.5.7 커스텀 Hook을 작성해 컴포넌트 리팩토링하기

** 9.5.8 포스트 열기

** 9.5.9 내 프로필 화면 구현하기

 

@ Note

1. default parameter 문법

 - 파라미터가 주어지지 않았다면 기본값으로 빈 객체 {} 를 사용하도록 함

 - 설정하지 않을 시 구조 분해 과정에서 오류 발생

export async function getPosts({userId, mode, id} = {}) {
  ...  
  return posts;
}

 

2. Firestore에서 색인 추가하기

 - 특정 사용자의 포스트를 조회하는 과정에서 user.id 를 찾아서 조회하고 createdAt을 내림차순으로 정렬하고 있는데, 이렇게 특정 조건이 붙고 특정 속성으로 정렬할 때는 Firestore에서 색인(index)을 추가해야 함

 

3. margin 관련 참고 사항

 - 프로필 화면에서 한 열에 3개의 항목이 보여지고, 각 컴포넌트에 0.5의 여백을 주면 추가한 여백이차지하는 크기는 총 3px (컴포넌트 사이에 1px, 화면의 양 끝에 0.5px) 이므로, dimensions.width 에서 3px을 차감함

...

function PostGridItem({post}) {
  const size = (dimensions.width - 3) / 3;
  ...
}

const styles = StyleSheet.create({
  block: {margin: 0.5},
  ...
});

export default PostGridItem;

 

4. 커스텀 Hook 작성을 통한 리팩토링

 - 컴포넌트에서 비슷한 UI 가 반복된다면 컴포넌트를 분리해 재사용하면 되고, 컴포넌트의 로직만 반복된다면 커스텀 Hook을 만들어서 로직을 재사용함

 - 컴포넌트와의 차이점으로 커스텀 Hook은 JSX를 반환하지 않음

...

export default function usePosts(userId) {
  ...
  return {posts, noMorePost, refreshing, onLoadMore, onRefresh};
}

 

5. 셀렉터 함수 (selector function)

 - 상태에서 어떤 값을 조회할지 정하는 함수

...

function PostCard({user, photoURL, description, createdAt, id}) {
  const routeNames0 = useNavigationState(state => state); // 전체 상태 조회
  console.log(routeNames0); // {"index": 0, "key": "stack-...", "routeNames": ["Feed", "Profile", "Post"], "routes": [{"key": "Feed-...", "name": "Feed", "params": undefined}], "stale": false, "type": "stack"} or ...

  const routeNames = useNavigationState(state => state.routeNames); // 객체 내부의 routeNames 값만 조회
  console.log(routeNames); // ["Feed", "Profile", "Post"] or ["MyProfile", "Post"]
  
  ...
  }

...

 

@ Result

0123
iOS / android - 사용자 프로필 조회 구현

 

iOS / android - 신규로 등록한 프로필 사진 가져오기

 

iOS / android - 본인 프로필 화면 구현

 

01
iOS / android - 본인 게시물 클릭 시 네비게이터 수정

 

iOS / android - 본인 게시물의 프로필 클릭 시 뒤로가기 구현

 

@ Git

 - https://github.com/eunbok-bocoder/PublicGalleryBocoder/commits/main

 

# Source tree

* lib > _posts.js : 기존 소스 참고용으로 주석 처리

 

# components > Avatar.js

import React from 'react';
import {Image} from 'react-native';

function Avatar({source, size, style}) {
  return (
    <Image
      source={source || require('../assets/user.png')}
      resizeMode="cover"
      style={[
        style,
        {
          width: size,
          height: size,
          borderRadius: size / 2,
        },
      ]}
    />
  );
}

Avatar.defaultProps = {
  size: 32,
};

export default Avatar;

 

# components > PostCard.js

import React, {useMemo} from 'react';
import {View, StyleSheet, Text, Image, Pressable} from 'react-native';
import Avatar from './Avatar';
import {useNavigation, useNavigationState} from '@react-navigation/native';

function PostCard({user, photoURL, description, createdAt, id}) {
  const routeNames = useNavigationState(state => state.routeNames);
  // console.log(routeNames); // ["Feed", "Profile", "Post"] or ["MyProfile", "Post"]

  const date = useMemo(
    () => (createdAt ? new Date(createdAt._seconds * 1000) : new Date()),
    [createdAt],
  );

  const navigation = useNavigation();

  const onOpenProfile = () => {
    // MyProfile이 존재하는지 확인
    if (routeNames.find(routeName => routeName === 'MyProfile')) {
      navigation.navigate('MyProfile');
    } else {
      navigation.navigate('Profile', {
        userId: user.id,
        displayName: user.displayName,
      });
    }
  };

  return (
    <View style={styles.block}>
      <View style={[styles.head, styles.paddingBlock]}>
        <Pressable style={styles.profile} onPress={onOpenProfile}>
          <Avatar source={user.photoURL && {uri: user.photoURL}} />
          <Text style={styles.displayName}>{user.displayName}</Text>
        </Pressable>
      </View>
      <Image
        source={{uri: photoURL}}
        style={styles.image}
        resizeMethod="resize"
        resizeMode="cover"
      />
      <View style={styles.paddingBlock}>
        <Text style={styles.description}>{description}</Text>
        <Text date={date} style={styles.date}>
          {date.toLocaleDateString()}
        </Text>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  block: {
    paddingTop: 16,
    paddingBottom: 16,
  },
  paddingBlock: {
    paddingHorizontal: 16,
  },
  head: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    marginBottom: 16,
  },
  profile: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  displayName: {
    lineHeight: 16,
    fontSize: 16,
    marginLeft: 8,
    fontWeight: 'bold',
  },
  image: {
    backgroundColor: '#bdbdbd',
    width: '100%',
    aspectRatio: 1,
    marginBottom: 16,
  },
  description: {
    fontSize: 16,
    lineHeight: 24,
    marginBottom: 8,
  },
  date: {
    color: '#757575',
    fontSize: 12,
    lineHeight: 18,
  },
});

export default PostCard;

 

# components > PostGridItem.js

import {useNavigation} from '@react-navigation/native';
import React from 'react';
import {StyleSheet, useWindowDimensions, Image, Pressable} from 'react-native';

function PostGridItem({post}) {
  const dimensions = useWindowDimensions();
  const size = (dimensions.width - 3) / 3;
  const navigation = useNavigation();

  const onPress = () => {
    // TODO: 단일 포스트 조회 화면 띄우기
    navigation.navigate('Post', {post});
  };

  return (
    <Pressable
      onPress={onPress}
      style={({pressed}) => [
        {
          opacity: pressed ? 0.6 : 1,
          width: size,
          height: size,
        },
        styles.block,
      ]}>
      <Image
        style={styles.image}
        source={{uri: post.photoURL}}
        resizeMethod="resize"
        resizeMode="cover"
      />
    </Pressable>
  );
}

const styles = StyleSheet.create({
  block: {margin: 0.5},
  image: {
    backgroundColor: '#bdbdbd',
    width: '100%',
    height: '100%',
  },
});

export default PostGridItem;

 

# components > Profile.js

import React, {useEffect, useState} from 'react';
import {
  ActivityIndicator,
  FlatList,
  StyleSheet,
  Text,
  View,
  RefreshControl,
} from 'react-native';
import {getUser} from '../lib/users';
import Avatar from './Avatar';
import PostGridItem from './PostGridItem';
// import {getNewerPosts, getOlderPosts, getPosts, PAGE_SIZE} from '../lib/posts';
import usePosts from '../hooks/usePosts';

function Profile({userId}) {
  const [user, setUser] = useState(null);

  const {posts, noMorePost, refreshing, onLoadMore, onRefresh} =
    usePosts(userId);

  // const [posts, setPosts] = useState(null);

  // const [noMorePost, setNoMorePost] = useState(false);
  // const [refreshing, setRefreshing] = useState(false);

  // const onLoadMore = async () => {
  //   if (noMorePost || !posts || posts.length < PAGE_SIZE) {
  //     return;
  //   }

  //   const lastPost = posts[posts.length - 1];
  //   const olderPosts = await getOlderPosts(lastPost.id, userId);

  //   if (olderPosts.length < PAGE_SIZE) {
  //     setNoMorePost(true);
  //   }
  //   setPosts(posts.concat(olderPosts));
  // };

  // const onRefresh = async () => {
  //   if (!posts || posts.length === 0 || refreshing) {
  //     return;
  //   }
  //   const firestPost = posts[0];
  //   setRefreshing(true);
  //   const newerPosts = await getNewerPosts(firestPost.id, userId);
  //   setRefreshing(false);
  //   if (newerPosts.length === 0) {
  //     return;
  //   }
  //   setPosts(newerPosts.concat(posts));
  // };

  useEffect(() => {
    getUser(userId).then(setUser);
    // getPosts({userId}).then(setPosts);
  }, [userId]);

  if (!user || !posts) {
    return (
      <ActivityIndicator style={styles.spinner} size={32} color="#6200ee" />
    );
  }

  return (
    <FlatList
      style={styles.block}
      data={posts}
      renderItem={renderItem}
      numColumns={3}
      keyExtractor={item => item.id}
      ListHeaderComponent={
        <View style={styles.userInfo}>
          <Avatar source={user.photoURL && {uri: user.photoURL}} size={128} />
          <Text styles={styles.username}>{user.displayName}</Text>
        </View>
      }
      onEndReached={onLoadMore}
      onEndReachedThreshold={0.25}
      ListFooterComponent={
        !noMorePost && (
          <ActivityIndicator
            style={styles.bottomSpinner}
            size={32}
            color="#6200ee"
          />
        )
      }
      refreshControl={
        <RefreshControl onRefresh={onRefresh} refreshing={refreshing} />
      }
    />
  );
}

const renderItem = ({item}) => <PostGridItem post={item} />;

const styles = StyleSheet.create({
  spinner: {
    flex: 1,
    justifyContent: 'center',
  },
  block: {
    flex: 1,
  },
  userInfo: {
    paddingTop: 80,
    paddingBottom: 64,
    alignItems: 'center',
  },
  username: {
    matginTop: 8,
    fontSize: 24,
    color: '#424242',
  },
  bottomSpineer: {
    height: 128,
  },
});

export default Profile;

 

# components > SetupProfile.js

import {useNavigation, useRoute} from '@react-navigation/core';
import React, {useState} from 'react';
import {
  Image,
  Pressable,
  StyleSheet,
  View,
  Platform,
  ActivityIndicator,
} from 'react-native';
import {signOut} from '../lib/auth';
import {createUser} from '../lib/users';
import BorderedInput from './BorderedInput';
import CustomButton from './CustomButton';
import {useUserContext} from '../contexts/UserContext';
import {launchImageLibrary} from 'react-native-image-picker';
import storage from '@react-native-firebase/storage';
import Avatar from './Avatar';

function SetupProfile() {
  const [displayName, setDisplayName] = useState('');
  const navigation = useNavigation();
  const {setUser} = useUserContext();
  const {params} = useRoute();
  const {uid} = params || {};

  const [response, setResponse] = useState(null);
  const [loading, setLoading] = useState(false);

  const onSubmit = async () => {
    setLoading(true);

    let photoURL = null;

    if (response) {
      const asset = response.assets[0];
      const extension = asset.fileName.split('.').pop(); // 확장자 추출
      const reference = storage().ref(`/profile/${uid}.${extension}`);

      if (Platform.OS === 'android') {
        await reference.putString(asset.base64, 'base64', {
          contentType: asset.type,
        });
      } else {
        await reference.putFile(asset.uri);
      }
      photoURL = response ? await reference.getDownloadURL() : null;
    }

    const user = {
      id: uid,
      displayName,
      photoURL,
    };

    createUser(user);
    setUser(user);
  };

  const onCancel = () => {
    signOut();
    navigation.goBack();
  };

  const onSelectImage = () => {
    launchImageLibrary(
      {
        mediaType: 'photo',
        maxWidth: 512,
        maxHeight: 512,
        // android의 경우 uri에서 직접 파일을 읽는 과정에서 권한 오류가 발생 할 수 있기 때문에 설정
        includeBase64: Platform.OS === 'android',
      },
      res => {
        if (res.didCancel) {
          //취소했을 경우
          return;
        }
        // console.log(res);
        setResponse(res);
      },
    );
  };

  return (
    <View style={styles.block}>
      <Pressable onPress={onSelectImage}>
        <Avatar
          source={response && {uri: response.assets[0].uri}}
          style={styles.circle}
          size={128}
        />
      </Pressable>
      <View style={styles.form}>
        <BorderedInput
          placeholder="닉네임"
          value={displayName}
          onChangeText={setDisplayName}
          onSubmitEditing={onSubmit}
          returnKeyType="next"
        />
        {loading ? (
          <ActivityIndicator size={32} color="#6200ee" style={styles.spinner} />
        ) : (
          <View style={styles.buttons}>
            <CustomButton title="다음" onPress={onSubmit} hasMarginbottom />
            <CustomButton title="취소" onPress={onCancel} theme="secondary" />
          </View>
        )}
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  block: {
    alignItems: 'center',
    marginTop: 24,
    paddingHorizontal: 16,
    width: '100%',
  },
  circle: {
    backgroundColor: '#cdcdcd',
    borderRadius: 64,
    width: 128,
    height: 128,
  },
  form: {
    marginTop: 16,
    width: '100%',
  },
  buttons: {
    marginTop: 48,
  },
});

export default SetupProfile;

 

# hooks > usePosts.js

import {useEffect, useState} from 'react';
import {getNewerPosts, getOlderPosts, getPosts, PAGE_SIZE} from '../lib/posts';

export default function usePosts(userId) {
  const [posts, setPosts] = useState(null);
  const [noMorePost, setNoMorePost] = useState(false);
  const [refreshing, setRefreshing] = useState(false);

  const onLoadMore = async () => {
    if (noMorePost || !posts || posts.length < PAGE_SIZE) {
      return;
    }

    const lastPost = posts[posts.length - 1];
    const olderPosts = await getOlderPosts(lastPost.id, userId);

    if (olderPosts.length < PAGE_SIZE) {
      setNoMorePost(true);
    }
    setPosts(posts.concat(olderPosts));
  };

  const onRefresh = async () => {
    if (!posts || posts.length === 0 || refreshing) {
      return;
    }
    const firstPost = posts[0];
    setRefreshing(true);
    const newerPosts = await getNewerPosts(firstPost.id, userId);
    setRefreshing(false);
    if (newerPosts.length === 0) {
      return;
    }
    setPosts(newerPosts.concat(posts));
  };

  useEffect(() => {
    getPosts({userId}).then(_posts => {
      setPosts(_posts);
      if (_posts.length < PAGE_SIZE) {
        setNoMorePost(true);
      }
    });
  }, [userId]);

  return {posts, noMorePost, refreshing, onLoadMore, onRefresh};
}

 

# lib > _posts.js

//before refactoring

import firestore from '@react-native-firebase/firestore';

const postsCollection = firestore().collection('posts');

// 포스트 생성
export function createPost({user, photoURL, description}) {
  return postsCollection.add({
    user,
    photoURL,
    description,
    createdAt: firestore.FieldValue.serverTimestamp(),
  });
}

export const PAGE_SIZE = 12;

// 포스트 불러오기
export async function getPosts(userId) {
  // const snapshot = await postsCollection
  //   .orderBy('createdAt', 'desc') // 시간순 내림차순 정렬
  //   .limit(PAGE_SIZE) // 페이지 수 제한
  //   .get();

  let query = postsCollection.orderBy('createdAt', 'desc').limit(PAGE_SIZE);
  if (userId) {
    query = query.where('user.id', '==', userId);
  }
  const snapshot = await query.get();

  const posts = snapshot.docs.map(doc => ({
    id: doc.id, // key 추가
    ...doc.data(),
  }));

  return posts;
}

// 이전 포스트 불러오기
export async function getOlderPosts(id, userId) {
  const cursorDoc = await postsCollection.doc(id).get();
  // const snapshot = await postsCollection
  //   .orderBy('createdAt', 'desc')
  //   .startAfter(cursorDoc) //  특정 포스트의 id 값 이전에 작성한 포스트를 불러옴
  //   .limit(PAGE_SIZE)
  //   .get();

  let query = postsCollection
    .orderBy('createdAt', 'desc')
    .startAfter(cursorDoc)
    .limit(PAGE_SIZE);
  if (userId) {
    query = query.where('user.id', '==', userId);
  }
  const snapshot = await query.get();

  const posts = snapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data(),
  }));

  return posts;
}

// 최신 포스트 불러오기
export async function getNewerPosts(id) {
  const cursorDoc = await postsCollection.doc(id).get();
  // const snapshot = await postsCollection
  //   .orderBy('createdAt', 'desc')
  //   .endBefore(cursorDoc) //  특정 포스트의 id 값 이후에 작성한 포스트를 불러옴
  //   .limit(PAGE_SIZE)
  //   .get();

  let query = postsCollection
    .orderBy('createdAt', 'desc')
    .endBefore(cursorDoc)
    .limit(PAGE_SIZE);
  if (userId) {
    query = query.where('user.id', '==', userId);
  }
  const snapshot = await query.get();

  const posts = snapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data(),
  }));

  return posts;
}

 

# lib > posts.js

import firestore from '@react-native-firebase/firestore';

const postsCollection = firestore().collection('posts');

// 포스트 생성
export function createPost({user, photoURL, description}) {
  return postsCollection.add({
    user,
    photoURL,
    description,
    createdAt: firestore.FieldValue.serverTimestamp(),
  });
}

export const PAGE_SIZE = 12;

// 포스트 불러오기
export async function getPosts({userId, mode, id} = {}) {
  let query = postsCollection.orderBy('createdAt', 'desc').limit(PAGE_SIZE);

  if (userId) {
    query = query.where('user.id', '==', userId);
  }

  if (id) {
    const cursorDoc = await postsCollection.doc(id).get();
    query =
      mode === 'older'
        ? query.startAfter(cursorDoc)
        : query.endBefore(cursorDoc);
  }

  const snapshot = await query.get();
  const posts = snapshot.docs.map(doc => ({
    id: doc.id, // key 추가
    ...doc.data(),
  }));
  return posts;
}

// 이전 포스트 불러오기
export async function getOlderPosts(id, userId) {
  return getPosts({
    id,
    mode: 'older',
    userId,
  });
}

// 최신 포스트 불러오기
export async function getNewerPosts(id, userId) {
  return getPosts({
    id,
    mode: 'newer',
    userId,
  });
}

 

# screens > FeedScreen.js

import React, {useEffect, useState} from 'react';
import {
  ActivityIndicator,
  FlatList,
  RefreshContol,
  RefreshControl,
  StyleSheet,
} from 'react-native';
import PostCard from '../components/PostCard';
import {getNewerPosts, getOlderPosts, getPosts, PAGE_SIZE} from '../lib/posts';
import usePosts from '../hooks/usePosts';

function FeedScreen() {
  // const [posts, setPosts] = useState(null);
  // // 마지막 포스트까지 조회했음을 명시하는 상태
  // const [noMorePost, setNoMorePost] = useState(false);
  // // refreshing 이 true 일때만 새로고침 표시
  // const [refreshing, setRefreshing] = useState(false);

  // useEffect(() => {
  //   // 컴포넌트가 처음 마운트될 때 포스트 목록을 조회한 후 'posts' 상태에 담기
  //   getPosts().then(setPosts);
  //   // 동일한 표현
  //   // getPosts().then(res => {
  //   //   setPosts(res);
  //   // });
  // }, []);

  // const onLoadMore = async () => {
  //   if (noMorePost || !posts || posts.length < PAGE_SIZE) {
  //     return;
  //   }
  //   const lastPost = posts[posts.length - 1];
  //   const olderPosts = await getOlderPosts(lastPost.id);
  //   if (olderPosts.length < PAGE_SIZE) {
  //     setNoMorePost(true);
  //   }
  //   setPosts(posts.concat(olderPosts));
  // };

  // const onRefresh = async () => {
  //   if (!posts || posts.length === 0 || refreshing) {
  //     return;
  //   }
  //   const firstPost = posts[0];
  //   setRefreshing(true);
  //   const newerPosts = await getNewerPosts(firstPost.id);
  //   setRefreshing(false);
  //   if (newerPosts.length === 0) {
  //     return;
  //   }
  //   setPosts(newerPosts.concat(posts));
  // };

  const {posts, noMorePost, refreshing, onLoadMore, onRefresh} = usePosts();

  return (
    <FlatList
      data={posts}
      renderItem={renderItem}
      keyExtractor={item => item.id}
      contentContainerStyle={styles.container}
      onEndReached={onLoadMore}
      onEndReachedThreshold={0.75} // 출발지(0) ~ 끝지점(1), 설정값에 도달 시 onEndReached 실행
      ListFooterComponent={
        // 모든 항목의 맨 아래 구현할 값
        !noMorePost && (
          <ActivityIndicator style={styles.spinner} size={32} color="6200ee" />
        )
      }
      refreshControl={
        <RefreshControl onRefresh={onRefresh} refreshing={refreshing} />
      }
    />
  );
}

const renderItem = ({item}) => (
  <PostCard
    createdAt={item.createdAt}
    description={item.description}
    id={item.id}
    user={item.user}
    photoURL={item.photoURL}
  />
);

const styles = StyleSheet.create({
  container: {
    paddingBottom: 48,
  },
  spinner: {
    height: 64,
  },
});

export default FeedScreen;

 

# screens > HomeStack.js

import React from 'react';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import FeedScreen from './FeedScreen';
import ProfileScreen from './ProfileScreen';
import PostScreen from './PostScreen';

const Stack = createNativeStackNavigator();

function HomeStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Feed" component={FeedScreen} />
      <Stack.Screen name="Profile" component={ProfileScreen} />
      <Stack.Screen
        name="Post"
        component={PostScreen}
        options={{title: '게시물'}}
      />
    </Stack.Navigator>
  );
}

export default HomeStack;

 

# screens > MyProfileScreen.js

import {useNavigation} from '@react-navigation/native';
import React from 'react';
import {useEffect} from 'react';

import Profile from '../components/Profile';
import {useUserContext} from '../contexts/UserContext';

function MyProfileScreen() {
  const {user} = useUserContext();
  const navigation = useNavigation();

  useEffect(() => {
    navigation.setOptions({
      title: user.displayName,
    });
  }, [navigation, user]);
  return <Profile userId={user.id} />;
}

export default MyProfileScreen;

 

# screens > MyProfileStack.js

import React from 'react';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import MyProfileScreen from './MyProfileScreen';
import PostScreen from './PostScreen';

const Stack = createNativeStackNavigator();

function MyProfileStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="MyProfile" component={MyProfileScreen} />
      <Stack.Screen
        name="Post"
        component={PostScreen}
        options={{title: '게시물'}}
      />
    </Stack.Navigator>
  );
}

export default MyProfileStack;

 

# screens > PostScreen.js

import {useRoute} from '@react-navigation/native';
import React from 'react';
import {ScrollView, StyleSheet} from 'react-native';
import PostCard from '../components/PostCard';

function PostScreen() {
  const route = useRoute();
  const {post} = route.params;

  return (
    <ScrollView contentContainerStyle={StyleSheet.contentContainer}>
      <PostCard
        user={post.user}
        photoURL={post.photoURL}
        description={post.description}
        createdAt={post.createdAt}
        id={post.id}
      />
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  block: {flex: 1},
  contentContainer: {
    paddingBottom: 40,
  },
});

export default PostScreen;

 

# screens > ProfileScreen.js

import {useNavigation, useRoute} from '@react-navigation/native';
import React from 'react';
import {useEffect} from 'react';
import Profile from '../components/Profile';

function ProfileScreen() {
  const route = useRoute();
  const navigation = useNavigation();
  const {userId, displayName} = route.params ?? {};

  useEffect(() => {
    navigation.setOptions({
      title: displayName,
    });
  }, [navigation, displayName]);

  return <Profile userId={userId} />;
}

export default ProfileScreen;

 

# screens > SignInScreen.js

import React, {useState, useEffect} from 'react';
import {
  Alert,
  Keyboard,
  KeyboardAvoidingView,
  Platform,
  StyleSheet,
  Text,
  View,
} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import SignButtons from '../components/SignButton';
import SignForm from '../components/SignForm';
import {signIn, signUp} from '../lib/auth';
import {getUser} from '../lib/users';
import {useUserContext} from '../contexts/UserContext';

function SignInScreen({navigation, route}) {
  useEffect(() => {
    return () => setLoading(false); // unmounted 시에 cleanup 기능 (하단 finally{} 구문의 에러 디버깅)
  }, []);

  const {isSignUp} = route.params ?? {}; // null 병합 연산자
  const [form, setForm] = useState({
    email: '',
    password: '',
    confirmPassword: '',
  });

  const [loading, setLoading] = useState();
  const {setUser} = useUserContext();

  const createChangeTextHandler = name => value => {
    setForm({...form, [name]: value});
  };

  const onSubmit = async () => {
    Keyboard.dismiss();
    // console.log('[Log] form : ', form);

    const {email, password, confirmPassword} = form;

    if (isSignUp && password !== confirmPassword) {
      Alert.alert('실패', '비밀번호가 일치하지 않습니다.');
      return;
    }

    if (email == '') return Alert.alert('실패', '이메일을 입력해주세요.');
    else if (password == '')
      return Alert.alert('실패', '비밀번호를 입력해주세요.');

    setLoading(true);
    const info = {email, password};
    // console.log('[LOG] info : ', info);

    try {
      const {user} = isSignUp ? await signUp(info) : await signIn(info);
      console.log('[LOG] user : ', user);
      const profile = await getUser(user.uid);
      if (!profile) {
        console.log('[LOG] profile : ', profile); // "undefined"
        navigation.navigate('Welcome', {uid: user.uid});
      } else {
        console.log('[LOG] profile : ', profile);
        setUser(profile);
      }
    } catch (e) {
      const message = {
        'auth/email-already-in-use': '이미 가입된 이메일입니다.',
        'auth/wrong-password': '잘못된 비밀번호입니다.',
        'auth/user-not-found': '존재하지 않는 계정입니다.',
        'auth/invalid-email': '유효하지 않은 이메일 주소입니다.',
      };

      const msg = message[e.code] || `${isSignUp ? '가입' : '로그인'} 실패`;
      Alert.alert('실패', msg);
      console.log('[Log] error :', e);
    } finally {
      setLoading(false); // 에러발생 : "Can't perform a React state update on an unmounted component"
    }
  };

  return (
    <KeyboardAvoidingView
      style={styles.KeyboardAvoidingView}
      behavior={Platform.select({ios: 'padding'})}>
      <SafeAreaView style={styles.fullscreen}>
        <Text style={styles.text}>PublicGallery</Text>
        <View style={styles.form}>
          <SignForm
            isSignUp={isSignUp}
            onSubmit={onSubmit}
            form={form}
            createChangeTextHandler={createChangeTextHandler}
          />
          <SignButtons
            isSignUp={isSignUp}
            onSubmit={onSubmit}
            loading={loading}
          />
        </View>
      </SafeAreaView>
    </KeyboardAvoidingView>
  );
}

const styles = StyleSheet.create({
  KeyboardAvoidingView: {
    flex: 1,
  },
  fullscreen: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  text: {
    fontSize: 32,
    fontWeight: 'bold',
  },
  form: {
    marginTop: 64,
    width: '100%',
    paddingHorizontal: 16,
  },
  //   buttons: {
  //     marginTop: 64,
  //   },
});

export default SignInScreen;

 

728x90
반응형