Skip to content

Tchaikovsky1114/FrontendAssignment

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

28 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Frontend Assignment - ๊น€๋ช…์„ฑ

Source Folder Structure

  • src
    • assets: ์•„์ด์ฝ˜ ๋ฐ ์ •์  ํŒŒ์ผ ์ €์žฅ์†Œ
      • icons: svg ์ €์žฅ์†Œ
    • components: ์žฌ์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ ๋ฐ ์Šคํฌ๋ฆฐ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ
      • common: ์•ฑ ์ „์—ญ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ
      • pages: ์Šคํฌ๋ฆฐ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ ๋ชจ์Œ
    • context: ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ
    • hooks: ์ปค์Šคํ…€ ํ›… ๋ฐ ๋ทฐ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋กœ์ง ์ €์žฅ์†Œ
    • routes: ๋ผ์šฐํ„ฐ ๊ตฌํš
    • screens: ์•ฑ ์Šคํฌ๋ฆฐ
    • styles: ์•ฑ ์Šคํƒ€์ผ๋ง ๋ฐ ํ…Œ๋งˆ
    • types : ํƒ€์ž… ์ •์˜
    • util : ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ๋ฐ ๊ณตํ†ต ๋กœ์ง

Issue & Solve

A. Animation

  1. Bottom Up Slide Animation

KeyboardAvoidingView๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด keyboardVerticalOffset ๊ฐ’์„ ํ•˜๋“œ์ฝ”๋”ฉ์œผ๋กœ ๋„ฃ์–ด ๋งž์ถฐ์•ผ ํ•ด์„œ ๋‹ค์–‘ํ•œ ๊ธฐ๊ธฐ์—์„œ ์ •ํ™•ํ•œ ๊ฐ’์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์–ด๋ ค์› ์Šต๋‹ˆ๋‹ค.
React-native ๋‚ด์žฅ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ Keyboard๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋†’์ด๋ฅผ ๊ตฌํ•ด ๊ฐ’์„ ๊ตฌํ•˜๋ฉด ํ‚ค๋ณด๋“œ ํฌ๊ธฐ์— ๋งž์ถ”์–ด ์ธํ’‹์„ ์˜ฌ๋ฆด ์ˆ˜ ์žˆ์—ˆ์œผ๋‚˜ ์˜ฌ๋ผ๊ฐ€๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋ถ€์ž์—ฐ์Šค๋Ÿฌ์› ์Šต๋‹ˆ๋‹ค.

๊ทธ ๋‹น์‹œ ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

const BottomUpSlideComponent = () => {
  const {width} = useWindowDimensions();
  const {theme} = useTheme();
  const [keyboardHeight, setKeyboardHeight] = React.useState(0);
  useEffect(() => {
    Keyboard.addListener('keyboardDidShow', e => {
      setKeyboardHeight(e.endCoordinates.height);
    });
    Keyboard.addListener('keyboardDidHide', () => {
      setKeyboardHeight(0);
    });
    return () => {
      Keyboard.removeAllListeners('keyboardDidShow');
      Keyboard.removeAllListeners('keyboardDidHide');
    };
  }, []);

  return (
    <KeyboardAvoidingView
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
      style={[
        styles.container,
        {height: keyboardHeight + 12 + 40},
        {backgroundColor: theme.backgroundColor, width: width},
      ]}>
      <TextInput style={styles.input} />
    </KeyboardAvoidingView>
  );
};

export default BottomUpSlideComponent;

์‹œ์—ฐ์˜์ƒ์„ ์ž์„ธํžˆ ๋ณด๋‹ˆ height๊ฐ’์„ ํ†ตํ•ด ์—๋‹ˆ๋ฉ”์ด์…˜์„ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ๋Š” ๊ฒƒ ๊ฐ™์•˜์Šต๋‹ˆ๋‹ค.
๊ธฐ์กด ๋กœ์ง์„ ๋ณ€๊ฒฝํ•˜๊ณ  react-native-reanimated๋ฅผ ์‚ฌ์šฉํ•˜์˜€๊ณ ,
ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์“ฐ์ด๋Š” ๊ณณ์ด ๋งŽ์„ ๊ฒƒ ๊ฐ™์•„ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

useKeybardHeight

