[React Native] 데이터 설계3 - context 만들기

Nadan
Nadan Dev Blog

3) Context

● React Redux 이해

리액트에서 Redux 다시 공부하고 올 것

● Prop과 비교

이런 생각이 들 수 있다. 여태껏 배운 prop을 통해 navigation params로 데이터를 넘겨주면 되지 않느냐 할 수 있는데, 앱이 간단하고 layer가 몇 개 없으면, 컴포넌트가 몇 개 없으면 이게 오히려 더 좋은 방법일 수 있다. 하지만 layer가 많아질 경우, 최종 layer에 데이터를 전달하기 위해 중간 레이어에 필요 없는 데이터가 전달된다면 매우 복잡한 코드가 될 것은 자명하다.

context를 활용하면 초기 setup은 복잡할 수 있지만 데이터가 필요한 곳에서 부모와 상관 없이 데이터를 받을 수 있기 때문에 관리가 매우 쉬워진다.

● Context를 사용하는 이유

-> 이미지는 네 예시로 직접 만들 것

context 1

다음과 같이 페이지가 4개가 있는 앱을 생각해보자. 데이터는 4개의 스크린에서 모두 사용해야 한다면, 데이터 관리를 어떻게 해줘야 할까?

context 2

데이터를 Index 스크린에서 받아오기 때문에 관리를 Index 스크린에서 해준다면 화면 간의 coupling이 매우 심해진다. 기능을 추가하고 싶은 경우, 예를 들어 댓글이나 이미지 같은 것, 더더욱 복잡해짐.

이렇게 데이터를 중앙에서 관리하고 필요한 페이지마다 중앙 데이터 관리처에서 가져다 사용할 수 있도록 해주는 것이 Provider 이다.

context 3

context 4

위 이미지를 보면 Index 스크린에서 데이터를 받지 않고, Provider에서 받아오고, 또 데이터 갱신이 필요한 경우 Provider에 요청한다.

Context 구현하기

1) BlogContext 생성

import React from 'react';

const BlogContext = React.createContext();

export const BlogProvider = ({ children }) => {
    return (
        <BlogContext.Provider>
            {children}
        </BlogContext.Provider>
    )
}

2) App.js에 Provider 등록

앱 전체를 감싸는 고 유의 컴포넌트 만들기. createAppContainer가 하는 일이 컴포넌트를 리턴하는 것임

createAppContainer를 아래와 같이 변형

const App = createAppContainer(navigator);

export default () => {
  return <App/>;
}

만약 Provider가 여러개라면 App을 감싸주기만 하면 되고 서로 부모-자식 어떤 걸로 감싸든 상관 없음

import React from 'react';
import { createAppContainer } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';
import IndexScreen from './src/screens/IndexScreen';
import { BlogProvider } from './src/context/BlogContext';

const navigator = createStackNavigator({
  Index: IndexScreen
}, {
  initialRouteName: 'Index',
  defaultNavigationOptions: {
    title: 'Blog'
  }
});

const App = createAppContainer(navigator);

export default () => {
  return (<BlogProvider><App/></BlogProvider>);
};

3) Provider에서 데이터 전달하기

Provider가 어떻게 데이터를 전달하는지 보자. BlogContext에서 IndexScreen으로 value를 넘겨준다. 어려운 용어보다 데이터 흐름에 집중하자

BlogContext.js

import React from 'react';

const BlogContext = React.createContext();

export const BlogProvider = ({ children }) => {
    return (
        <BlogContext.Provider value={5}>
            {children}
        </BlogContext.Provider>
    )
};

export default BlogContext;

IndexScreen.js

import BlogContext from '../context/BlogContext'

const IndexScreen = (props) => {

    const value = useContext(BlogContext);

    return (
        <View>
            <Text>Index</Text>
            <Text>{value}</Text>
        </View>
    )
};

Object는 어떻게 전달해야 할까?

주의할 점은 jsx로는 Object를 못 전달한다. 그렇다고 Object를 못 전달하는 것은 아니고, 직접적으로 jsx에 넣지만 않으면 됨. 리스트를 사용하면 된다.

BlogContext.js

export const BlogProvider = ({ children }) => {

    const blogPosts = [
        { title: 'Blox Post #1' },
        { title: 'Blox Post #1' }
    ]

    return (<BlogContext.Provider value={blogPosts}>{children}</BlogContext.Provider>)
};

IndexScreen.js

const IndexScreen = (props) => {

    const blogPosts = useContext(BlogContext);
    return (
        <View>
            <FlatList 
                data={blogPosts}
                keyExtractor={(blogPost) => blogPost.title}
                renderItem={({ item }) => {
                    return <Text>{item.title}</Text>
                }}
            />
        </View>
    )
};

4) IndexScreen에서 데이터 갱신하기

context 6

context에서 state를 가지고 있고, 스크린에서 이벤트에 따라 state를 변경할 수 있도록 함수를 내려보내주면, 스크린에서 해당 함수를 호출하고, 그러면 state가 갱신되고, 그러면 다시 화면이 갱신. 결국 원리는 state를 갱신하면 화면이 갱신되는 것을 이용하는 것

