0. Context API & AsyncStorage?
Context API
React에서 Props와 State는 부모 컴포넌트와 자식 컴포넌트 또는 컴포넌트 안에서 데이터를 다루기 위해
사용된다.
즉, 부모 컴포넌트에서 자식 컴포넌트로, 위에서 아래로 한 방향으로만 데이터가 흐르게 된다.
하지만 Context API는 부모 컴포넌트에서 자식 컴포넌트로 전달되는 데이터의 흐름과는 상관없이,
전역으로 사용되는 데이터를 다룬다.
전역 데이터를 저장한 후, 컴포넌트에서 필요한 데이터를 불어와 사용한다.
AsyncStorage
React에서 Props와 State, Context는 모두 휘발성으로 데이터가 메모리에만 존재하며, 물리적으로 데이터를
저장하지는 않는다.
AsyncStorage는 앱 내에서 키 값 (Key-Value)으로 간단하게 데이터를 저장할 수 있는 저장소이다.
웹에서 사용하는 localStorage와 유사하다.
1. 프로젝트 준비
react-native init TodoList --template react-native-template-typescript
React Native CLI 명령어를 사용해서 Todo 프로젝트를 생성한다.
yarn add styled-components
yarn add -D @types/styled-components @types/styled-components-react-native babel-plugin-root-import
개발을 편하게 할 수 있는 패키지도 추가로 설치한다.
설치가 완료되면 절대 경로 설정을 위해서 babel.config.js 파일을 열고 수정한다.
module.exports = {
// ...
plugins: [
[
'babel-plugin-root-import',
{
rootPathPrefix: '~',
rootPathSuffix: 'src',
},
],
],
};
그리고 마찬가지로 tsconfig.js도 수정을 해준다.
// prettier-ignore
{
"compilerOptions": {
// ...
"baseUrl": "./src",
"paths": {
"~/*":["*"]
},
// ...
},
"exclude": [
"node_modules", "babel.config.js", "metro.config.js", "jest.config.js"
]
}
2. 개발
1. AsyncStorage 설치 및 설정
yarn add @react-native-async-storage/async-storage
명령어를 실행해서 AsyncStorage를 설치한다.
만약 macOS를 사용하는 iOS 환경이라면
npx pod-install
명령어를 한번 더 실행해야 한다.
2. Context
// src/Context/TodoListContext/@types/index.d.ts
interface ITodoListContext {
todoList: Array<string>;
addTodoList: (todo: string) => void;
removeTodoList: (index: number) => void;
}
데이터 타입을 타입스크립트로 정의할 때 일반적으론 같은 파일에 정의를 했다.
같은 파일에 타입을 정의하면 정의한 타입은 해당 파일 안에서만 타입을 사용할 수 있다.
하지만 @types/index.d.ts 파일을 만들고 해당 파일 안에 타입을 정의하면 프로젝트 전체가 타입을 사용할 수 있다.
Context의 데이터는 프로젝트 전체가 사용하므로 따로 정의하였다.
// src/Context/TodoListContext/index.tsx
import React, {createContext, useState, useEffect} from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface Props {
children: JSX.Element | Array<JSX.Element>;
}
const TodoListContext = createContext<ITodoListContext>({
todoList: [],
addTodoList: (todo: string): void => {},
removeTodoList: (index: number): void => {},
});
const TodoListContextProvider = ({children}: Props) => {
const [todoList, setTodoList] = useState<Array<string>>([]);
const addTodoList = (todo: string): void => {
const list = [...todoList, todo];
setTodoList(list);
AsyncStorage.setItem('todoList', JSON.stringify(list));
};
const removeTodoList = (index: number): void => {
let list = [...todoList];
list.splice(index, 1);
setTodoList(list);
AsyncStorage.setItem('todoList', JSON.stringify(list));
};
const initData = async () => {
try {
const list = await AsyncStorage.getItem('todoList');
if (list !== null) {
setTodoList(JSON.parse(list));
}
} catch (e) {
console.log(e);
}
};
useEffect(() => {
initData();
}, []);
return (
<TodoListContext.Provider
value={{
todoList,
addTodoList,
removeTodoList,
}}>
{children}
</TodoListContext.Provider>
);
};
export {TodoListContextProvider, TodoListContext};
Context/TodoListContext 아래에 index.tsx로 TodoContext를 만들었다.
import React, {createContext, useState, useEffect} from 'react';
Context를 만들기 위해서 createContext를 사용하고, useState로 생성한 데이터를 Context에 저장한다.
최초 실행 시 useEffect를 사용해서 AsyncStorage에 저장된 데이터가 있다면 Context에 넣어준다.
const TodoListContext = createContext<ITodoListContext>({
todoList: [],
addTodoList: (todo: string): void => {},
removeTodoList: (index: number): void => {},
});
createContext에 초기값을 할당해서 Context를 생성할 수 있다.
이때 @types/index.d.ts에 정의한 타입으로 데이터 타입을 지정하였다.
const TodoListContextProvider = ({children}: Props) => {
// ...
return (
<TodoListContext.Provider
value={{
todoList,
addTodoList,
removeTodoList,
}}>
{children}
</TodoListContext.Provider>
);
};
Context를 사용하기 위해서는 공통 부모 컴포넌트에서 Context의 프로바이더를 사용한다.
TodoListContextProvider는 Context의 프로바이더 컴포넌트로서 사용된다.
따라서 자식 컴포넌트를 children 매개변수를 통해서 전달받는다. 전달받은 컴포넌트를 createComponent로
생성한 TodoListContext.Provider 하위에 위치하도록 설정했다.
const [todoList, setTodoList] = useState<Array<string>>([]);
Context를 사용하기 위해서 만든 TodoListContextProvider도 컴포넌트이므로, 수정 가능한 데이터를
사용하기 위해서 useState를 사용했다.
TodoList는 문자열 배열 타입을 가지고 있으므로 Array <string>으로 선언하였다.
const addTodoList = (todo: string): void => {
const list = [...todoList, todo];
setTodoList(list);
AsyncStorage.setItem('todoList', JSON.stringify(list));
};
useState로 만든 todoList는 수정할 수 없는 불변 값이다.
그래서 새로운 list 변수를 만들어 todoList의 데이터를 넣어주고(... todoList ), 새로운 데이터( todo )를
추가했다.
마지막으로 AsyncStorage의 setItem을 통해서 데이터를 물리적으로 저장한다.
이때 문자열 배열인 데이터는 JSON.stringfy 함수를 사용해 문자로 변경 후 저장한다.
const removeTodoList = (index: number): void => {
let list = [...todoList];
list.splice(index, 1);
setTodoList(list);
AsyncStorage.setItem('todoList', JSON.stringify(list));
};
Todo List에서 삭제할 데이터를 전달받아 지운 뒤 다시 AsyncStorage에 업데이트를 한다.
const initData = async () => {
try {
const list = await AsyncStorage.getItem('todoList');
if (list !== null) {
setTodoList(JSON.parse(list));
}
} catch (e) {
console.log(e);
}
};
앱이 시작될 때, AsyncStorage에 저장된 데이터를 불러와서 Context의 값을 초기화시켜준다.
AsyncStorage의 setItem과 getItem은 모두 Promise 함수이다.
하지만 setItem은 작업 후 특정한 작업을 추가로 하지 않으므로 비동기로 데이터를 처리했지만
getItem은 값을 바로 초기화하기 위해서 async-await을 사용해 동기로 데이터를 처리했다.
3. 프로바이더 설정
프로바이더는 Context를 공유할 컴포넌트들의 최상단 공통 부모 컴포넌트에서 사용한다.
이번 앱의 최상단 공통 부모 컴포넌트인 src/App.tsx에서 사용하게끔 설정한다.
import React from 'react';
import styled from 'styled-components/native';
import {TodoListContextProvider} from './Context/TodoListContext';
// import Todo from './Screens/Todo';
const Container = styled.View`
flex: 1;
background-color: #eee;
`;
const App = () => {
return (
<TodoListContextProvider>
<Container>{/* <Todo /> */}</Container>
</TodoListContextProvider>
);
};
export default App;
Context에서 프로바이더 컴포넌트를 불러와, 최상단 공통 부모 컴포넌트로 사용했다.
이제 App.tsx 컴포넌트를 부모로 사용하는 모든 컴포넌트에서 Context API를 사용할 수 있다.
4. Todo 컴포넌트
import React from 'react';
import styled from 'styled-components/native';
// import TodoListView from './TodoListView';
// import AddTodo from './AddTodo';
const Container = styled.View`
flex: 1;
`;
interface Props {}
const Todo = ({}: Props) => {
return (
<Container>
{/* <TodoListView />
<AddTodo /> */}
</Container>
);
};
export default Todo;
Todo 컴포넌트는 List를 보여줄 TodoListView 컴포넌트와 Todo를 추가할 수 있는 AddTodo로 나뉜다.
5. TodoListView 컴포넌트
import React from 'react';
import styled from 'styled-components/native';
// import Header from './Header';
// import TodoList from './TodoList';
const Container = styled.SafeAreaView`
flex: 1;
`;
interface Props {}
const TodoListView = ({}: Props) => {
return (
<Container>
{/* <Header />
<TodoList /> */}
</Container>
);
};
export default TodoListView;
마찬가지로 TodoListView 역시 Header와 TodoList 컴포넌트로 나누어진다.
6. Header 컴포넌트
import React from 'react';
import styled from 'styled-components/native';
const Container = styled.View`
height: 40px;
justify-content: center;
align-items: center;
`;
const TitleLabel = styled.Text`
font-size: 24px;
font-weight: bold;
`;
interface Props {}
const Header = ({}: Props) => {
return (
<Container>
<TitleLabel>Todo List App</TitleLabel>
</Container>
);
};
export default Header;
Header 컴포넌트는 단순하게 Todo List App이라는 문자를 화면에 표시하기 위한 컴포넌트이다.
7. TodoList 컴포넌트
import React, {useContext} from 'react';
import styled from 'styled-components/native';
import {FlatList} from 'react-native';
import {TodoListContext} from '~/Context/TodoListContext';
import EmptyItem from './EmptyItem';
import TodoItem from './TodoItem';
const Container = styled(FlatList)``;
interface Props {}
const TodoList = ({}: Props) => {
const {todoList, removeTodoList} =
useContext<ITodoListContext>(TodoListContext);
return (
<Container
data={todoList}
keyExtractor={(item, index) => {
return `todo-${index}`;
}}
ListEmptyComponent={<EmptyItem />}
renderItem={({item, index}) => (
<TodoItem
text={item as string}
onDelete={() => removeTodoList(index)}
/>
)}
contentContainerStyle={todoList.length === 0 && {flex: 1}}></Container>
);
};
Context에 저장된 Todo 데이터를 화면에 표시하는 TodoList 컴포넌트이다.
const {todoList, removeTodoList} =
useContext<ITodoListContext>(TodoListContext);
useContext를 사용해 TodoListContext를 초기값을 설정하고 todoList 변수와 removeTodoList 함수를 불렀다.
import React, {useContext} from 'react';
import styled from 'styled-components/native';
import {FlatList} from 'react-native';
// ...
const Container = styled(FlatList)``;
// ...
const TodoList = ({}: Props) => {
//...
return (
<Container
data={todoList}
keyExtractor={(item, index) => {
return `todo-${index}`;
}}
ListEmptyComponent={<EmptyItem />}
renderItem={({item, index}) => (
<TodoItem
text={item as string}
onDelete={() => removeTodoList(index)}
/>
)}
contentContainerStyle={todoList.length === 0 && {flex: 1}}></Container>
);
};
React Native의 리스트 뷰 중 하나인 FlatList 컴포넌트를 사용해서 구성하였다.
data={todoList}
keyExtractor={(item, index) => {
return `todo-${index}`;
}}
ListEmptyComponent={<EmptyItem />}
renderItem={({item, index}) => (
<TodoItem
text={item as string}
onDelete={() => removeTodoList(index)}
/>
)}
contentContainerStyle={todoList.length === 0 && {flex: 1}}
- data : 리스트뷰에 표시할 데이터의 배열
- keyExtractor : React에 반복적으로 동일한 컴포넌트를 표시할 때 key를 설정해야 한다.
React는 이것을 보고 컴포넌트를 구별해서 컴포넌트를 업데이트하는데, FlatList는
keyExtractor를 사용해 반복적인 Item을 구별한다. - ListEmptyComponent : 주어진 배열(data)에 데이터가 없는 경우 표시되는 컴포넌트
- renderItem : 주어진 배열에 데이터를 사용해서 반복적으로 나타낼 컴포넌트
- contentContainerStyle : 표시될 데이터가 없을 경우 ListEmptyComponent가 화면에 나타나는데, 이것 역시 하나의
Item으로 나타나기 때문에 전체 화면으로 표시하기 위해서 flex : 1 옵션을 주었다.
8. EmptyItem 컴포넌트
import React from 'react';
import styled from 'styled-components/native';
const Container = styled.View`
flex: 1;
align-items: center;
justify-content: center;
`;
const Label = styled.Text``;
interface Props {}
const EmptyItem = ({}: Props) => {
return (
<Container>
<Label>하단에 " + " 버튼을 눌러 새로운 할 일을 등록해 보세요.</Label>
</Container>
);
};
export default EmptyItem;
데이터가 없을 때, 데이터를 추가하도록 안내하는 문구를 표시했다.
9. TodoItem 컴포넌트
import React from 'react';
import styled from 'styled-components/native';
const Container = styled.View`
flex-direction: row;
background-color: #fff;
margin: 4px 16px;
padding: 8px 16px;
border-radius: 8px;
align-items: center;
`;
const Label = styled.Text`
flex: 1;
`;
const DeleteButton = styled.TouchableOpacity``;
const Icon = styled.Image`
width: 24px;
height: 24px;
`;
interface Props {
text: string;
onDelete: () => void;
}
const TodoItem = ({text, onDelete}: Props) => {
return (
<Container>
<Label>{text}</Label>
<DeleteButton onPress={onDelete}>
<Icon source={require(`~/Assets/images/remove.png`)} />
</DeleteButton>
</Container>
);
};
export default TodoItem;
TodoList 컴포넌트에 데이터가 있을 경우, 표시할 TodoItem 컴포넌트이다.
interface Props {
text: string;
onDelete: () => void;
}
TodoList 컴포넌트로부터, 데이터를 전달받는데 화면에 나타낼 텍스트( text: string )와
데이터를 지우기 위해서 사용하는 삭제 함수 ( onDelete: () => void )를 전달받아온다.
여기서 사용되는 Icon은 이전 카운터 앱에서 사용한 아이콘을 다시 사용하였다.
'React Native > TypeScript' 카테고리의 다른 글
[React Native] Todo 앱 - Context & AsyncStorage_2편 (2) | 2022.07.20 |
---|---|
[React Native] 카운터 앱 - Props & State (1) | 2022.07.18 |