@ 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
# 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;
'React Native > React Native_study' 카테고리의 다른 글
[리액트 네이티브를 다루는 기술 #12] 7장 다이어리 앱 만들기2 (p .357 ~ 379) (2) | 2022.01.11 |
---|---|
[리액트 네이티브를 다루는 기술 #11] 6장 다이어리 앱 만들기1 (p.330 ~ 356) (0) | 2022.01.07 |
[리액트 네이티브를 다루는 기술 #9] 6장 다이어리 앱 만들기1 (p.285 ~ 305) (0) | 2021.12.31 |
[리액트 네이티브를 다루는 기술 #8] 5장 리액트 내비게이션으로 여러 화면 관리하기 (p.244 ~ 284) (0) | 2021.12.30 |
[리액트 네이티브를 다루는 기술 #7] 5장 리액트 내비게이션으로 여러 화면 관리하기 (p.215 ~ 243) (0) | 2021.12.27 |