import {Keyboard} from 'react-native';
import {useEffect, useState} from 'react';
import {
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';

interface Props {
  additionalHeight: number;
}

const useKeyboardHeight = ({additionalHeight}: Props) => {
  const [keyboardHeight, setKeyboardHeight] = useState(0);
  const height = useSharedValue(0);
  const animatedStyle = useAnimatedStyle(() => {
    return {
      height: height.value,
    };
  });

  useEffect(() => {
    height.value = withTiming(keyboardHeight + additionalHeight, {
      duration: 100,
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [keyboardHeight]);

  useEffect(() => {
    Keyboard.addListener('keyboardDidShow', e => {
      setKeyboardHeight(e.endCoordinates.height);
    });
    Keyboard.addListener('keyboardDidHide', () => {
      setKeyboardHeight(0);
    });
    return () => {
      Keyboard.removeAllListeners('keyboardDidShow');
      Keyboard.removeAllListeners('keyboardDidHide');
    };
  }, []);
  return {
    keyboardHeight,
    height: height.value,
    animatedStyle,
  };
};

export default useKeyboardHeight;

BottomUpSlideComponent

import React, {forwardRef} from 'react';
import {StyleSheet, TextInput} from 'react-native';

import useKeyboardHeight from '../../hooks/useKeyboardHeight';
import Animated from 'react-native-reanimated';

const BottomUpSlideComponent = forwardRef<TextInput, {}>((_, ref) => {
  const {animatedStyle} = useKeyboardHeight({
    additionalHeight: 64,
  });

  return (
    <Animated.View style={[styles.container, animatedStyle]}>
      <TextInput ref={ref} style={styles.input} />
    </Animated.View>
  );
});

export default BottomUpSlideComponent;

์‹ค์ œ ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๋ฅผ ์—ฌ๋Ÿฌ๊ณณ์—์„œ ์‚ฌ์šฉํ•  ๋•Œ, ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ํ•ด์ œ๊ฐ€ ๋˜์ง€ ์•Š์•„ ์ •์ƒ์ ์œผ๋กœ Input์ด ๋žœ๋”๋ง ๋˜์ง€ ์•Š๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
Error: Attempted to remove more RCtKeyboardObserver listteneners than added

์ด๋ฒคํŠธ๋ฅผ ๋ณ€์ˆ˜์— ํ• ๋‹นํ•˜์—ฌ ComponentDidUnmount์‹œ ์ด๋ฐดํŠธ๋ฅผ ์ง€์šฐ๋Š” ๋ฐฉ์‹์œผ๋กœ ํ•ด๊ฒฐํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  useEffect(() => {
    // ๋ณ€์ˆ˜ ํ• ๋‹น
    const keyboardWillShowListener = Keyboard.addListener('keyboardWillShow', e => {
      setKeyboardHeight(e.endCoordinates.height);
    });
    const keyboardWillHideListener = Keyboard.addListener('keyboardWillHide', () => {
      setKeyboardHeight(0);
    });
    return () => {
      // component did unmount์ผ ๋•Œ ์‚ญ์ œ
      keyboardWillShowListener.remove(); 
      keyboardWillHideListener.remove();
    };
  }, []);

๋˜ํ•œ BottomUpSlideInput์„ ์—ฌ๋Ÿฌ๊ณณ์—์„œ ์‚ฌ์šฉํ•˜๋‹ค๋ณด๋‹ˆ TextInput์— ํ• ๋‹นํ•œ useRef์˜ ์ค‘๋ณต์œผ๋กœ ์ธํ•œ ๋žœ๋”๋ง ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

forwardRef์™€ useImperativeHandle์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ œ์–ดํ•˜๋Š” ๊ฒฝ์šฐ,
๊ฐ ์ž์‹ ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•ด ๋ณ„๋„์˜ ref๋ฅผ ์ƒ์„ฑํ•ด์•ผ ํ•˜๋Š”๋ฐ, ํ•œ๊ฐœ์˜ ์ธํ’‹์„ ์—ฌ๋Ÿฌ ๋ถ€๋ชจ์ปดํฌ๋„ŒํŠธ๊ฐ€ ref ์ด์–ด ๊ณต์œ ํ•˜๋‹ค๋ณด๋‹ˆ,
์‹ค์ œ ์‚ฌ์šฉํ•  ๋•Œ์—๋Š” ์›ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์•„๋‹Œ ์Šคํฌ๋ฆฐ ์ตœ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ์— ์„ ์–ธ๋œ BottomUpSlideInput ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋žœ๋”๋ง ๋˜๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์œผ๋กœ๋Š” BottomUpSlideInput ์ปดํฌ๋„ŒํŠธ ๋‚ด์—์„œ ์‚ฌ์šฉ์ฒ˜์— ๋งž๊ฒŒ ref์™€ input์„ ์ƒ์„ฑํ•˜๊ณ  ์ธํ’‹์— ๋‹ฌ์•„์ฃผ๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์—ˆ์œผ๋‚˜
์ฝ”๋“œ์˜ ์˜๋„๋ฅผ ์‰ฝ๊ฒŒ ํŒŒ์•…ํ•  ์ˆ˜ ์—†๊ณ , ์ถ”๊ฐ€ ์‚ฌ์šฉ์ฒ˜๊ฐ€ ์ƒ๊ธธ๋•Œ๋งˆ๋‹ค ์ธํ’‹๊ณผ ref๋ฅผ ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ์— ์ถ”๊ฐ€ ํ•ด์ฃผ์–ด์•ผํ•˜๊ณ ,
input์™ธ ๋‹ค๋ฅธ ์š”์†Œ๋“ค์ด ๋“ค์–ด์˜ฌ ์ˆ˜ ์—†์–ด ์žฌ์‚ฌ์šฉ์„ฑ์ด ๋–จ์–ด์ง€๋Š” ์ด์œ ๋กœ ์ปดํฌ๋„ŒํŠธ ๋ฆฌํŒฉํ„ฐ๋ง์„ ์ง„ํ–‰ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

BottomUpSlideInput ์ปดํฌ๋„ŒํŠธ๋ฅผ BottomUpSliderComponent๋กœ ์ด๋ฆ„์„ ๋ฐ”๊ฟ”์ฃผ๊ณ 
Input์„ children์œผ๋กœ ์ „๋‹ฌํ•œ ๋’ค editMode State๋ฅผ ํ†ตํ•ด Input์„ ์ปจํŠธ๋กค ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋ณ€๊ฒฝํ•œ ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

const BottomUpSlideComponent = ({
  onSubmit,
  children,
}: PropsWithChildren<Props>) => {
  const {width} = useWindowDimensions();
  const {theme} = useTheme();
  const {delay} = useDelay(); // timeout custom hook
  const {keyboardHeight, animatedStyle} =
  useKeyboardHeight({ additionalHeight: 64, }); // animation custom hook
  
  const [fakeLoading, setFakeLoading] = useState(false); // fake loading state

  const onPressSubmit = () => {
    onSubmit();
    setFakeLoading(true);
    delay(() => {
      setFakeLoading(false);
    }, 500);
  };

  return (
    <Animated.View
      style={[
        styles.container,
        animatedStyle,
        {
          width,
          bottom: keyboardHeight ? 0 : -100,
          backgroundColor: theme.backgroundColor,
        },
      ]}>
      {children}
      <TouchableOpacity onPress={onPressSubmit} style={styles.submitButton}>
        {fakeLoading ? (
          <ActivityIndicator color={theme.grey} size="small" />
        ) : (
          <Upload width={32} height={32} fill={theme.accent} />
        )}
      </TouchableOpacity>
    </Animated.View>
  );
};

export default BottomUpSlideComponent;

BottomUpSlideComponent๋ฅผ ์‚ฌ์šฉํ•œ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

{
  editMode === 'create' &&
  <BottomUpSlideComponent onSubmit={createChecklist}>
          {/* Children */}
    <TextInput
      ref={createChecklistRef}
      style={[
        styles.input,
        {borderColor: theme.lightGrey, fontSize: theme.textXS},
      ]}
      value={newChecklistContent + ''}
      placeholder='์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'
      onChangeText={onChangeNewChecklistContent}
      selectionColor={theme.accent}
      keyboardType='default'
    />
  </BottomUpSlideComponent>
  }
  {
    editMode === 'update' &&
    <BottomUpSlideComponent onSubmit={onSubmitUpdateContent}>
            {/* Children */}
      <TextInput
      ref={updateChecklistInputRef}
      style={[
        styles.input,
        {borderColor: theme.accent, fontSize: theme.textXS},
      ]}
      value={editText + ''}
      placeholder='์ˆ˜์ •ํ•  ๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'
      onChangeText={onChangeEditText}
      selectionColor={theme.accent}
      keyboardType='default'
    />
    </BottomUpSlideComponent>
  }

Dry ์›์น™๋„ ์ค‘์š”ํ•˜๋‚˜ ํ•œ ๋ˆˆ์— ์˜๋„๋ฅผ ์•Œ์•„ ๋ณผ ์ˆ˜ ์žˆ๋Š” ์ ์ด ๋” ํด๋ฆฐํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•˜์—ฌ ๊ณ ๋ฏผํ•œ ๋์— ์œ„์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.


B. ์•ฑ์˜ ๊ทœ๋ชจ์™€ ๋ชฉ์ ์— ๋งž๋Š” ์ƒํƒœ๊ด€๋ฆฌ

  1. Checklists์˜ ์ƒํƒœ๋ฅผ non-persistent local state๋กœ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋…ธ๋ ฅํ•˜์˜€์Šต๋‹ˆ๋‹ค.
    ์ „์—ญ์ƒํƒœ ๋˜๋Š” Context Api๋ฅผ ํ†ตํ•ด ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ useChecklists ์ปค์Šคํ…€ ํ›…์„ ์ž‘์„ฑํ•˜์—ฌ ๋‚ด๋ถ€์— ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๊ณ  ์กฐ์ž‘ํ•˜๋Š” ๋กœ์ง์„ ๋‘๊ณ ,
    ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์— ๋‚ด๋ ค์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ local state๋ฅผ ์ง€ํ‚ค๋ ค๊ณ  ๋…ธ๋ ฅํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  2. ๊ทธ ์™ธ ์•ฑ ์ „์—ญ์œผ๋กœ ํ•„์š”ํ•œ ์ƒํƒœ๋“ค์€ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ทœ๋ชจ์— ์ ํ•ฉํ•œ Context Api๋ฅผ ์„ ํƒํ•˜์—ฌ ๊ด€๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

ํ”„๋กœ์ ํŠธ์˜ ๊ทœ๋ชจ๋ฅผ ๋ณด๋ฉด Context Api๋งŒ์œผ๋กœ๋„ ์ถฉ๋ถ„ํžˆ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•˜์˜€๊ณ , useContext ํ›…์„ ํ†ตํ•ด ์‚ฌ์šฉ์„ ๋‹จ์ˆœํ™” ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
๋˜ํ•œ Context Api๋Š” React์— ๋‚ด์žฅ๋˜์–ด ์žˆ์–ด ๋‹ค๋ฅธ Hook๋“ค๊ณผ ํ•จ๊ป˜ ์ž˜ ์ž‘๋™ํ•˜๋ฏ€๋กœ useState, useEffect์™€ ๊ฐ™์€ ํ›…๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

Context Api๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ๊ตฌ๋…ํ•˜๋Š” ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋žœ๋”๋ง ๋˜๋Š” ๋ถ€๋ถ„์— ์ฃผ์˜๋ฅผ ๊ธฐ์šธ์—ฌ ๋‚ด๋ ค์ฃผ๋Š” ๊ฐ’๊ณผ ํ•จ์ˆ˜๋“ค์— useMemo์™€ useCallback์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฆฌ๋žœ๋”๋ง์„ ์ตœ์†Œํ™” ํ•˜์˜€์Šต๋‹ˆ๋‹ค.


C. ์•ฑ ํผํฌ๋จผ์Šค ์ธก๋ฉด์— ๋Œ€ํ•œ ์ตœ์ ํ™”์— ๋Œ€ํ•œ ๊ณ ๋ฏผ

  1. ์ฒดํฌ๋ฆฌ์ŠคํŠธ์— ์•„์ดํ…œ์„ ๋งŽ์ด ๋“ฑ๋กํ•˜์—ฌ๋„ ๋žœ๋”๋ง์— ๋ฌธ์ œ๊ฐ€ ์—†๊ฒŒ๋” ์ฒดํฌ๋ฆฌ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ๋ฅผ Flatlist๋กœ ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  <FlatList
    style={[styles.container,{ width }]}
    data={checklists}
    keyExtractor={item => item.id}
    ListEmptyComponent={EmptyChecklistComponent}
    // ์•„๋ž˜ ์ฝ”๋“œ๋Š” ์ธ๋ผ์ธ ํ•จ์ˆ˜๋กœ ๋ฆฌ๋žœ๋”๋ง์„ ์ดˆ๋ž˜ํ•ฉ๋‹ˆ๋‹ค. 3๋ฒˆ์— ํ•ด๊ฒฐ ๊ณผ์ •์— ์ ์–ด๋‘์—ˆ์Šต๋‹ˆ๋‹ค.
    ListHeaderComponent={
      () => checklists.length > 0 &&
      <ProgressBar completedCount={completedChecklistCount} inCompletedCount={checklists.length} />
    }
    renderItem={({ item: checklist }) => (
      <ChecklistItem
        checklist={checklist}
        onFocusInput={onFocusInput}
        deleteChecklist={deleteChecklist}
        onChangeCompleted={onChangeCompleted}
      />
    )}
  />
  1. ์‚ฌ์šฉํ•˜๋Š” ์•„์ด์ฝ˜์€ react-native-svg ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ†ตํ•ด svg๋กœ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
import Checked from '../../../assets/icons/Checked.svg';
// ...
<IconButton
  Icon={<Checked/>}
  onPress={() => onChangeCompleted(checklist)}
/>
// ...
  1. ํ”„๋กญ์Šค๋กœ ์ „๋‹ฌํ•˜๋Š” ์ธ๋ผ์ธ ํ•จ์ˆ˜์˜ ๋ฆฌ๋žœ๋”๋ง ๋ฌธ์ œ

๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ์˜ ์ตœ์ ํ™” ๊ด€๋ จํ•ด์„œ ์กฐ์‹ฌํ•˜์ง€ ๋ชปํ•œ ๋ถ€๋ถ„์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
ํ”„๋กญ์Šค๋กœ ์ธ๋ผ์ธ ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ•  ๋•Œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง ๋ฌธ์ œ์˜€๋Š”๋ฐ์š”,

๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

<FlatList
  // ...
  ListHeaderComponent={
    () => checklists.length > 0 &&
    <ProgressBar completedCount={completedChecklistCount} inCompletedCount={checklists.length} />
  }
  // ...
/>

์œ„ ์ฝ”๋“œ์—์„œ ๋ฌธ์ œ๋Š” FlatList ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง๋  ๋•Œ๋งˆ๋‹ค ListHeaderComponent ํ”„๋กญ์Šค๋กœ ์ „๋‹ฌ๋œ ์ธ๋ผ์ธ ํ•จ์ˆ˜๊ฐ€ ํ•ญ์ƒ ์ƒˆ๋กœ ์ƒ์„ฑ๋œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ฒฐ๊ณผ์ ์œผ๋กœ FlatList๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง๋  ๋•Œ๋งˆ๋‹ค ListHeaderComponent๋Š” ์ƒˆ๋กœ์šด ๊ฐ’์œผ๋กœ ๊ฐฑ์‹ ๋˜์–ด ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง์ด ๋ฐœ์ƒํ•˜์˜€๊ณ ,
ํ•ด๋‹น ํ”„๋กœ๊ทธ๋ž˜์Šค๋Š” ์ง„์ฒ™๋„๋ฅผ ์˜๋ฏธํ•˜๋Š” ์—๋‹ˆ๋ฉ”์ด์…˜์„ ๊ณ„์† 0๋ถ€ํ„ฐ ์‹คํ–‰ํ•˜๋Š” ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ธ๋ผ์ธ ํ•จ์ˆ˜๋ฅผ ์ปดํฌ๋„ŒํŠธ ๋ฐ”๊นฅ์œผ๋กœ ๋นผ๋‚ด์–ด ๋ณ€์ˆ˜์— ํ• ๋‹นํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์ง€๋งŒ,
useCallback ํ›…์„ ํ™œ์šฉํ•˜์—ฌ ํ•จ์ˆ˜๋ฅผ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค๊ณ  ์ƒ๊ฐํ•˜๊ณ  ๋ฆฌํŽ™ํ„ฐ๋ง์„ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

์•„๋ž˜๋Š” ์ด๋ฅผ ์ ์šฉํ•œ ์ˆ˜์ •๋œ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

const ProgressBarComponent = useCallback(({ completedCount, inCompletedCount }: {
    completedCount: number,
    inCompletedCount: number,
  }) => {
    return <ProgressBar completedCount={completedCount} inCompletedCount={inCompletedCount} />
  },[checklists])

  const listHeader = checklists?.length > 0
        ? <ProgressBarComponent completedCount={completedChecklistCount} inCompletedCount={checklists.length} />
        : null;

  return (    
    <FlatList
      style={[styles.container,{ width }]}
      data={checklists}
      keyExtractor={item => item.id}
      ListEmptyComponent={EmptyChecklistComponent}
      ListHeaderComponent={listHeader}
      renderItem={({ item: checklist }) => (
        <ChecklistItem
          checklist={checklist}
          onFocusInput={onFocusInput}
          deleteChecklist={deleteChecklist}
          onChangeCompleted={onChangeCompleted}
        />
      )}
    />
  );
  1. ์ฒดํฌ๋ฆฌ์ŠคํŠธ์˜ CRUD ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ณผ์ •์—์„œ ์„ธ ๊ฐ€์ง€ ๋กœ์ง์„ ๊ณ ๋ฏผํ•˜์˜€์Šต๋‹ˆ๋‹ค.
    ๊ฐ๊ฐ์˜ ๋กœ์ง์€ ๋ถˆ๋ณ€์„ฑ, ์ฝ”๋“œ ๊ฐ€๋…์„ฑ ์ธก๋ฉด์—์„œ ์žฅ๋‹จ์ ์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์„œ ์„ ํƒํ•˜๊ธฐ ์–ด๋ ค์› ๊ณ , ์–ด๋–ค ๊ฒƒ์ด ๋” ์ข‹์€ ์ฝ”๋“œ์ธ์ง€ ๊ณ ์‹ฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ์ฒซ๋ฒˆ์งธ ๋ฐฉ๋ฒ•. ๋ถˆ๋ณ€์„ฑ ๊ทธ๋ฆฌ๊ณ  ๋ณต์žก์„ฑ
    ์ฒซ ๋ฒˆ์งธ ๋ฐฉ๋ฒ•์€ useMemo์™€ ํ•จ๊ป˜ ๊นŠ์€ ๋ณต์‚ฌ๋ฅผ ํ†ตํ•ด ๋ถˆ๋ณ€์„ฑ์„ ํ™•๋ณดํ•˜๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค.
    ํ˜„์žฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋Š” 2๋ށ์Šค์˜ ๊ฐ์ฒด์ด์ง€๋งŒ ์ถ”๊ฐ€์ ์ธ ํ”„๋กœํผํ‹ฐ๋‚˜ ์š”๊ตฌ์‚ฌํ•ญ์ด ๊ณ ๋„ํ™” ๋  ๋•Œ์˜ ํ™•์žฅ์„ฑ์— ์ดˆ์ ์„ ๋งž์ถ”์—ˆ์Šต๋‹ˆ๋‹ค.
    ๋‹ค๋งŒ ๊นŠ์€ ๋ณต์‚ฌ ๋กœ์ง์ด ์ถ”๊ฐ€๋˜๊ณ , useMemo๋ฅผ ์‚ฌ์šฉํ•จ์œผ๋กœ์„œ ๊ด€๋ฆฌํ•ด์•ผํ•˜๋Š” ์˜์กด์„ฑ ๋ฐฐ์—ด์ด ๋Š˜์–ด๋‚œ๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
    ๋˜ํ•œ Memoization์€ ์„ฑ๋Šฅ์ƒ ์ด์ ์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์ง€๋งŒ, ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋งˆ๋‹ค ์ƒˆ๋กœ์šด ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ถ€๋ถ„์—์„œ ์„ฑ๋Šฅ์„ ๋–จ์–ด๋œจ๋ฆด ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋ผ๋Š” ๊ณ ๋ฏผ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
const mutableCopyChecklist = useMemo(
  () => deepCopy(allChecklists),
  [allChecklists],
);

const updateChecklistChanges = useCallback(
  (newChecklist: NewChecklist) => {
    if (!mutableCopyChecklist) {
      return;
    }

    const weekNumber = newChecklist.weekNumber;
    const index = mutableCopyChecklist[weekNumber].findIndex(
      checklist => checklist.id === newChecklist.id,
    );
    if (index < 0) {
      return;
    }
    mutableCopyChecklist[weekNumber][index] = newChecklist;
    setAllChecklists(mutableCopyChecklist);
  },
  [mutableCopyChecklist],
);
  • ๋‘ ๋ฒˆ์งธ ๋ฐฉ๋ฒ•. ๊ฐ„๊ฒฐํ•จ๊ณผ ํ™•์žฅ์„ฑ
    ๋‘ ๋ฒˆ์งธ ๋ฐฉ๋ฒ•์€ ์–•์€ ๋ณต์‚ฌ๋ฅผ ์ค‘์ฒฉ ์‚ฌ์šฉํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ๋” ๊ฐ„๋‹จํ•˜๊ฒŒ ๋งŒ๋“ค๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค.
    ํ˜„์žฌ์˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๊ตฌ์กฐ์—์„œ๋Š” 2๋‹จ๊ณ„ ์ •๋„์˜ ๊นŠ์ด์ด๊ธฐ ๋•Œ๋ฌธ์— ์–•์€๋ณต์‚ฌ ๋˜ํ•œ ์ ์ ˆํ•  ๊ฒƒ์œผ๋กœ ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.
    ํ•˜์ง€๋งŒ ์ถ”๊ฐ€์ ์ธ ํ”„๋กœํผํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ๊นŠ์ด๊ฐ€ ๊นŠ์–ด์ง€๋Š” ์š”๊ตฌ์‚ฌํ•ญ์— ์ ์ ˆํ•˜๊ฒŒ ๋Œ€์ฒ˜ํ•˜๊ธฐ ์–ด๋ ค์šธ ์ˆ˜ ์žˆ๊ณ ,
    ์ƒ์„ฑ, ์‚ญ์ œ, ์ˆ˜์ • ๋“ฑ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐ์ž‘ํ•˜๋Š” ๋กœ์ง์— ์ค‘๋ณต๋œ ๋‚ด๋ถ€ ๋กœ์ง์ด ์ฆ๊ฐ€ํ•˜๋Š” ๋‹จ์ ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
  const updateChecklistChanges = useCallback((newChecklist: NewChecklist) => {
    const weekNumber = newChecklist.weekNumber;
    setAllChecklists(prev => {
      if (!prev) {
        return {};
      }
      const mutable = {
        ...prev,
        [weekNumber]: [...prev[weekNumber]],
      };
      const index = prev[weekNumber].findIndex(
        checklist => checklist.id === newChecklist.id,
      );
      if (index < 0) {
        return prev;
      }
      mutable[weekNumber][index] = newChecklist;
      return mutable;
    });
  }, []);
  • ์„ธ ๋ฒˆ์งธ ๋ฐฉ๋ฒ•. ๊ฐ„๊ฒฐํ•จ๊ณผ ํ™•์žฅ์„ฑ
    2๋ฒˆ์งธ์™€๋Š” ๋‹ฌ๋ฆฌ ์ฝ”๋“œ ์–‘์ด ์ค„์–ด๋“ค์—ˆ๊ณ , ๋ถˆ๋ณ€์„ฑ์„ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ ์ฝ”๋“œ๋ฅผ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ๋ณด์˜€์Šต๋‹ˆ๋‹ค.
    ํ•˜์ง€๋งŒ ์ด ์—ญ์‹œ 2๋ฒˆ์งธ ๋ฐฉ๋ฒ•์ด ๊ฐ–๋Š” ํ™•์žฅ์„ฑ ๋ถ€์กฑ๊ณผ ์—ฌ๋Ÿฌ ๊ณณ์—์„œ ์‚ฌ์šฉ ํ•  ๋•Œ ์ค‘๋ณต ๋กœ์ง์ด๋ผ๋Š” ๋‹จ์ ์ด ์žˆ์—ˆ์œผ๋ฉฐ,
    ์ฝ”๋“œ์˜ ์–‘์€ ์ค„์–ด๋“ค์—ˆ์ง€๋งŒ ์ฃผ๊ด€์  ๊ฐ€๋…์„ฑ์ด ์ƒ๋Œ€์ ์œผ๋กœ ๋‚ฎ์•„์กŒ์Šต๋‹ˆ๋‹ค.
    ํŠนํžˆ, ์—…๋ฐ์ดํŠธ๋œ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ์ฐพ์•„๋‚ด๋Š” ๋กœ์ง์ด ํ•œ ์ค„์— ๋ชจ๋‘ ํ‘œํ˜„๋˜์–ด ์žˆ์–ด ์ฝ”๋“œ๋ฅผ ์œ ์ง€๋ณด์ˆ˜ ํ•  ๋•Œ ๋‚œํ•ดํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.
const updateChecklistChanges = useCallback((newChecklist: NewChecklist) => {
  setAllChecklists(prev => {
    if (!prev) {
      return {};
    }
    const weekNumber = newChecklist.weekNumber;
    const mutableChecklists = {
      ...prev,
      [weekNumber]: prev[weekNumber]?.map(item =>
        item.id === newChecklist.id ? newChecklist : item
      ),
    };
    return mutableChecklists;
  });
}, []);

์„ ํƒ

์ €๋Š” ์ฒซ๋ฒˆ์งธ ๋ฐฉ๋ฒ•์„ ์„ ํƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.
๊ทธ ์ด์œ ๋Š” ๋ถˆ๋ณ€์„ฑ ์œ ์ง€์™€ ํ™•์žฅ์„ฑ ์ธก๋ฉด์—์„œ์˜ ์•ˆ์ •์„ฑ์„ ์ค‘์‹œํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

  1. ๋ถˆ๋ณ€์„ฑ์˜ ํ™•๋ณด
    useMemo์™€ ํ•จ๊ป˜ ๊นŠ์€ ๋ณต์‚ฌ๋ฅผ ํ†ตํ•ด ๋ถˆ๋ณ€์„ฑ์„ ํ™•๋ณดํ•˜๋Š”๋ฐ ์ฃผ๋ ฅํ–ˆ์Šต๋‹ˆ๋‹ค.

  2. ํ™•์žฅ์„ฑ์— ๋Œ€ํ•œ ๊ณ ๋ ค
    ํ˜„์žฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋Š” 2๋‹จ๊ณ„์˜ ๊ฐ์ฒด์ด์ง€๋งŒ, ์ถ”๊ฐ€์ ์ธ ํ”„๋กœํผํ‹ฐ๋‚˜ ์š”๊ตฌ์‚ฌํ•ญ์ด ๊ณ ๋„ํ™”๋  ๋•Œ์˜ ํ™•์žฅ์„ฑ์— ์ดˆ์ ์„ ๋งž์ถ”์—ˆ์Šต๋‹ˆ๋‹ค.

  3. Memoization์„ ํ†ตํ•œ ์„ฑ๋Šฅ ์ตœ์ ํ™”
    useMemo๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•˜๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค.
    ์ด๋Š” ๋ถˆ๋ณ€์„ฑ์„ ์œ ์ง€ํ•˜๋ฉด์„œ๋„, ์„ฑ๋Šฅ์ ์ธ ์ด์ ์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

  4. ์ฝ”๋“œ ๊ฐ€๋…์„ฑ์˜ ์œ ์ง€
    ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด์„œ ๋ถˆ๋ณ€์„ฑ์„ ํ™•์‹คํžˆ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ ๊ฐ€๋…์„ฑ์„ ์ตœ๋Œ€ํ•œ ์œ ์ง€ํ•˜๋ ค๊ณ  ๋…ธ๋ ฅํ–ˆ์Šต๋‹ˆ๋‹ค.
    ๊นŠ์€ ๋ณต์‚ฌ ๋กœ์ง์ด ์ถ”๊ฐ€๋˜์–ด ์ฝ”๋“œ๊ฐ€ ๋ณต์žกํ•ด์ง€๋Š” ๋‹จ์ ์ด ์žˆ์ง€๋งŒ, ์ฝ”๋“œ๋ฅผ ์ดํ•ดํ•˜๋Š”๋ฐ ์žˆ์–ด์„œ ๋ช…ํ™•์„ฑ์„ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.


D. ๋””๋ฐ”์ด์Šค ํฌ๊ธฐ๋ฅผ ๊ณ ๋ คํ•œ ๋””์ž์ธ

์‚ฌ์šฉ์ž๋“ค์˜ ๋‹ค์–‘ํ•œ ๊ธฐ๊ธฐ์—์„œ ์ผ๊ด€๋œ ๋ฐ ํ–ฅ์ƒ๋œ ์•ฑ ๊ฒฝํ—˜์„ ๋ˆ„๋ฆด ์ˆ˜ ์žˆ๋„๋ก ๊ณ ๋ฏผํ–ˆ์Šต๋‹ˆ๋‹ค.
๋‹ค์–‘ํ•œ iPhone ๋””๋ฐ”์ด์Šค ํฌ๊ธฐ๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ํƒญ ํฌ๊ธฐ๋ฅผ ์žก์•˜์Šต๋‹ˆ๋‹ค.
๊ฐ„๋‹จํ•œ ์‹œ์—ฐ ์˜์ƒ์„ ์ฒจ๋ถ€ํ•ฉ๋‹ˆ๋‹ค.

example

Etc

์‚ฌ์šฉํ•œ 3rd ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

  1. react-native-reanimated@3
  2. react-native-svg, react-native-svg-transformer
  3. react-navigation@6

About

23H2 Frontend Assignment for Global Dev Squad Applicants

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors