[React Native] 데이터 설계3 - context 활용 이해
Nadan Dev Blog
Context 활용 이해
● Context 설계 이해
갑자기 AuthContext가 생기지 않는다. AuthForm이 갑자기 생기는 것도 아님. 외워서, 배운대로, 기억나는대로 코드를 짜면 스스로 논리를 만들어 갈 수 없음. 힘들어도 처음에는 하나에서 시작하고, 익숙해지면 그 하나에서 먼저 앞서가서 머리에서 분리를 하는 것.
AuthForm, NavLink는 뚝 떨어진 게 아니고 SignIn과 SignUp에서 공통으로 사용하니까 컴포넌트로 분리한 것일 뿐임.
그럼 당연히 AuthForm에서 사용할 state도 SignIn, SignUp 소속이었던 것임
버튼을 누르면 SignIn, SignUp에서는 signin, signup이라는 함수를 호출할 것. 이것을 우리가 배웠던 reducer화 시키는 것임. reducer로 만들면 dispatch를 통해서 두 함수를 한 곳에서 관리할 수 있음. 따라서, 처음부터 AuthContext가 생기는 것이 아니고, 로그인/로그아웃/계정을 관리하는 함수들이 각각 reducer가 되고, 공통으로 모아서 Context로 만드는데, 적당한 이름을 찾아보면 Acoount/Login/Auth 정도가 있을 것. 그래서 AuthContext가 되는 것. 처음부터 이게 필요하다고 생각하면 안 되고, 하나의 파일에 다 작성한다고 생각해야 스스로 논리를 만들어갈 수 있음
context 실전 구현 방법
1) createDataContext
- Context 생성
-
state, dispatch 생성
- 넘겨 받은 데이터 함수(action), 초기값, reducer 함수를 useReducer에 넘겨주고
- state, dispatch 리턴
-
action 함수 bound
- 데이터 함수(action)를 리턴하는 함수 리스트를 map
- dispatch를 호출할 수 있도록 argument로 넘겨주고
- action 함수를 리턴
- boundAction Object에 넣어서 리턴
-
Context.Provider 리턴
- value 값으로 state와 action 설정
- children으로 App 혹은 Provider 설정
- Context.Provider 리턴
- Context, Provider Object 리턴
2) X-Context
-
데이터 함수(action)를 리턴하는 함수 생성
- dispatch를 넘겨받고
- dispatch를 호출할 수 있는 데이터 함수(action) 리턴
-
reducer 함수 생성
- reducer 설계
- increment, decrement, add, delete 등 action으로 type을 결정
- CRUD reducer
read → state.find
const blogPost = state.find((blogPost) => blogPost.id === navigation.getParam('id'));
add → …기존 리스트, 새 값
delete → filter로 해당 id는 빼고 기존 리스트 리턴
update → map으로 해당 id 값을 변경한 리스트 리턴
const blogReducer = (state, action) => { switch (action.type) { case 'add_blogPost': return [...state, { id: Math.floor(Math.random() * 99999), title: action.payload.title, content: action.payload.content }] case 'delete_blogPost': return state.filter((blogPost) => blogPost.id !== action.payload); case 'edit_blogPost': return state.map((blogPost) => blogPost.id === action.payload.id ? action.payload : blogPost); default: return state; } }
- createDataContext 호출
초기값 설정
위에서 만든 action 함수 넘겨주기
reducer 함수 넘겨주기
Context, Provider 리턴
3) App.js
createAppContainer에 Provider가 App을 감싸게 해줘서 context 단위에서 state와 action을 사용할 수 있도록 변경
4) X-Screen
Context import
useContext import
useContext에 사용하려고 하는 Context를 넘겨주고 state와 함수 리턴
● Context 초기값 설정 활용
초기값을 설정해줘서 매번 입력하지 않도록 할 수 있다
export const { Context, Provider } = createDataContext(
blogReducer,
{ addBlogPost, deleteBlogPost },
[{ title: 'TEST POST', content: 'TEST CONTENT', id: 1 }]);
● Context flow 이해(역순)
Screen에서 useContext를 통해 Context 함수 호출
-> dispatch 호출
-> reducer 호출
● Context를 언제 사용해야하는지
reducer action 함수라고 무조건 dispatch를 하는게 아니라, 서버 호출이 될 수도 있다. context에는 꼭 state를 변경하지 않아도 관련 함수들을 넣어둔다고 생각할 수 있다. tryLocalSignIn이나 api POST 호출을 생각해 보면 꼭 dispatch를 호출한 것이 아니고 데이터 처리를 담당하는 것을 알 수 있다. 함수를 짤 때 context에 중앙 관리할 만한 함수인지 생각해보자
● 실전 예시1)
AuthContext.js
import createDataContext from './CreateDataContext';
const authReducer = (state, action) => {
switch (action.type) {
default:
return state;
}
};
const signup = (dispatch) => {
return ({ email, password }) => {
// make api request to sign up with that email and passwore
// if we sign up, modify our state, and say that we are authenicated
// if signing up fails, we need to reflect and error message somewhere
}
}
const signin = (dispatch) => {
return ({ email, password }) => {
// Try to signin
// Handle success by updating state
// Handle failure by showing error message
}
}
const signout = (dispatch) => {
return () => {
}
}
export const { Provider, Context } = createDataContext(
authReducer,
{ signin, signup, signout },
{ isSignedIn: false }
)
● Context 간 연동
나 스스로 해도 이렇게 구분할 수 있을까 생각해보자
LocationContext에 함수를 추가할 수 있지만 따로 TrackContext를 만들고 Context간의 연동에 대해 알아보자
TrackForm.js
const TrackForm = () => {
const { state : { name, recording, locations },
startRecording, stopRecording, changeName } = useContext(LocationContext);
return (
<>
<Input
placeholder="Enter Name"
onChangeText={(text) => changeName(text)}
value={name}
onKeyPress={() => startRecording()}
/>
{ recording ?
<Button title="Stop" onPress={() => stopRecording() }/> :
<Button title="Start" onPress={() => startRecording() }/>
}
{
!recording && locations.length ?
<Button title="Save Recording" /> :
null
}
</>
)
}
TrackContext.js
import createDataContext from './CreateDataContext';
const trackReducer = (state, action) => {
switch (action.type) {
default:
return state;
};
};
const fetchTracks = dispatch => () => {
};
const createTrack = dispatch => () => {
};
export const { Provider, Context } = createDataContext(
trackReducer,
{ fetchTracks, createTrack },
[]
)
App.js
import { Provider as TrackProvider } from '../src/context/TrackProvider';
...
export default () => {
return (
<TrackProvider>
<LocationProvider>
<AuthProvider>
<App ref={(navigator) => { setNavigator(navigator) }}/>
</AuthProvider>
</LocationProvider>
</TrackProvider>
)
}
● hook을 이용한 Context 간 연동
hook을 리턴할 때 [] 를 쓰는 것이 컨벤션
hook을 이용한다는 것이 두 context의 데이터와 함수를 모아서 편의 기능을 하나 만드는 것임
useSaveTrack.js
import { useContext } from 'react';
import { Context as TrackContext } from '../context/TrackContext';
import { Context as LocationContext } from '../context/LocationContext';
export default () => {
const { createTrack } = useContext(TrackContext);
const { state: { locations, name }} = useContext(LocationContext);
const saveTrack = () => {
createTrack(name, locations);
};
return [saveTrack];
};
TrackForm.js
const TrackForm = () => {
const { state : { name, recording, locations },
startRecording, stopRecording, changeName } = useContext(LocationContext);
const [saveTrack] = useSaveTrack();
return (
<>
<Input
placeholder="Enter Name"
onChangeText={(text) => changeName(text)}
value={name}
onKeyPress={() => startRecording()}
/>
{ recording ?
<Button title="Stop" onPress={() => stopRecording() }/> :
<Button title="Start" onPress={() => startRecording() }/>
}
{
!recording && locations.length ?
<Button title="Save Recording" onPress={() => saveTrack()}/> :
null
}
</>
)
}
● 유의할 점1 : 무조건 context만 써야 하는 것은 아님
처음 context를 접했을 때 너무나 신세계였고 모든 데이터를 context로 관리하려고 했다. 하지만 각각 컴포넌트에 종속되는 state라면 굳이 context에 사용할 필요 없이 useState로 관리해주면 된다.
import { StyleSheet, Text, TextInput, View, Button } from 'react-native';
const CreateScreen = ({ navigation }) => {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
return (
<View>
<Text style={styles.label}>Enter Title</Text>
<TextInput
style={styles.input}
value={title}
onChangeText={(text) => setTitle(text)} />
<Text style={styles.label}>Enter Content</Text>
<TextInput
style={styles.input}
value={content}
onChangeText={(text) => setContent(text)} />
<Button title="Add Blog Post" />
</View>
)
};
● 유의할 점2 : 초기값으로 여러개 설정할 수 있음
처음에 초기값을 [ ] 배열로 넘겨줘야 하는 줄 알고 list와 detail를 각각 다른 context 사용한 경우가 있음. { } object로 넘겨주기 때문에 초기값으로 필요한 데이터를 모두 정의할 수 있음.
● Context 실전 적용 고민 : tryLocalSignIn의 위치
이거 고민해보면 Context에 들어가야 하는 함수에 대해 깊이 고민해 볼 수 있음
- Auth와 관련된 함수를 여기서 모아서 해야 하나?
- 그런데 굳이 navigation, AsyncStorage 등 외부 라이브러리를 다 모아둘 필요가 있나?
- 한번만 사용하는데 중앙 관리할 필요가 있나?
- 아니지, 원래 Auth에 쓰는 것들을 모아둔 곳인데… 아닌가?
- 하지만, 두 라이브러리만 사용한다면 그럴 듯 함
const tryLocalSignin = dispatch => async () => {
const token = await AsyncStorage.getItem('token');
if (token) {
dispatch({ type: 'signin', payload: token });
navigate('TrackList');
} else {
navigate('loginFlow')
}
};
...
export const { Provider, Context } = createDataContext(
authReducer,
{ signin, signup, signout, clearErrorMessage, tryLoalSignin },
{ token: null, errorMessage: '' }
)
● Context 실전 적용 고민 : Context와 컴포넌트의 state 관리
이번 track-ex2는 context 활용을 매우 극대화해서 각 컴포넌트에서 각자 state에 접근했다. 이렇게 하면 screen에서 관리하지 않고 직접 데이터에 접근한다는 장점이 있는데, 문제는 흐름이 명시적으로 만들어지지 않아 어디서 문제가 발생했는지 흐름을 타고 발견하기가 좀 어려움. 즉, TrackCreate에서 데이터를 선언하고 TrackForm, Map으로 흘려 보내면 아 여기서 문제가 생겼군 하는게 빨리 판단이 서는데, TrackForm이랑 Map에서 따로따로 관리하다보니 누가 먼저 잘못했는지, 누가 먼저 데이터를 건드렸는지를 모르겠음. 잘 절충해서 사용하기 바람
추가) 데이터 시작, 멈춤, 추가는 TrackForm에서 보여주기는 Map에서, 데이터는 useLocation에서 갱신 이렇게 역할을 나눠서 컴포넌트화 했다면, 문제 발생시 좀 더 찾아가기 쉬웠을 듯