Skip to content

πŸ’¬ React Native둜 λ§Œλ“  Chatting App

Notifications You must be signed in to change notification settings

ssi02014/React-Native-Chat

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

44 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ’» React-Native-Chat

React-Native-Chat μ €μž₯μ†Œ


πŸŽ₯ App View


πŸ“š 기술 μŠ€νƒ 및 μ£Όμš” 라이브러리

  1. React-Native
  2. Styled-Components: Styling
  3. Google Material Design: Icon
  4. React-Navigation(Stack, Tab)
  5. Context API: μƒνƒœ 관리
  6. Firebase: μ„œλΉ„μŠ€μ— ν•„μš”ν•œ μ„œλ²„μ™€ λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό 직접 κ΅¬μΆ•ν•˜μ§€ μ•Šκ³  개발이 κ°€λŠ₯ν•œ 개발 ν”Œλž«νΌ
  7. expo-image-picker: 기기의 μ‚¬μ§„μ΄λ‚˜ μ˜μƒμ„ κ°€μ Έμ˜¬ 수 μžˆλ„λ‘ μ‹œμŠ€ν…œ UI에 μ ‘κ·Όν•  수 μžˆλŠ” κΈ°λŠ₯을 제곡
  8. moment: μ‹œκ°„μ„ λ‹€μ–‘ν•œ ν˜•νƒœλ‘œ λ³€κ²½ν•˜λŠ” λ“± μ‹œκ°„κ³Ό κ΄€λ ¨λœ λ§Žμ€ κΈ°λŠ₯을 제곡
  9. react-native-keyboard-aware-scroll-view: ν‚€λ³΄λ“œκ°€ 화면을 κ°€λ¦¬λ©°μ„œ μƒκΈ°λŠ” λΆˆνŽΈν•œ 점을 ν•΄κ²°ν•  수 μžˆλŠ” κΈ°λŠ₯ 제곡
  10. react-native-gifted-chat: λ©”μ‹œμ§€λ₯Ό μ£Όκ³ λ°›λŠ” μ±„νŒ… 화면을 μ‰½κ²Œ κ΅¬ν˜„ν•  수 μžˆλ„λ‘ λ•λŠ” 라이브러리

πŸ“‚ App File Structure

1

  • components: μ»΄ν¬λ„ŒνŠΈ 파일 관리
  • contexts: Context API 파일 관리
  • navigations: λ‚΄λΉ„κ²Œμ΄μ…˜ 파일 관리
  • screens: ν™”λ©΄ 파일 관리
  • utils: ν”„λ‘œμ νŠΈμ—μ„œ μ΄μš©ν•  기타 κΈ°λŠ₯ 관리

πŸ‘¨πŸ»β€πŸ’» Firebase

πŸ”– https://console.firebase.google.com/

  • FirebaseλŠ” 인증(Authentication), λ°μ΄ν„°λ² μ΄μŠ€(Database) λ“±μ˜ λ‹€μ–‘ν•œ κΈ°λŠ₯을 μ œκ³΅ν•˜λŠ” 개발 ν”Œλž«νΌμ΄λ‹€.
  • Firebaseκ°€ μ œκ³΅ν•˜λŠ” κΈ°λŠ₯을 μ΄μš©ν•˜λ©΄ λŒ€λΆ€λΆ„μ˜ μ„œλΉ„μŠ€μ—μ„œ ν•„μš”ν•œ μ„œλ²„μ™€ λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό 직접 κ΅¬μΆ•ν•˜μ§€ μ•Šμ•„λ„ 개발이 κ°€λŠ₯ν•˜λ‹€.
    //Firebase Setting
    1. ν”„λ‘œμ νŠΈ μ„€μ • > 일반 > λ‚΄ μ•±μ—μ„œ 'μ›Ή'을 μ„ νƒν•˜κ³  앱을 μΆ”κ°€
    
    2. ν”„λ‘œμ νŠΈ μ„€μ • > 일반 > λ‚΄ μ•±μ—μ„œ 'Firebase SDK snippet'μ—μ„œ Firebase 섀정값을 ν™•μΈν•œλ‹€.

    3. ν”„λ‘œμ νŠΈ 루트 디렉터리에 firebase.json νŒŒμΌμ„ 생성 ν›„ 2λ²ˆμ—μ„œ ν™•μΈν•œ μ½”λ“œλ₯Ό λ„£λŠ”λ‹€.
         - firebase.json은 μ€‘μš”ν•œ 파일이기 λ•Œλ¬Έμ— .gitignore에 μΆ”κ°€ν•œλ‹€.

    //firebase.json
    {
        "apiKey": "...",
        "authDomain": "...",
        "projectId": "...",
        "storageBucket": "...",
        "messagingSenderId": "...",
        "appId": "...",
        "measurementId": "..."
    }

    4. 인증, λ°μ΄ν„°λ² μ΄μŠ€, μŠ€ν† λ¦¬μ§€ μ„€μ •ν•œλ‹€.

    5. expo install firebase λ₯Ό 톡해 라이브러리λ₯Ό μ„€μΉ˜ν•œλ‹€.

    6. firebase.js νŒŒμΌμ„ μƒμ„±ν•œλ‹€.

    //src/utils/firebase.js
    import * as firebase from "firebase";
    import config from "../../firebase.json";

    const app = firebase.initializeApp(config);

