[React Native] 컴포넌트 이해 - 컴포넌트 설계/분리

Nadan
Nadan Dev Blog

1. 뷰 컴포넌트

jsx 작성할 때는 항상 ‘컴포넌트로 만들 수 있을까’를 생각한다. form? element? item? style? 이 소속 중 하나로 만들 수 있을까? form 만들 때면 컴포넌트화 할 수 있는지 고민하고, View를 그릴 때는 잘라서 element화 할 수 있는가, 공통 style로 만들 수 있는가

1) element

NavLink처럼 링크 하나를 분리할 수도 있고, 화면상 기능 하나를 통쨰로 분리할 수 있음. NavLink처럼 분리한 경우 다른 element 컴포넌트에서도 사용할 수 있음.

뷰를 그릴 때도 설계가 필요한데, 공통으로 사용할 뷰를 적절하게 분리해 낸다면 개발, 유지보수 시간이 빨라진다. 예를 들어, 모든 뷰가 Layout 컴포넌트로 둘러쌀 경우, 메인 색상이 바뀌었을 때 일일이 찾아가서 바꾸지 않아도 된다. 매우 간단한 텍스트 하나도 그러한데, 전체적으로 텍스트 폰트를 키우고 싶을 때 중앙에서 컴포넌트로 개발했다면 한 곳에서 변경하고 사용성을 빠르게 테스트 할 수 있을 것이다.

2) item

Flatlist, map을 사용하는 경우 item을 따로 분리해서 관리해준다.

3) form

input을 이루는 항목들을 모아서 하나의 form 컴포넌트화. 두 가지 생각이 드는데, 첫번째는 signin, signup 두 곳에서 모두 사용한다면 form으로 분리하는 게 의미가 있을 것이고, 그렇지 않다고 하더라도 screen에서 모든 뷰를 다루기에 너무 복잡하기 때문에 따로 분리하면 관리하기 수월할 것이다.

import React, { useState } from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
import { Text, Button, Input } from 'react-native-elements';

const LoginForm = ({ title, errorMessage, onSubmit, buttonText }) => {

    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');

    return (
        <>
            <Text h3>{title}</Text>
            <Input 
                autoCapitalize="none"
                autoCorrect={false}
                label="Email"
                value={email}
                onChangeText={(newText) => setEmail(newText)}/>
            <Input 
                secureTextEntry
                autoCapitalize="none"
                autoCorrect={false}
                label="Password"
                value={password}
                onChangeText={(newText) => setPassword(newText)}
                errorMessage={errorMessage ? <Text style={styles.errorMessage}>{errorMessage}</Text> : null}
            />
            <Button 
                title={buttonText}
                onPress={() => onSubmit({ email, password})}
            />
            <TouchableOpacity onPress={() => navigation.navigate('SignIn')}>
                <Text style={styles.link}>Sign in</Text>
            </TouchableOpacity>
        </>
    )
}

const styles = StyleSheet.create({
    errorMessage: {
        fontSize: 16,
        color: 'red'
    },
    link: {
        color: 'blue'
    }
});

export default AuthForm;

4) style

안드로이드의 styles와 매우 비슷하다. element를 분리했듯이 ‘공통’ 스타일을 분리해보자. 다음처럼 공통으로 사용할 margin 값을 분리해주면 앱의 전반적인 공간을 늘리고 싶을 때 여기서 관리해주면 개발이 수월해질 것이다.

import React from 'react';
import { View, StyleSheet } from 'react-native';

const Space_M1 = ({ children }) => {
    return <View stlye={stlyes.spacer}>{children}</View>
}

const stlyes = StyleSheet.create({
    spacer: {
        margin: 12
    }
});

export default Spacer;

2. 데이터(state, 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가 되는 것. 처음부터 이게 필요하다고 생각하면 안 되고, 하나의 파일에 다 작성한다고 생각해야 스스로 논리를 만들어갈 수 있음.

3. 기능 컴포넌트

1) hook

react native에서 hook이 여러 의미를 갖지만, 여기서는 두 context의 데이터와 함수를 모아서 하나의 편의 기능을 만드는 것을 뜻함.

예를 들어 그럴 경우는 거의 없겠지만 로그아웃을 하는 버튼이 여러곳에 있다고 가정해보자. 사용자가 로그아웃을 하고 다시 로그인을 할 수 있기 때문에 로그아웃을 할 때 반드시 캐시를 지워줘야 한다. signOut 함수는 AuthContext에 있을 것이고, 데이터 함수는 ***Context에 있을 것인데, 호출되는 곳마다 signOut은 AuthContext에서, 기타 데이터 캐시들은 각기 Context에서 가져오면 코드가 복잡해질 것이다. 이 때, signOutHook을 만들어서 한 번에 호출할 수 있도록 한다.

다시 로그인하면 state 값들이 당연히 초기화 되겠지만, 데이터가 복잡한 경우 이런 과정을 거쳐주는 것이 좋다.

import { useContext } from 'react';
import { Context as LoginContext } from '../context/LoginContext';
import { Context as CenterContext } from '../context/CenterContext';

export default () => {
    const { signOut } = useContext(LoginContext);
    const { clearCenter } = useContext(CenterContext);

    const saveTrack = () => {
        signOut();
      	clearCenter();
    };

    return [saveTrack];
};

2) util

#목적에 알맞게

navlink → 링크를 다루니까 콜백 함수를 받기보다 routeName을 받아서 직접 넘어가 주는 것이 목적에 부합할 듯