React Native/React Native_study

[리액트 네이티브를 다루는 기술 #10] 6장 다이어리 앱 만들기1 (p.306 ~ 329)

bocoder
728x90
반응형

@ List

* 6.3 새 글 작성하기

** 6.3.1 FloatingWriteButton 만들기

** 6.3.2 WriteScreen UI 준비하기

** 6.3.3 useRef로 컴포넌트 레퍼런스 선택하기

** 6.3.4 KeyboardAvoidingView로 화면 감싸기

** 6.3.5 WriteScreen에서 텍스트 상태 관리하기

** 6.3.6 LogContext로 배열 상태 관리하기

** 6.3.7 Log 작성 기능 마무리하기

 

@ Note

1. 리팩토링 습관

 - 반복되는 코드 발견 시, 이를 컴포넌트로 만들어서 재사용하는 형태로 리팩토링 할 것

 

2. useRef Hook 사용

 - 함수 컴포넌트에서 컴포넌트의 레퍼런스를 선택할 수 있게 하는 Hook

 - const bodyRef = ueseRef() 로 ref를 생성 후, TextInput의 Props로 지정해주면 원하는 컴포넌트 레퍼런스 선택 가능

 - useRef()로 선택한 레퍼런스는 .current 값을 조회해 확인 가능

 - TextInput의 레퍼런스에 구현된 메서드

  > .current.focus() : TextInput에 포커스를 잡음

  > .current.blur() : TextInput에 포커스를 해제

  > .current.clear() : TextInput의 내용을 모두 비움

  > reference : https://ko.reactjs.org/docs/hooks-reference.html#useref

3. iOS에서  KeyboardAvoidingView 의 사용

 - TextInput에서 기본적으로 보여줄 수 있는 줄수를 초과할 경우, iOS는 하단 내용이 잘리게 됨

 - KeyboardAvoidingView로 내용을 감싸줘야 함

 

4. uuid 라이브러리 사용

 - UUID (universally uniqute identifier) 범용 고유 식별자로, 일반적으로 v4를 많이 사용함

 - uuid 라이브러리는 Node.js 의 crypto 기능을 사용함

 - react-native에 내장 되어 있지 않아서 react-native-get-random-values 라는 라이버리를 설치해 호환시켜야 함

 

5. 날짜를 문자열로 치환하는 함수

 - new Date().toISOString()

 

@ Result

iOS / android - WriteScreen UI 준비

 

0123
iOS / android - 새 글 작성하기 테스트 및 LogContext 로 배열 상태 관리

 

 

 

# Source tree

 

# index.js

/**
 * @format
 */

import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';

// uuid 사용을 위한 라이브러리
import 'react-native-get-random-values';

AppRegistry.registerComponent(appName, () => App);

 

# App.js

import React from 'react';
import {NavigationContainer} from '@react-navigation/native';
import RootStack from './screens/RootStack';
// import LogContext from './contexts/LogContext';
import {LogContextProvider} from './contexts/LogContext';

function App() {
  return (
    <NavigationContainer>
      {/* <LogContext.Provider value="안녕하세요"> */}
      <LogContextProvider>
        <RootStack />
      </LogContextProvider>
      {/* </LogContext.Provider> */}
    </NavigationContainer>
  );
}

export default App;

 

# screens > CalendarScreen.js

import React, {useContext} from 'react';
import {StyleSheet, Text, View} from 'react-native';
import LogContext from '../contexts/LogContext';

function CalendarScreen() {
  return <View style={styles.block} />;
}

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

export default CalendarScreen;

 

# screens > FeedsScreen.js

import React, {useContext} from 'react';
import {StyleSheet, View, TextInput} from 'react-native';
import FloatingWriteButton from '../components/FloatingWriteButton';
import LogContext from '../contexts/LogContext';

function FeedsScreen() {
  const {logs} = useContext(LogContext);
  console.log(JSON.stringify(logs, null, 2));

  return (
    <View style={styles.block}>
      <FloatingWriteButton />
    </View>
  );
}

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

export default FeedsScreen;

 

# screens > MainTab.js

import React from 'react';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import FeedsScreen from './FeedsScreen';
import CalendarScreen from './CalendarScreen';
import SearchScreen from './SearchScreen';
import Icon from 'react-native-vector-icons/MaterialIcons';

const Tab = createBottomTabNavigator();

function MainTab() {
  return (
    <Tab.Navigator
      screenOptions={{
        tabBarShowLabel: false,
        activeTintColor: '#009688',
      }}>
      <Tab.Screen
        name="Feeds"
        component={FeedsScreen}
        options={{
          tabBarIcon: ({color, size}) => (
            <Icon name="view-stream" size={size} color={color} />
          ),
        }}
      />
      <Tab.Screen
        name="Calendar"
        component={CalendarScreen}
        options={{
          tabBarIcon: ({color, size}) => (
            <Icon name="event" size={size} color={color} />
          ),
        }}
      />
      <Tab.Screen
        name="Search"
        component={SearchScreen}
        options={{
          tabBarIcon: ({color, size}) => (
            <Icon name="search" size={size} color={color} />
          ),
        }}
      />
    </Tab.Navigator>
  );
}

export default MainTab;

 

# screens > RootStack.js

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

const Stack = createNativeStackNavigator();
function RootStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen
        name="MainTab"
        component={MainTab}
        options={{headerShown: false}}
      />
      <Stack.Screen
        name="Write"
        component={WriteScreen}
        options={{headerShown: false}}
      />
    </Stack.Navigator>
  );
}