πŸ‘¨πŸ»β€πŸ’» μ•± μ•„μ΄μ½˜κ³Ό λ‘œλ”© ν™”λ©΄

  • ν”„λ‘œμ νŠΈμ—μ„œ μ‚¬μš©ν•  이미지와 폰트λ₯Ό 미리 λΆˆλŸ¬μ™€μ„œ μ‚¬μš©ν•  수 μžˆλ„λ‘ cacheImages, cacheFonts ν•¨μˆ˜λ₯Ό μž‘μ„±ν•˜κ³  이λ₯Ό _loadAssets ν•¨μˆ˜λ₯Ό κ΅¬μ„±ν–ˆλ‹€.
  • μ΄λ―Έμ§€λ‚˜ 폰트λ₯Ό 미리 뢈러였면 μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μ‚¬μš©ν•˜λŠ” ν™˜κ²½μ— 따라 μ΄λ―Έμ§€λ‚˜ ν°νŠΈκ°€ 느리게 μ μš©λ˜λŠ” 문제λ₯Ό κ°œμ„ ν•  수 μžˆλ‹€.
  • μ• ν”Œλ¦¬μΌ€μ…˜μ€ 미리 λΆˆλŸ¬μ™€μ•Ό ν•˜λŠ” ν•­λͺ©λ“€μ„ λͺ¨λ‘ 뢈러였고 화면이 λ Œλ”λ§ λ˜λ„λ‘ AppLoading μ»΄ν¬λ„ŒνŠΈλ₯Ό μ‚¬μš©ν•œλ‹€.
    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));
    };

    const App = () => {
        (...)

    const _loadAssets = async () => {
        const imageAssets = cacheImages([
            require("../assets/splash.png"),
        ]);

        const fontAssets = cacheFonts([]);

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

    return isReady ? (
        (...)
    ) : (
        <AppLoading
            startAsync={_loadAssets}
            onFinish={() => setIsReady(true)}
            onError={console.error}
        />
      );
    };

πŸ‘¨πŸ»β€πŸ’» 둜고 μ μš©ν•˜κΈ°

  • 이번 μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ 둜고λ₯Ό Firebase μŠ€ν† λ¦¬μ§€μ— μ—…λ‘œλ“œν•˜κ³  둜그인 ν™”λ©΄μ—μ„œ μ‚¬μš©ν•˜λ„λ‘ λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€.
  • μŠ€ν† λ¦¬μ§€μ— νŒŒμΌμ„ μ—…λ‘œλ“œν•˜κ³  파일 μ •λ³΄μ—μ„œ 이름을 ν΄λ¦­ν•˜λ©΄ ν•΄λ‹Ή 파일의 url을 얻을 수 μžˆμŠ΅λ‹ˆλ‹€.
    //1. src/utils/images.js 생성

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

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

    //2. src/App.js (_loadAssets λ©”μ„œλ“œ μˆ˜μ •)

    const _loadAssets = async () => {
        const imageAssets = cacheImages([
            require("../assets/splash.png"),
            ...Object.values(images),
        ]);

        const fontAssets = cacheFonts([]);

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

    //3. Firebase μŠ€ν† λ¦¬μ§€ Rules μˆ˜μ •

    rules_version = '2';
    service firebase.storage {
      match /b/{bucket}/o {
        match /logo.png {
          allow read;
        }
      }
    }

πŸ‘¨πŸ»β€πŸ’» useRef, forwardRef

  • useRefλ₯Ό μ΄μš©ν•˜μ—¬ passwordRefλ₯Ό λ§Œλ“€κ³  λΉ„λ°€λ²ˆν˜Έλ₯Ό μž…λ ₯ν•˜λŠ” Input μ»΄ν¬λ„ŒνŠΈμ˜ ref둜 μ§€μ •ν–ˆμŠ΅λ‹ˆλ‹€.
  • 이메일을 μž…λ ₯ν•˜λŠ” Input μ»΄ν¬λ„ŒνŠΈμ˜ onSubmitEditing ν•¨μˆ˜λ₯Ό passwordRef λ₯Ό μ΄μš©ν•΄μ„œ λΉ„λ°€λ²ˆν˜Έλ₯Ό μž…λ ₯ν•˜λŠ” Input μ»΄ν¬λ„ŒνŠΈλ‘œ ν¬μ»€μŠ€κ°€ μ΄λ™λ˜λ„λ‘ μž‘μ„±ν•©λ‹ˆλ‹€.
  • refλŠ” key처럼 λ¦¬μ•‘νŠΈμ—μ„œ μžμ‹ μ»΄ν¬λ„ŒνŠΈμ˜ props둜 μ „λ‹¬λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. μ΄λ•Œ, forwardRef ν•¨μˆ˜λ₯Ό μ΄μš©ν•˜λ©΄ refλ₯Ό 전달받을 수 μžˆμŠ΅λ‹ˆλ‹€.
const Input = forwardRef(
  (
    {
      (...)
    },
    ref
  ) => {
      
    return (
      <Container>
        (...)
        <StyledTextInput
          ref={ref}
          isFocused={isFocused}
          value={value}
          onChangeText={onChangeText}
          onSubmitEditing={onSubmitEditing}
          onFocus={() => setIsFocused(true)}
          onBlur={() => {
            setIsFocused(false);
            onBlur();
          }} //input에 ν¬μ»€μŠ€κ°€ ν’€λ¦΄λ•Œ ν˜ΈμΆœλ˜λŠ” 콜백
          placeholder={placeholder}
          secureTextEntry={isPassword} //문자λ₯Ό κ°μΆ”λŠ” κΈ°λŠ₯
          returnKeyType={returnKeyType} //리턴 ν‚€λ₯Ό λ ˆμ΄λΈ”λ‘œ μ„€μ •
          maxLength={maxLength} //μž…λ ₯ ν•  μˆ˜μžˆλŠ” μ΅œλŒ€ 문자 수λ₯Ό μ œν•œ
          autoCapitalize="none" //μžλ™ λŒ€λ¬Έμž λ³€ν™˜
          autoCorrect={false} //μžλ™ μˆ˜μ •
          textContentType="none" //iOS
          underlineColorAndroid="transparent" //Android TextInput 밑쀄 의 색상
        />
      </Container>
    );
  }
);

πŸ‘¨πŸ»β€πŸ’» ν‚€λ³΄λ“œ 감좔기

  • TextInput에 μž…λ ₯ 도쀑 λ‹€λ₯Έ 곳을 ν„°μΉ˜ν•˜λ©΄ ν‚€λ³΄λ“œκ°€ μ‚¬λΌμ§€λŠ”λ°, μ΄λŠ” μ‚¬μš©μž 편의λ₯Ό μœ„ν•œ 일반적인 μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ 동μž₯ λ°©μ‹μž…λ‹ˆλ‹€.
  • λ¦¬μ•‘νŠΈ λ„€μ΄ν‹°λΈŒμ—μ„œ TouchableWithoutFeedback μ»΄ν¬λ„ŒνŠΈμ™€ Keyboard APIλ₯Ό μ‚¬μš©ν•΄μ„œ μœ„ 동μž₯ 방식을 κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • μœ„μ— 두 μ»΄ν¬λ„ŒνŠΈλŠ” λŒ€μ‹ , μœ„μΉ˜μ— 따라 ν‚€λ³΄λ“œκ°€ Input μ»΄ν¬λ„ŒνŠΈλ₯Ό κ°€λ¦¬λŠ” 문제λ₯Ό ν•΄κ²°ν•˜μ§€λŠ” λͺ»ν•©λ‹ˆλ‹€.
  • react-native-keyboard-aware-scroll-view 라이브러리λ₯Ό μ΄μš©ν•˜λ©΄ μœ„ 문제λ₯Ό ν•΄κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 뿐만 μ•„λ‹ˆλΌ focusκ°€ μžˆλŠ” TextInput μ»΄ν¬λ„ŒνŠΈμ˜ μœ„μΉ˜λ‘œ μžλ™ μŠ€ν¬λ‘€λ˜λŠ” κΈ°λŠ₯ λ“± Input μ»΄ν¬λ„ŒνŠΈμ— ν•„μš”ν•œ κΈ°λŠ₯듀을 μ œκ³΅ν•©λ‹ˆλ‹€.
  //import 
  import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";

  <KeyboardAwareScrollView
    contentContainerStyle={{ flex: 1 }}
    extraScrollHeight={20} //μŠ€ν¬λ‘€λ˜λŠ” μœ„μΉ˜λ₯Ό μ‘°μ •ν•  λ•Œ μ‚¬μš©
  >
    (...)
  </KeyboardAwareScrollView>

πŸ‘¨πŸ»β€πŸ’» 이메일 μœ νš¨μ„± 검사

  //μ˜¬λ°”λ₯Έ 이메일 ν˜•μ‹ 검사
  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, "");
  };

πŸ‘¨πŸ»β€πŸ’» Button μ»΄ν¬λ„ŒνŠΈ

  • TouchableOpacityλŠ” ν„°μΉ˜ 이벀트(onPress)λ₯Ό μ‚¬μš©ν•  수 μžˆλŠ” View
  const Container = styled.TouchableOpacity`
    (...)
  `;

πŸ‘¨πŸ»β€πŸ’» λ…ΈμΉ˜ λ””μžμΈ

  • react-native-safe-area-context λΌμ΄λΈŒλŸ¬λ¦¬κ°€ μ œκ³΅ν•˜λŠ” useSafeAreaInsets Hook ν•¨μˆ˜λ₯Ό μ΄μš©ν•˜λ©΄ λ…ΈμΉ˜λ””μžμΈμ„ ν•΄κ²°ν•  수 μžˆλ‹€.
  • useSafeAreaInsets의 μž₯점은 iOSλΏλ§Œμ•„λ‹ˆλΌ μ•ˆλ“œλ‘œμ΄λ“œμ—μ„œλ„ 적용 κ°€λŠ₯ν•œ padding 값을 μ „λ‹¬ν•œλ‹€.
  //import 
  import { useSafeAreaInsets } from "react-native-safe-area-context";

  //padding topκ³Ό bottom의 값을 useSafeAreaInsets ν•¨μˆ˜κ°€ μ•Œλ €μ£ΌλŠ” κ°’λ§ŒνΌ μ„€μ •ν•œλ‹€.
  const Container = styled.View`
    (...)
    padding: 0 20px;
    padding-top: ${({ insets: { top } }) => top}px;
    padding-bottom: ${({ insets: { bottom } }) => bottom}px;
  `;

πŸ‘¨πŸ»β€πŸ’» κΆŒν•œ μš”μ²­, μ‚¬μ§„μ˜ 정보 κ°€μ Έμ˜€κΈ°

πŸƒκΆŒν•œ μš”μ²­(iOS)

  • expo-image-picker 라이브러리λ₯Ό ν†΅ν•΄μ„œ 기기의 사진첩에 μ ‘κ·Όν•΄μ„œ μ„ νƒλœ μ‚¬μ§„μ˜ 정보λ₯Ό κ°€μ Έμ˜¬ 수 μžˆλ‹€.
  • iOSμ—μ„œλŠ” 사진첩에 μ ‘κ·Όν•˜κΈ° μœ„ν•΄ μ‚¬μš©μžμ—κ²Œ κΆŒν•œμ„ μš”μ²­ν•˜λŠ” 과정이 ν•„μš”ν•˜λ―€λ‘œ, κΆŒν•œμ„ μš”μ²­ν•˜λŠ” 뢀뢄을 μΆ”κ°€ν•΄μ•Ό ν•œλ‹€. μ•ˆλ“œλ‘œλ“œμ—μ„œλŠ” νŠΉλ³„ν•œ μ„€μ • 없이 사진에 μ ‘κ·Όν•  수 μžˆλ‹€.
  //install
  expo install expo-image-picker

  //import 
  import * as ImagePicker from "expo-image-picker";
  import * as Permissions from "expo-permissions";

  //iOS κΆŒν•œ μš”μ²­
  useEffect(() => {
    async () => {
      try {
        if (Platform.OS === "ios") {
          const { status } = await Permissions.askAsync(
            Permissions.CAMERA_ROLL
          );
          if (status !== "granted") {
            Alert.alert(
              "Photo Permission",
              "Please turn on the camera roll permissions"
            );
          }
        }
      } catch (e) {
        Alert.alert("Photo Permission Error", e.message);
      }
    };
  }, []);