BlogContext.js

export const BlogProvider = ({ children }) => {

    const [blogPosts, setBlogPosts] = useState([]);

    const addBlogPost = () => {
        setBlogPosts([...blogPosts, { title: `Blog Post #${blogPosts.length + 1}` }])
    }

    return (<BlogContext.Provider value={{ data: blogPosts, addBlogPost }}>{children}</BlogContext.Provider>)
};

IndexScreen.js

const IndexScreen = (props) => {

    const { data, addBlogPost } = useContext(BlogContext);

    return (
        <View>
            <Button title="Add Post" onPress={() => addBlogPost() }/>
            <FlatList 
                data={data}
                keyExtractor={(blogPost) => blogPost.title}
                renderItem={({ item }) => {
                    return <Text>{item.title}</Text>
                }}
            />
        </View>
    )
};

5) useReducer를 통해 여러 state를 한 곳에서 관리해주기

context 7

여기까지만 해도 사실 context를 사용할 수 있다. 하지만 state가 많아지고, 관리해야 할 함수가 많아지면, 더 나아가 context가 여러개로 많아지면 코드가 중복되고 관리하기가 힘들어 진다. 따라서 몇 스텝만 더 나가면 개발과 관리를 더욱 수월하게 할 수 있다.

이 때 활용할 수 있는 것 중 하나가 앞서 배운 reducer이다. reducer를 통해 state를 한 곳에서 관리할 수 있다고 배웠다. reducer를 통해 type과 payload만 전달해주면 reducer에서 함수 호출과 state 갱신을 알아서 처리해주기 때문에 수정이 필요할 경우 state와 함수를 찾아다닐 필요가 없다.

구현은 다음과 같이 할 수 있다.

BlogContext.js

const blogReducer = (state, action) => {

    switch (action.type) {
        case 'add_blogPost':
            return [...state, { title: `Blog Post #${state.length + 1}` }]
        default:
            return state;
    }
}

export const BlogProvider = ({ children }) => {

    // state === blogPosts
    const [state, dispatch] = useReducer(blogReducer, []);

    const addBlogPost = () => {
        dispatch({ type: 'add_blogPost' })
    }

    return (<BlogContext.Provider value={{ data: state, addBlogPost }}>{children}</BlogContext.Provider>)
};

6) Context 생성 프로세스 자동화하기

Context는 하나의 데이터 흐름을 담당하기 때문에 다른 데이터 흐름이 발생할 경우, 예를 들어 댓글, 이미지 등이 생기면 다른 context를 만들어야 함. 그 때마다 같은 프로세스를 거쳐서 만들 것이기 때문에 자동화 해두면 편하다

createDataContext.js

import React, { useReducer } from 'react';

export default (reducer, actions, initialState) => {

    const Context = React.createContext();

    const Provider = ({ children }) => {
        const [state, dispatch] = useReducer(reducer, initialState);
    
        // actions === { addBlogPost: (dispatch) => { return () => () } }
        const boundActions = {};
        for ( let key in actions) {
            boundActions[key] = actions[key](dispatch)
        }

        return <Context.Provider value={{ state, ...boundActions }}>{children}</Context.Provider>
    }

    return { Context, Provider }
}

7) 전체 코드

BlogContext.js

import React, { useReducer } from 'react';
import createDataContext from './createDataContext';

const blogReducer = (state, action) => {
    switch (action.type) {
        case 'add_blogPost':
            return [...state, { title: `Blog Post #${state.length + 1}` }]
        default:
            return state;
    }
}

const addBlogPost = (dispatch) => {
    return () => {
        dispatch({ type: 'add_blogPost' })
    };
}

export const { Context, Provider } = createDataContext(blogReducer, { addBlogPost }, []);

IndexScreen.js

import { Context as BlogContext } from '../context/BlogContext'

const IndexScreen = (props) => {

    const { state, addBlogPost } = useContext(BlogContext);

		return (
		...
}

App.js

import { Provider } from './src/context/BlogContext';

const navigator = createStackNavigator({
  Index: IndexScreen
}, {
  initialRouteName: 'Index',
  defaultNavigationOptions: {
    title: 'Blog'
  }
});

const App = createAppContainer(navigator);

export default () => {
  return (<Provider><App/></Provider>);
};

createDataContext.js

import React, { useReducer } from 'react';

export default (reducer, actions, initialState) => {

    const Context = React.createContext();

    const Provider = ({ children }) => {
        const [state, dispatch] = useReducer(reducer, initialState);
    
        // actions === { addBlogPost: (dispatch) => { return () => () } }
        const boundActions = {};
        for ( let key in actions) {
            boundActions[key] = actions[key](dispatch)
        }

        return <Context.Provider value={{ state, ...boundActions }}>{children}</Context.Provider>
    }

    return { Context, Provider }
}