export default RootStack;

 

# screens > SearchScreen.js

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

function SearchScreen() {
  return <View style={styles.block} />;
}

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

export default SearchScreen;

 

# screens > WriteScreen.js

import {useNavigation} from '@react-navigation/native';
import React, {useContext, useState} from 'react';
import {StyleSheet, KeyboardAvoidingView, Platform} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import WriteEditor from '../components/WriteEditor';
import WriteHeader from '../components/WriteHeader';
import LogContext from '../contexts/LogContext';

function WriteScreen() {
  const [title, setTitle] = useState('');
  const [body, setBody] = useState('');
  const navigation = useNavigation();

  const {onCreate} = useContext(LogContext);
  const onSave = () => {
    onCreate({
      title,
      body,
      // 날짜를 문자열로 변환
      date: new Date().toISOString(),
    });
    navigation.pop();
  };

  return (
    <SafeAreaView style={styles.block}>
      <KeyboardAvoidingView
        style={styles.avoidingView}
        behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
        <WriteHeader onSave={onSave} />
        <WriteEditor
          title={title}
          body={body}
          onChangeTitle={setTitle}
          onChangeBody={setBody}
        />
      </KeyboardAvoidingView>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  block: {
    flex: 1,
    backgroundColor: 'white',
  },
  avoidingView: {
    flex: 1,
  },
});

export default WriteScreen;

 

# components > FloatingWriteButton.js

import {useNavigation} from '@react-navigation/native';
import React from 'react';
import {Platform, Pressable, StyleSheet, View} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';

function FloatingWriteButton() {
  const navigation = useNavigation();
  const onPress = () => {
    navigation.navigate('Write');
  };

  return (
    <View style={styles.wrapper}>
      <Pressable
        style={({pressed}) => [
          styles.button,
          Platform.OS === 'ios' && {
            opacity: pressed ? 0.6 : 1,
          },
        ]}
        android_ripple={{color: 'white'}}
        onPress={onPress}>
        <Icon name="add" size={24} style={styles.icon} />
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  wrapper: {
    position: 'absolute',
    bottom: 16,
    right: 16,
    width: 56,
    borderRadius: 28,
    // iOS 전용 그림자 설정
    shadowColor: '#4d4d4d',
    shadowOffset: {width: 0, height: 4},
    shadowOpacity: 0.3,
    shadowRadius: 4,
    // 안드로이드 전용 그림자 설정
    elevation: 5,
    // 안드로이드에서 물결 효과가 영역 밖으로 나가지 않도록 설정
    // iOS에서는 overflow가 hidden일 경우 그림자가 보여지지 않음
    overflow: Platform.select({android: 'hidden'}),
  },
  button: {
    width: 56,
    height: 56,
    borderRadius: 28,
    backgroundColor: '#009688',
    justifyContent: 'center',
    alignItems: 'center',
  },
  icon: {
    color: 'white',
  },
});