πŸƒμ‚¬μ§„ μž…λ ₯λ°›κΈ°

  • 사진 λ³€κ²½ λ²„νŠΌμ„ ν΄λ¦­ν•˜λ©΄ ν˜ΈμΆœλ˜λŠ” ν•¨μˆ˜μ—μ„œ 기기의 사진에 μ ‘κ·Όν•˜κΈ° μœ„ν•΄ ν˜ΈμΆœλ˜λŠ” 라이브러리 ν•¨μˆ˜λŠ” λ‹€μŒκ³Ό 같은 값듀을 ν¬ν•¨ν•œ 객체λ₯Ό νŒŒλΌλ―Έν„°λ‘œ μ „λ‹¬λ°›λŠ”λ‹€.
    1. mediaTypes: μ‘°νšŒν•˜λŠ” 자료의 νƒ€μž…
    2. allowsEditing: 이미지 선택 ν›„ νŽΈμ§‘ 단계 진행 μ—¬λΆ€
    3. aspect: μ•ˆλ“œλ‘œμ΄λ“œ μ „μš© μ˜΅μ…˜μœΌλ‘œ 이미지 νŽΈμ§‘μ‹œ μ‚¬κ°ν˜•μ˜ λΉ„μœ¨([x, y])
    4. quality: 0 ~ 1 μ‚¬μ΄μ˜ 값을 λ°›μœΌλ©° μ••μΆ• ν’ˆμ§ˆμ„ 의미 (1: μ΅œλŒ€ ν’ˆμ§ˆ)
  const _handelEditButton = async () => {
      try {
        const result = await ImagePicker.launchCameraAsync({
          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);
      }
    };

  • 기기의 사진에 μ ‘κ·Όν•˜λŠ” ν•¨μˆ˜λŠ” κ²°κ³Όλ₯Ό λ°˜ν™˜ν•˜λŠ”λ°, cancelled 값을 톡해 선택 μ—¬λΆ€λ₯Ό 확인할 수 μžˆλ‹€. λ§Œμ•½ μ‚¬μš©μžκ°€ 사진을 μ„ νƒν–ˆλ‹€λ©΄ λ°˜ν™˜λœ 결과의 uriλ₯Ό 톡해 μ„ νƒλœ μ‚¬μ§„μ˜ μ£Όμ†Œλ₯Ό μ•Œ 수 μžˆλ‹€.
  //상단에 result의 λ°˜ν™˜ κ°’
  {
    "cancelled": true,
  }

  {
    "cancelled": false,
    "height": 000,
    "type": "image",
    "uri": "file:../...jpg",
    "width": 000,
  }

πŸ‘¨πŸ»β€πŸ’» 둜그인, νšŒμ›κ°€μž… κΈ°λŠ₯ κ΅¬ν˜„

πŸƒ 둜그인

  • νŒŒμ΄μ–΄λ² μ΄μŠ€λ₯Ό μ΄μš©ν•œ 이메일과 λΉ„λ°€λ²ˆν˜Έλ₯Ό μ΄μš©ν•΄ μΈμ¦λ°›λŠ” ν•¨μˆ˜λŠ” signInWithEmailAndPassword μž…λ‹ˆλ‹€.
  //utils/firebase.js
  export const login = async ({ email, password }) => {
    const { user } = await Auth.signInWithEmailAndPassword(email, password);
    return user;
  };

  //screens/Login.js
  import { login } from "../utils/firebase";

  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 μž…λ‹ˆλ‹€.
//utils/firebase.js

  export const signup = async ({ email, password, name, photoUrl }) => {
    const { user } = await Auth.createUserWithEmailAndPassword(email, password);
    return user;
  };

  //screens/Signup.js
  import { signup } from "../utils/firebase";

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

πŸ‘¨πŸ»β€πŸ’» Spinner(with ContextAPI)

πŸƒ Spinner Component

  • Spinner μ»΄ν¬λ„ŒνŠΈλŠ” 둜그인 ν˜Ήμ€ νšŒμ›κ°€μž…μ΄ μ§„ν–‰λ˜λŠ” λ™μ•ˆ 데이터λ₯Ό μˆ˜μ •ν•˜κ±°λ‚˜ λ²„νŠΌμ„ μΆ”κ°€λ‘œ ν΄λ¦­ν•˜λŠ” 일이 λ°œμƒν•˜μ§€ μ•Šλ„λ‘ λ°©μ§€ν•˜λŠ” κΈ°λŠ₯을 ν•œλ‹€.
  • Spinner μ»΄ν¬λ„ŒνŠΈλŠ” λ¦¬μ•‘νŠΈ λ„€μ΄ν‹°λΈŒμ—μ„œ μ œκ³΅ν•˜λŠ” AcitivityIndicator μ»΄ν¬λ„ŒνŠΈλ₯Ό μ΄μš©ν•΄μ„œ μ‰½κ²Œ λ§Œλ“€ 수 μžˆλ‹€.
  • Spinner μ»΄ν¬λ„ŒνŠΈλ₯Ό AuthStack λ‚΄λΉ„κ²Œμ΄μ…˜μ˜ ν•˜μœ„ μ»΄ν¬λ„ŒνŠΈλ‘œ μ‚¬μš©ν•˜λ©΄ λ‚΄λΉ„κ²Œμ΄μ…˜μ„ ν¬ν•¨ν•œ ν™”λ©΄ 전체λ₯Ό 차지할 수 μ—†μŠ΅λ‹ˆλ‹€. λ‚΄λΉ„κ²Œμ΄μ…˜μ„ ν¬ν•¨ν•œ ν™”λ©΄ 전체λ₯Ό 감싸기 μœ„ν•΄μ„œλŠ” navigations ν΄λ”μ˜ index.jsμ—μ„œ AuthStack λ‚΄λΉ„κ²Œμ΄μ…˜κ³Ό 같은 μœ„μΉ˜μ— Spinner μ»΄ν¬λ„ŒνŠΈλ₯Ό μ‚¬μš©ν•΄μ•Ό λ©λ‹ˆλ‹€.
  import React, { useContext } from 'react';
  import { ActivityIndicator } from 'react-native';
  import styled, { ThemeContext } from 'styled-components/native';

  const Container = styled.View`
    (...)
  `;

  const Spinner = () => {
      const theme = useContext(ThemeContext);
      return (
          <Container>
              <ActivityIndicator size={'large'} color={theme.spinnerIndicator} />
          </Container>
      )
  };

  export default Spinner;

  • Spinner μ»΄ν¬λ„ŒνŠΈλ₯Ό AuthStack λ‚΄λΉ„κ²Œμ΄μ…˜μ˜ ν•˜μœ„ μ»΄ν¬λ„ŒνŠΈλ‘œ μ‚¬μš©ν•˜λ©΄ λ‚΄λΉ„κ²Œμ΄μ…˜μ„ ν¬ν•¨ν•œ ν™”λ©΄ 전체λ₯Ό 차지할 수 μ—†μŠ΅λ‹ˆλ‹€. λ‚΄λΉ„κ²Œμ΄μ…˜μ„ ν¬ν•¨ν•œ ν™”λ©΄ 전체λ₯Ό 감싸기 μœ„ν•΄μ„œλŠ” navigations ν΄λ”μ˜ index.jsμ—μ„œ AuthStack λ‚΄λΉ„κ²Œμ΄μ…˜κ³Ό 같은 μœ„μΉ˜μ— Spinner μ»΄ν¬λ„ŒνŠΈλ₯Ό μ‚¬μš©ν•΄μ•Ό λ©λ‹ˆλ‹€.
  (...)
  const Navigation = () => {
    const { inProgress } = useContext(ProgressContext);

    return (
      <NavigationContainer>
        <AuthStack />
        {inProgress && <Spinner />}
      </NavigationContainer>
    );
  };
  (...)

πŸƒ Context API

  • createContext ν•¨μˆ˜λ₯Ό μ΄μš©ν•΄ Contextλ₯Ό μƒμ„±ν•˜κ³ , Provider μ»΄ν¬λ„ŒνŠΈμ˜ value에 Spinner μ»΄ν¬λ„ŒνŠΈμ˜ λ Œλ”λ§ μƒνƒœλ₯Ό 관리할 inPrgress μƒνƒœ λ³€μˆ˜μ™€ μƒνƒœλ₯Ό λ³€κ²½ν•  수 μžˆλŠ” ν•¨μˆ˜λ₯Ό μ „λ‹¬ν•©λ‹ˆλ‹€.
  //contexts/Progress.js

  import React, { useState, createContext } from 'react';

  //Context 생성
  const ProgressContext = createContext({
    inProgress: false,
    spinner: () => {},
  });

  //ProgressProvider
  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 };

  //contexts/index.js
  import {ProgressContext, ProgressProvider } from './Progress';
  export { ProgressContext, ProgressProvider }; 

  //src/App.js
  <ThemeProvider theme={theme}>
    <ProgressProvider>
      <StatusBar barStyle="light-content" />
      <Navigation />
    </ProgressProvider>
  </ThemeProvider>

