[React Native] 데이터 설계2 - callback, reducer 활용

Nadan
Nadan Dev Blog

개요

모든 state를 하나의 screen 페이지에서 다루면 코드가 복잡해진다. 컴포넌트간 상호작용이 있을 때는 state 값을 다른 컴포넌트에서 변경하는 경우가 생기기도 하고, sqlite, api에서 불러온 값을 여러 screen에 뿌려서 사용하기도 한다. 어떤 방법이 있는지 알아보자. functional component 기준임.

1) Callback

  1. 부모에 useState로 state 초기화
  2. 부모에 state를 변경할 수 있는 callback 함수 정의
  3. 자식에 callback 함수 prop으로 넘겨주기
  4. 자식에서 이벤트 발생시 callback 함수 호출

2) useReducer 이용

1) reducer 정의

이름이 좀 납득하기 어려워서 그렇긴 한데, state 값 변경을 ‘담당’하는 ‘함수’이다. 이 때 두 가지 object argument를 전달받아 호출되는데, 첫번째 argument는 ‘state’ 값을 포함한 object이고, 두번째 argument는 변경하고 싶은 값을 담은 object이다.

2) reducer 호출시 주의할 점

reducer에서 주의할 점 두 가지가 있는데, 첫번째는 절대 ‘state’ 값을 직접 변경하면 안 된다는 점이다. 예를 들어 state.cound = 3 이런식으로 하면 안 된다. state를 사용해본 사람이라면 당연하게 받아들일 수 있다. 두 번째는 항상 state값을 리턴해야 한다는 것이다. 만약 변경된 값이 없다면 그냥 기존의 state를 리턴하고, 값을 변경했다면 기존 state에 변경된 값을 더해 리턴해 줘야 한다.

3) reducer가 사용되는 상황 분석

예를 들어 정수 x, y 값을 +1, -1하는 counter가 있다고 가정해보자. state 값은 x, y가 될 것이고, 각 +, - 버튼이 호출하는 함수는 add, minus가 될 것이다. 상황을 다음 두 가지 기준으로 일반화 해보면 1) state x, y가 호출하는 함수가 비슷하다. 둘 다 이름만 다를 뿐 결국 +1, -1 하는 변수인 것이다. 2) 호출하는 함수가 2가지로 정의된다. 다른 말로는 행위가 정확하게 두가지로 쪼개진다.

당연한 이야기지만, 굳이 reducer를 사용하지 않아도 앱 개발에는 문제가 없다. 다만 이 때 x, y 처리를 각각 다른 곳에서 하면 관리가 쉽지 않다. 따라서 state 변경하는 곳을 reducer라는 하나의 함수에서 담당하고, 호출할 때 1) 어떤 행위를 하고 싶은지와 2) 어떤 값으로 변경하고 싶은지 이렇게 두 가지 argument를 넘겨주면 된다. 1)에서 어떤 행위를 하고 싶은지를 주로 action type라고 하고, 2) 어떤 값으로 변경하고 싶은지를 payload라고 한다.

예제로 다시 설명하면 action type은 addx, minusx, addy, minusy가 될 것이다.

4) reducer 사용 방식

react에서 제공해주는 hook인 useReducer를 사용한다. useReducer는 두 가지 argument를 받아 사용되는데, 첫번째 argument는 reducer 함수이고, 두 번째 argument는 사용할 state와 초기값들이다. useReducer를 호출하면 두 가지를 리턴해주는데, state와 dispatch 함수이다. state는 두 번째 argument로 넘겨준 값이고, dispatch는 reducer에게 action과 payload를 전달해 주는 함수이다.

flow는 이러하다. 원래는 setX, setY와 같이 state 변경 함수를 직접 호출했었다. 이제 state 값 변경은 reducer 함수에서 하고, 직접 호출하는 함수는 dispatch가 된다. dispatch에 action과 payload를 전달함으로써 reducer 함수가 어떤 state를 어떤 값으로 변경할지 알고, 다시 state를 return 함으로써 state가 갱신된다.

결국 reducer의 action이 될 수 있는 함수, 행위를 생각해야 한다.

5) useState vs useReducer

useReducer는 결국 useState를 어떻게 사용하는지로부터 왔다. 왜 useReducer로 변경하는지 코드를 비교하면서 원리를 파악할 필요가 있다. 최종적으로는 코드가 많아지고, reducer라는 함수가 추가된 것처럼 보인다. 하지만 실제 프로젝트상에서 state 값이 복잡하고 각 state별 행위가 많아질 경우 reducer를 사용하면 state 관리가 월등히 편해진다.

const [x, setX] = useState(0);
const [y, setY] = useState(0);

const addX = () => setX(x + INCREMENT);
const minusX = () => setX(x - INCREMENT);
const addY = () => setY(y + INCREMENT);
const minusY = () => setY(y - INCREMENT);

<View>
    <Calculator 
        value={x} 
        onAdd={() => addX()} 
        onMinus={() => minusX()} 
    />
    <Calculator 
        value={y} 
         onAdd={() => addY()} 
        onMinus={() => minusY()} 
    />
</View>
// reducer 함수 선언
const reducer = (state, action) => {
    switch (action.type) {
        case 'add_x':
            return {...state, x: state.x + action.payload};
        case 'minus_x':
            return {...state, x: state.x - action.payload};
        case 'add_y':
            return {...state, y: state.y + action.payload};
        case 'minus_y':
            return {...state, y: state.y - action.payload};
        default:
            return state;
    }
}

// reducer 초기화
const [state, dispatch] = useReducer(reducer, { x: 0, y: 0 });
const { x, y } = state;

// dispatch 호출
const addX = () => dispatch({ type: 'add_x', payload: x + INCREMENT });
const minusX = () => dispatch({ type: 'minus_x', payload: x - INCREMENT });
const addY = () => dispatch({ type: 'add_y', payload: y + INCREMENT });
const minusY = () => dispatch({ type: 'minus_y', payload: y + INCREMENT });

<View>
    <Calculator 
        value={x} 
        onAdd={() => addX()} 
        onMinus={() => minusX()} 
    />
    <Calculator 
        value={y} 
         onAdd={() => addY()} 
        onMinus={() => minusY()} 
    />
</View>