export default FloatingWriteButton;

 

# components > TransparentCircleButton.js

import React from 'react';
import {Platform, Pressable, StyleSheet, View} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';

function TransparentCircleButton({name, color, hasMarginRight, onPress}) {
  return (
    <View
      style={[styles.iconButtonWrapper, hasMarginRight && styles.rightMargin]}>
      <Pressable
        style={({pressed}) => [
          styles.iconButton,
          Platform.OS === 'ios' && pressed && {backgroundColor: '#efefef'},
        ]}
        onPress={onPress}
        android_ripple={{color: '#ededed'}}>
        <Icon name={name} size={24} color={color} />
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  iconButtonWrapper: {
    width: 32,
    height: 32,
    borderRadius: 16,
    overflow: 'hidden',
  },
  iconButton: {
    alignItems: 'center',
    justifyContent: 'center',
    width: 32,
    height: 32,
    borderRadius: 16,
  },
  rightMargin: {
    marginRight: 8,
  },
});

export default TransparentCircleButton;

 

# components > WriteEditor.js

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

function WriteEditor({title, body, onChangeTitle, onChangeBody}) {
  const bodyRef = useRef();

  return (
    <View style={styles.block}>
      <TextInput
        placeholder="제목을 입력하세요"
        style={styles.titleInput}
        returnKeyType="next"
        onChangeText={onChangeTitle}
        value={title}
        onSubmitEditing={() => {
          bodyRef.current.focus();
        }}
      />
      <TextInput
        placeholder="당신의 오늘을 기록해보세요"
        style={styles.bodyInput}
        multiline
        textAlignVertical="top"
        onChangeText={onChangeBody}
        returnKeyType="next"
        value={body}
        ref={bodyRef}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  block: {flex: 1, padding: 16},
  titleInput: {
    paddingVertical: 0,
    fontSize: 18,
    marginBottom: 16,
    color: '#263238',
    fontWeight: 'bold',
  },
  bodyInput: {
    flex: 1,
    fontSize: 16,
    paddingVertical: 0,
    color: '#263238',
  },
});

export default WriteEditor;

 

# components > WriteHeader.js

import {useNavigation} from '@react-navigation/native';
import React from 'react';
import {StyleSheet, View} from 'react-native';
import TransparentCircleButton from './TransparentCircleButton';

function WriteHeader({onSave}) {
  const navigation = useNavigation();
  const onGoBack = () => {
    navigation.pop();
  };
  return (
    <View style={styles.block}>
      <View style={styles.iconButtonWrapper}>
        <TransparentCircleButton
          onPress={onGoBack}
          name="arrow-back"
          color="#424242"
        />
      </View>
      <View style={styles.buttons}>
        <TransparentCircleButton
          name="delete-forever"
          color="#ef5350"
          hasMarginRight
        />
        <TransparentCircleButton
          name="check"
          color="#009688"
          onPress={onSave}
        />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  block: {
    height: 48,
    paddingHorizontal: 8,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
  },
  iconButtonWrapper: {
    width: 32,
    height: 32,
    borderRadius: 16,
    overflow: 'hidden',
  },
  iconButton: {
    alignItems: 'center',
    justifyContent: 'center',
    width: 32,
    height: 32,
    borderRadius: 16,
  },
  buttons: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  marginRight: {
    marginRight: 8,
  },
});

export default WriteHeader;

 

# contexts > LogContext.js

import React from 'react';
import {createContext, useState} from 'react';
import {v4 as uuidv4} from 'uuid';

// const LogContext = createContext('안녕하세요.');

const LogContext = createContext();

export function LogContextProvider({children}) {
  // const [text, setText] = useState('');
  // return (
  //   <LogContext.Provider value={{text, setText}}>
  //     {children}
  //   </LogContext.Provider>
  // );

  const [logs, setLogs] = useState([]);

  const onCreate = ({title, body, date}) => {
    const log = {
      id: uuidv4(),
      title,
      body,
      date,
    };
    setLogs([log, ...logs]);
  };

  return (
    <LogContext.Provider value={{logs, onCreate}}>
      {children}
    </LogContext.Provider>
  );
}

export default LogContext;

 

728x90
반응형