πŸ‘¨πŸ»β€πŸ’» Stack λ‚΄λΉ„κ²Œμ΄μ…˜ 속 Tab λ‚΄λΉ„κ²Œμ΄μ…˜μ˜ 헀더 λ³€κ²½

  • MainStack λ‚΄λΉ„κ²Œμ΄μ…˜μ—μ„œ MainTab λ‚΄λΉ„κ²Œμ΄μ…˜μ΄ ν™”λ©΄μœΌλ‘œ μ‚¬μš©λ˜λŠ” Screen μ»΄ν¬λ„ŒνŠΈμ˜ name은 "Main"으둜 μ„€μ •λ˜μ–΄ μžˆλ‹€. ν—€λ”μ˜ 타이틀과 κ΄€λ ¨ν•΄ νŠΉλ³„νžˆ μ„€μ •ν•˜μ§€ μ•ŠμœΌλ©΄ Screen μ»΄ν¬λ„ŒνŠΈμ˜ name에 μ„€μ •λœ 값이 ν—€λ”μ˜ νƒ€μ΄ν‹€λ‘œ 되기 λ•Œλ¬Έμ—, ν”„λ‘œν•„ ν™”λ©΄κ³Ό 채널 λͺ©λ‘ λͺ¨λ‘ 'Main'으둜 타이틀이 λ‚˜νƒ€λ‚œλ‹€.
  <Stack.Navigator
    initialRouteName="Main"
    (...)
  >   
    <Stack.Screen name="Main" component={MainTab} />
    (...)
  </Stack.Navigator>

  • MainTab λ‚΄λΉ„κ²Œμ΄μ…˜μ€ MainStack λ‚΄λΉ„κ²Œμ΄μ…˜μ˜ ν™”λ©΄μœΌλ‘œ μ‚¬μš©λ˜μ—ˆκΈ° λ•Œλ¬Έμ— λ‹€λ₯Έ ν™”λ©΄λ“€κ³Ό λ§ˆμ°¬κ°€μ§€λ‘œ propsλ₯Ό 톡해 navigationκ³Ό routeλ₯Ό 전달 λ°›λŠ”λ‹€.
  • route에 ν¬ν•¨λœ state의 값은 λ‹€μŒκ³Ό κ°™λ‹€
    1. index: ν˜„μž¬ λ Œλ”λ§ λ˜λŠ” ν™”λ©΄μ˜ 인덱슀
    2. routeNames: ν™”λ©΄μœΌλ‘œ μ‚¬μš©λ˜λŠ” Navigator μ»΄ν¬λ„ŒνŠΈμ—μ„œ Screen μ»΄ν¬λ„ŒνŠΈλ“€μ˜ name 속성을 λ°°μ—΄λ‘œ κ°–λŠ”λ‹€.
    3. type: ν˜„μž¬ ν™”λ©΄μœΌλ‘œ μ‚¬μš©λ˜λŠ” Navigator μ»΄ν¬λ„ŒνŠΈμ˜ νƒ€μž…μ΄λ©°, MainTab λ‚΄λΉ„κ²Œμ΄μ…˜μ€ νƒ­ λ‚΄λΉ„κ²Œμ΄μ…˜μ΄κΈ° λ•Œλ¬Έμ— 'tab' 값을 κ°–λŠ”λ‹€.
  //route의 state
  {
    "index": 0,
    "routeNames": [
      "Channel List",
      "Profile",
    ],
    "type": "tab",
    ...
  }

  //MainTab
  useEffect(() => {
    const titles = route.state?.routeNames || ['Channels'];
    const index = route.state?.index || 0;
    navigation.setOptions({ headerTitle: titles[index ]});
  }, [route]);

  • ν•˜μ§€λ§Œ μœ„μ— λ°©μ‹λŒ€λ‘œ ν•˜λ©΄ route의 state에 직접 μ ‘κ·Όν•΄μ„œ λ°œμƒν•˜λŠ” κ²½κ³ λ©”μ‹œμ§€κ°€ λœ¬λ‹€. 이걸 ν•΄κ²°ν•˜λ €λ©΄ getFocusedRouteNameFromRoute λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•˜λ©΄ λœλ‹€.
  • πŸ”– κ΄€λ ¨ 이슈: Alchemist85K/my-first-react-native#26
  useEffect(() => {
      const screenName = getFocusedRouteNameFromRoute(route) || 'Channels';

      navigation.setOptions({ 
        headerTitle: screenName,
      });
  }, [route]);

πŸ‘¨πŸ»β€πŸ’» Setting a timer for a long period of time, ... 였λ₯˜

  • πŸ”– κ΄€λ ¨ 이슈: Alchemist85K/my-first-react-native#28
  • /node_modules/react-native/Libraries/Core/Timers/JSTimers.js
  • MAX_TIMER_DURATION_MS λΌλŠ” λ³€μˆ˜ 값을 60 * 1000 μ—μ„œ 10000 * 1000으둜 λ³€κ²½

πŸ‘¨πŸ»β€πŸ’» λ°μ΄ν„°λ² μ΄μŠ€

  • νŒŒμ΄μ–΄λ² μ΄μŠ€μ—μ„œ μ œκ³΅ν•˜λŠ” νŒŒμ΄μ–΄μŠ€ν† μ–΄λŠ” NoSQL λ¬Έμ„œ μ€‘μ‹¬μ˜ λ°μ΄ν„°λ² μ΄μŠ€μ΄λ‹€.
  • SQL λ°μ΄ν„°λ² μ΄μŠ€μ™€ 달리 ν…Œμ΄λΈ”μ΄λ‚˜ 행이 μ—†κ³  μ»¬λ ‰μ…˜, λ¬Έμ„œ, ν•„λ“œλ‘œ κ΅¬μ„±λœλ‹€.
    1. μ»¬λ ‰μ…˜μ€ λ¬Έμ„œμ˜ μ»¨ν…Œμ΄λ„ˆ 역할을 ν•˜λ©°, λͺ¨λ“  λ¬Έμ„œλŠ” 항상 μ»¬λ ‰μ…˜μ— μ €μž₯λœλ‹€.
    2. λ¬Έμ„œλŠ” νŒŒμ΄μ–΄μŠ€ν† μ–΄μ˜ μ €μž₯ λ‹¨μœ„λ‘œ 값이 μžˆλŠ” ν•„λ“œλ₯Ό κ°–λŠ”λ‹€. λ¬Έμ„œμ˜ κ°€μž₯ 큰 νŠΉμ§•μ€ μ»¬λ ‰μ…˜μ„ ν•„λ“œλ‘œ κ°€μ§ˆ 수 μžˆλ‹€.
  • νŒŒμ΄μ–΄μŠ€ν† μ–΄λŠ” 일반적인 λ°μ΄ν„°λ² μ΄μŠ€μ™€ 달리 λ°μ΄ν„°λ² μ΄μŠ€μ˜ λ‚΄μš©μ΄ μˆ˜μ •λ˜λ©΄ μ‹€μ‹œκ°„μœΌλ‘œ λ³€κ²½λœ λ‚΄μš©μ„ μ•Œ 수 μžˆλ‹€.
  • μ»¬λ ‰μ…˜κ³Ό λ¬Έμ„œλŠ” 항상 μœ μΌν•œ IDλ₯Ό κ°–κ³  μžˆμ–΄μ•Ό ν•œλ‹€λŠ” κ·œμΉ™μ΄ μžˆλ‹€.

1

  //νŒŒμ΄μ–΄μŠ€ν† μ–΄ λ³΄μ•ˆ κ·œμΉ™ μˆ˜μ •
  rules_version = '2';
    service cloud.firestore {
      match /databases/{database}/documents {
        match /channels/{channel} {
          allow read, write: if request.auth.uid != null;
        }
      }
    }

πŸ‘¨πŸ»β€πŸ’» FlatList

  • μ§€κΈˆκΉŒμ§€ λ§Žμ€ μ–‘μ˜ 데이터λ₯Ό λ Œλ”λ§ν•  λ•Œ ScrollView μ»΄ν¬λ„ŒνŠΈλ₯Ό μ΄μš©ν•΄ 화면이 λ„˜μ–΄κ°€λ„λ‘ 슀크둀이 μƒμ„±λ˜μ–΄ 확인할 수 μžˆλ„λ‘ λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€.
  • FlatListμ»΄ν¬λ„ŒνŠΈλŠ” ScrollView μ»΄ν¬λ„ŒνŠΈμ™€ 같은 역할을 ν•˜λŠ”λ°, ScrollView μ»΄ν¬λ„ŒνŠΈλŠ” λ Œλ”λ§ν•΄μ•Ό ν•˜λŠ” λͺ¨λ“  데이터λ₯Ό ν•œλ²ˆμ— λ Œλ”λ§ν•©λ‹ˆλ‹€. 즉, λ°μ΄ν„°μ˜ 양을 μ•Œκ³  μžˆμ„ λ•Œ μ‚¬μš©ν•˜λŠ” 것이 μ’‹κ³ , 데이터가 맀우 많으면 λ Œλ”λ§ 속도가 λŠλ €μ§€κ³  λ©”λͺ¨λ¦¬ μ‚¬μš©λŸ‰μ΄ μ¦κ°€ν•˜λŠ” λ“± μ„±λŠ₯이 μ €ν•˜λ©λ‹ˆλ‹€.
  • κ·Έμ—λ°˜ν•΄, FlatListλŠ” 화면에 μ μ ˆν•œ μ–‘μ˜ λ°μ΄ν„°λ§Œ λ Œλ”λ§ν•˜κ³  슀크둀의 이동에 맞좰 ν•„μš”ν•œ 뢀뢄을 μΆ”κ°€μ μœΌλ‘œ λ Œλ”λ§ν•˜λŠ” νŠΉμ§•μ΄ μžˆμŠ΅λ‹ˆλ‹€.
  • FlatListμ—λŠ” 기본적으둜 3가지 속성이 μžˆμŠ΅λ‹ˆλ‹€.
    1. data: 처음 λ Œλ”λ§ν•  ν•­λͺ©μ˜ 데이터λ₯Ό λ°°μ—΄λ‘œ μ „λ‹¬ν•œλ‹€.
    2. renderItem: μ „λ‹¬λœ λ°°μ—΄μ˜ ν•­λͺ©μ„ μ΄μš©ν•΄ λ Œλ”λ§ν•˜λŠ” ν•¨μˆ˜λ₯Ό μž‘μ„±ν•΄μ•Ό ν•œλ‹€.
    3. keyExtractor: keyλ₯Ό μΆ”κ°€ν•˜κΈ° μœ„ν•΄ κ³ μœ ν•œ 값을 λ°˜ν™˜ν•˜λŠ” ν•¨μˆ˜λ₯Ό 전달해야 ν•œλ‹€.

  <FlatList
    keyExtractor={item => item['id'].toString()}
    data={channels}
    renderItem={({ item }) => {
        return (
            <Item item={item} onPress={_handleItemPress} />
        )
    }}
    windowSize={3}
  />

πŸƒ windowSize

  • FlatListμ—μ„œ λ Œλ”λ§ λ˜λŠ” λ°μ΄ν„°μ˜ 양을 μ‘°μ ˆν•˜κ³  μ‹Άλ‹€λ©΄ windowSize 속성을 μΆ”κ°€ν•΄μ„œ 값을 μ›ν•˜λŠ” κ°’μœΌλ‘œ μ„€μ •ν•˜λ©΄ λœλ‹€.
  • windowSize의 값을 μž‘μ€ κ°’μœΌλ‘œ λ³€κ²½ν•˜λ©΄ λ Œλ”λ§λ˜λŠ” 데이터가 쀄어듀어 λ©”λͺ¨λ¦¬μ˜ μ†ŒλΉ„λ₯Ό 쀄이고 μ„±λŠ₯을 ν–₯상 μ‹œν‚¬ 수 μžˆμ§€λ§Œ, λΉ λ₯΄κ²Œ μŠ€ν¬λ‘€ν•˜λŠ” μƒν™©μ—μ„œ 미리 λ Œλ”λ§λ˜μ§€ μ•Šμ€ 뢀뢄은 μˆœκ°„μ μœΌλ‘œ 빈 λ‚΄μš©μ΄ λ‚˜νƒ€λ‚  수 μžˆλ‹€λŠ” 단점이 μžˆλ‹€.

  <FlatList
    (...)
    windowSize={3}
  />

πŸƒ React.Memo

  • React.MemoλŠ” useMemo Hook ν•¨μˆ˜μ™€ λΉ„μŠ·ν•˜μ§€λ§Œ, λΆˆν•„μš”ν•œ ν•¨μˆ˜μ˜ μž¬μ—°μ‚°μ„ λ°©μ§€ν•˜λŠ” useMemo와 달리 μ»΄ν¬λ„ŒνŠΈμ˜ λ¦¬λ Œλ”λ§μ„ λ°©μ§€ν•œλ‹€λŠ” 차이가 μžˆλ‹€.
  • React.MemoλŠ” μ»΄ν¬λ„ŒνŠΈλ₯Ό κ°μ‹ΈλŠ” κ²ƒμœΌλ‘œ κ°„λ‹¨νžˆ μ μš©ν•  수 μžˆλ‹€.
  • React.Memoλ₯Ό μ‚¬μš©ν•˜λ©΄ Item μ»΄ν¬λ„ŒνŠΈλŠ” propsκ°€ 변경될 λ•ŒκΉŒμ§€ λ¦¬λ Œλ”λ§λ˜μ§€ μ•ŠλŠ”λ‹€.

const Item = React.memo(({ item: { id, title, description, createdAt }, onPress }) => {
  const theme = useContext(ThemeContext);
  console.log(`Item ${id}`);

  return (
    <ItemContainer onPress={() => onPress({ id, title })}>
        <ItemTextContainer>
            <ItemTitle>{title}</ItemTitle>
            <ItemDescription>{description}</ItemDescription>
        </ItemTextContainer>
        <ItemTime>{createdAt}</ItemTime>
        <MaterialIcons 
            name="keyboard-arrow-right"
            size={24}
            color={theme.listIcon}
        />
    </ItemContainer>
  );

πŸ‘¨πŸ»β€πŸ’» 채널 데이터 μˆ˜μ‹ 

  • firebase의 Cloud Firestoreμ—μ„œ μ‹€μ‹œκ°„ 데이터λ₯Ό λ°›μ•„μ˜€κΈ° μœ„ν•΄μ„œλŠ” onSnapshop λ©”μ„œλ“œλ₯Ό μ΄μš©ν•˜μ—¬ 데이터λ₯Ό μˆ˜μ‹ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • onSnapshop λ©”μ„œλ“œλŠ” μˆ˜μ‹  λŒ€κΈ° μƒνƒœλ‘œ μžˆλ‹€κ°€ λ°μ΄ν„°λ² μ΄μŠ€μ— λ¬Έμ„œκ°€ μΆ”κ°€λ˜κ±°λ‚˜ μˆ˜μ •λ  λ•Œλ§ˆλ‹€ μ§€μ •λœ ν•¨μˆ˜κ°€ ν˜ΈμΆœλ©λ‹ˆλ‹€.
  • μ΄λ•Œ, μ˜€λ¦„μ°¨μˆœ, λ‚΄λ¦Όμ°¨μˆœμ€ orderBy λ©”μ„œλ“œλ₯Ό μ΄μš©ν•΄μ„œ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

  useEffect(() => {
    const unsubscribe = DB.collection('channels')
      .orderBy('createdAt', 'desc')
      .onSnapshot(snapshot => {
        const list = [];

        snapshot.forEach(doc => {
          list.push(doc.data());
        });
        setChannels(list);
      });

    return () => unsubscribe();
  }, []);

πŸ‘¨πŸ»β€πŸ’» react-native-gifted-chat

  • μ±„νŒ… ν™”λ©΄μ—μ„œ μ‚¬μš©ν•  수 μžˆλŠ” κΈ°λŠ₯을 λ‹€μ–‘ν•˜κ²Œ μ œκ³΅ν•˜λŠ” react-native-gifted-chat 라이브러리
  • react-native-gifted-chat 라이브러리의 GiftedChat μ»΄ν¬λ„ŒνŠΈλŠ” λ‹€μ–‘ν•œ 섀정이 κ°€λŠ₯ν•˜λ„λ‘ λ§Žμ€ 속성을 μ œκ³΅ν•œλ‹€.
    1. μž…λ ₯된 λ‚΄μš©μ„ μ„€μ •λœ μ‚¬μš©μžμ˜ 정보 및 μžλ™μœΌλ‘œ μƒμ„±λœ ID와 ν•¨κ»˜ 전달 ν•˜λŠ” κΈ°λŠ₯
    2. 전솑 λ²„νŠΌμ„ μˆ˜μ •ν•˜λŠ” κΈ°λŠ₯
    3. 슀크둀의 μœ„μΉ˜μ— 따라 슀크둀 μœ„μΉ˜λ₯Ό λ³€κ²½ν•˜λŠ” λ²„νŠΌ λ Œλ”λ§

  <Container>
    <GiftedChat
      listViewProps={{
        style: { backgroundColor: theme.background },
      }}
      placeholder="Enter a Message"
      messages={messages}
      user={{ _id: uid, name, avatar: photoUrl }}
      onSend={_handleMessageSend}
      alwaysShowSend={true}
      textInputProps={{
        autoCapitalize: "none",
        autoCorrect: false,
        textContentType: "none",
        underlineColorAndroid: "transparent",
      }}
      multiline={false}
      renderUsernameOnMessage={true}
      scrollToBottom={true}
      renderSend={(props) => <SendButton {...props} />}
    />
  </Container>

  • user에 λ‹€μŒκ³Ό 같은 ν˜•νƒœλ‘œ μ‚¬μš©μžμ˜ 정보λ₯Ό μž…λ ₯해두면 onSend에 μ •μ˜ν•œ ν•¨μˆ˜κ°€ 호좜될 λ•Œ μž…λ ₯된 λ©”μ‹œμ§€μ™€ μ‚¬μš©μžμ˜ 정보λ₯Ό ν¬ν•¨ν•œ 객체λ₯Ό μ „λ‹¬ν•œλ‹€.
  User {
    _id: string | number;
    name: string;
    avatar: string | renderFunction;
  }

  Message {
    _id: string | number;
    text: string;
    createdAt: Date | number;
    user: User;
    ...
  }

About

πŸ’¬ React Native둜 λ§Œλ“  Chatting App

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages