리액트 네이티브로 모바일 앱 개발해보자, 꼬꼬꼬~~
모바일앱 개발의 양대산맥, 리액트 네이티브와 플러터
리액트로 웹 개발을 해봤다면 리액트 네이티브로, 뉴비라면 플러터로
둘 다 해보고 싶다면 why not.
참고 : 플러터 VS 리액트 네이티브, 2023년의 승자는? - https://youtu.be/Z9cCjrbTW50
자바스크립트만 알면, 모든 개발이 가능합니다. 개발자 되기 차암~ 쉽죠잉~
React.js - 웹 개발
React Native - aos, ios 모바일 앱 개발
node.js - 백엔드 서버 개발
아래 내용은 리액트 네이티브의 핵심 개념만 정리한 것이고
리액트 네이티브 개발을 시작해 보고 싶다면 - 추천 무료 강의
설치해 샘플 앱 실행시켜 보기
엑스포 vs 네이티브 - 당연히 엑스포 버리고 리액트 네이티브 선택, 뭔가 만들고 싶으면 결국엔 어느 정도 네이티브를 알아야 함. 간단한 앱은 엑스포 만으로 빠르게 만들 수 있다지만, 앱 만들다 보면 엑스포 만으로 만들 수 있는 앱이 거의 없음.
- Expo CLI : 초반 앱 개발 단순화 (필요한게 모두 들어 있음), 엑스포가 지원하지 못하는 기능 구현을 못함 (추가 네이티브 모듈 사용 불가)
- React Native CLI : 뭐든 가능, 지원되는 것이 없어서 필요한 기능 구현을 위해서 라이브러리들 설치 필요, 맥북 필수, 네이티브 기본 지식 필수
구글이나 애플 인앱결제를 붙이고 싶다, 구글 애드몹 광고를 붙이고 싶다 등등 - 결국 리액트 네이티브로 가야 함
엑스포의 장점은 맥이 없어도 아이폰 앱 개발이 가능하다는 점
하지만, 리액트 네이티브로 앱을 만들면 타입 스크립트로 생성이 되니, 일단은 엑스포로 만들어 자바 스크립트로 개발 하는 방법으로 먼저 진행.
자바 스크립트 아니, dart 새로 배우기 싫어서 리액트 네이티브 하는건데, 타입 스크립트 또 배워야 하면 뭐가 달라. 일단은 자바 스크립트 쓸 수 있는 엑스포로 먼저 배우가 타입 스크립트는 나중에 고민.
엑스포로 만든 소스도 나중에 엑스포를 버리고, 리액트 네이티브로 전환 가능.
설치해야 할 것이 졸라 많음 ㅠㅠ - 이걸 다 설치하고 정상적으로 세팅해야 앱 개발 가능. 건투를 빕니다.
개발자 되는데 좌절하기 쉬운 부분입니다. 준비를 하는데 이렇게 엄청난 노력이 필요.
- node.js
- JDK
- Android Studio
- Android SDK
- Android Simulator
- Ruby : rbenv 설치해서, 루비는 2.7.6을 설치해야 함 (어이 없게도 다른 버전 설치하면 정상 동작 안됨)
- Xcode
- iOS Simulator
- Cocoa Pods
샘플 앱을 만들어 보자 - 졸라 오래 걸림, 꾹 참고 기다리면 만들어짐
- Expo-CLI : npx create-expo-app my-app
- Native-CLI : npx react-native@latest init my-app
만든 샘플 앱을 실행 - 아 이건 뭐, 더 머리 아프네. 안되는거 투성 (안되는건 구글링으로 한땀, 한땀 해결)
- Expo-CLI
- npx expo start - 화면에 QR 코드 나타남, 각 폰에서 Expo Go 앱 설치 후 스캔하면 앱이 실행 됨
- npm run android
- npm run ios
- npm run web
- Native-CLI
- npx react-native start
- npx react-native run-android
- npx react-native run-ios
Core Components
핵심적으로 익혀야 하는 자주 사용하게 되는 컴포넌트들. 사용하기 쉽고, 많지 않아 쉽게 익힐 수 있음.
- View
- Text
- Image
- ScrollView
- TextInput
- Button
- Switch
웹 개발하는 화면과 상당히 비슷하면서도 약간 다름
비슷한 부분이 상당히 많아서 웹 개발 경험이 있으면 받아 들이기 매우 편함
버튼, 스위치를 보면 리액트 네이티브와 플러터와 차이점이 확연히 들어나는데
- 리액트 네이티브 : aos, ios의 컴포넌트를 사용함 - 해당 OS의 기본 UI를 사용하기 때문에 해당 OS의 UI와 동일한 느낌을 줄 수 있음. 대신 양쪽 모두 동일한 느낌의 UI를 만들려면 고달픔
- 플러터 : 모든걸 플러터가 직접 그려줌, 양쪽이 동일한 UI를 만들 때는 편하지만, 해당 OS의 공유한 UI를 살릴 수 없음
뭐가 좋다고 말하기 애매한 장단점인데, 난 개인적으로는 플러터 방식을 선호.
App.js
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View, Image, TextInput, ScrollView, Button, Switch } from 'react-native';
export default function App() {
return (
<View style={styles.container}>
<Text style={styles.text}>Open up App.js to start working on your app!!</Text>
<StatusBar style="auto" />
<Image source={require("./assets/cat.jpg")} style={styles.local_image} />
<Image source={{uri: "https://cdn.pixabay.com/photo/2017/02/20/18/03/cat-2083492__340.jpg"}}
style={styles.url_image} />
<TextInput placeholder="이름을 입력해주세요" />
<Button title="click" onPress={()=>{console.log("clicked");}}/>
<Switch value={true} />
<ScrollView>
<Image source={{uri: "https://i.ytimg.com/vi/ByH9LuSILxU/maxresdefault.jpg"}}
style={styles.url_image} />
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
text: {
fontSize: 20,
fontWeight: "bold",
},
local_image: {
width: 100,
height: 100,
},
url_image: {
width: 200,
height: 200,
},
});
컴포넌트 만들기, 그리고 prop
컴포넌트 : 재사용 가능하도록 분리된 코드 블럭 - 여러 컴포넌트를 조합하여 화면 구성
컴포넌트에 포함된 데이터가 변경되면 화면 갱신은 프레임워크(리액트 네이티브)가 알아서 새로 고침을 해줌
화면설계서를 보고 어떻게 컴포넌트로 나눌까 고민해서 컴포넌트로 분리
컴포넌트에 정보 전달을 어떻게 하나? - prop
상위 컴포넌트가 하위 컴포넌트에게 데이터를 전달하는 방법
컴포넌트들이 많아지고 복잡해지면 계층구조가 복잡해 질 수 밖에 없음
이러면 여러 컴포넌트들이 정보를 받아서 하위로 전달하는 것이 매우 귀찮아짐
이런 문제를 해결하기 위해서 전역 상태 관리가 필요해지는데, 리액트 웹 개발할 때와 동일하게 redux 를 많이 사용함
App.js
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
const Header = (props) => {
console.log("Header : ", props);
return (
<Text>{props.title}</Text>
);
}
export default function App() {
return (
<View style={styles.container}>
<Header title="헤더정보" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
React Hooks - useState, useEffect
Hooks - 변수를 업데이트 하기 위한 체계, 콜백 함수로 구현
리액트 초기 버전은 클래스형으로 개발 되었으나, 상속구조가 복잡해지면서 성능이 저하되는 문제 발생
성능 개선을 위해 함수형으로 변경됨
함수형이다 보니 모든게 함수로 처리, 클래스형에서 클래스의 멤버로 이벤트 핸들러가 담당하던 역할을 함수로 대체
이게 바로 리액트 훅.
관리되어야 하는 정보가 있다면 - useState 사용
변수 이름과 함수 이름을 한 쌍으로 관리
특정시점에 뭘 해야 한다면 - useEffect 사용
함수형으로 사용하면 코드도 훨씬 더 심플해지는 부가적인 장점도 있습니다
useState 사용해 봅시다
aos와 ios 화면 보이는 모습이 완전 달라서 참 거슬립니다.
이걸 동일하게 맞추는건 상당히 스트레스가 될 것도 같지만, 뭐 그래도 그동안 리액트 네이티브로 만들어진 앱이 몇개고 동일한 고민을 하면서 노력한 사람들이 얼마일건데 다 방법이 있겠죠.
계속 공부해 나가다 보면 알게 될 겁니다.
이번엔 컴포넌트를 별도 파일로 분리하고, App.js에서 임포트 하고 화면에 컴포넌트를 출력합니다.
App.js
import { StyleSheet, View } from "react-native";
import StateWithFuctionalComponent from "./StateWithFuctionalComponent";
export default function App() {
return (
<View style={styles.container}>
{/* <StateWithClassComponent /> */}
<StateWithFuctionalComponent />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
컴포넌트에서 useState를 사용합니다.
사용을 위한 준비는 매우 간단. 변수명/함수이름 정의하고 초기값을 설정해 주면 됩니다.
어디에 선언해야 하는지 눈여겨 봐두고
사용은 값을 읽고 싶으면 변수명을, 값을 변경하고 싶으면 함수명을 사용하면 됩니다.
간단하죠.
StateWithFuctionalComponent.js
import React, { useState } from "react";
import { View, Text, Button, Switch, TextInput } from "react-native";
const Component = () => {
const [count, setCount] = useState(0); // number
const [isOn, setIsOn] = useState(false); // boolean
const [name, setName] = useState(""); // string
return (
<View>
<Text>You clicked {count} times</Text>
<Button title="Click me" onPress={() => setCount(count + 1)} />
<Text>-------</Text>
<Switch
value={isOn}
onValueChange={(v) => {
console.log("v", v);
setIsOn(v);
}}
/>
<Text>-------</Text>
<TextInput
value={name}
onChangeText={(v) => {
console.log("v", v);
setName(v);
}}
/>
</View>
);
};
export default Component;
useEffect 사용해 봅시다
앱이 실행 될 때 한번만 실행되게 하고 싶으면 배열을 빈 상태로 useEffect 사용
특정 변수(스테이트)가 변경되었을 때 실행되게 하고 싶으면 배열에 변수명 기입
App.js
import { StyleSheet, View, Button } from "react-native";
import UseEffectWithFunctionalComponent from "./UseEffectWithFunctionalComponent";
export default function App() {
return (
<View style={styles.container}>
<UseEffectWithFunctionalComponent />
<Button title="toggle" onPress={() => setIsTrue(!isTrue)} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
UseEffectWithFunctionalComponent.js
import React, { useEffect, useState } from "react";
import {
View,
Text,
Button,
TextInput,
Switch,
ActivityIndicator,
} from "react-native";
const Component = () => {
const [count, setCount] = useState(0);
const [isOn, setIsOn] = useState(true);
const [input, setInput] = useState("");
const [isRefresh, setIsRefresh] = useState(false);
useEffect(() => {
console.log("didMount");
}, []);
useEffect(() => {
console.log("didUpdate - count", count);
}, [count]);
useEffect(() => {
console.log("didUpdate - isOn", isOn);
}, [isOn]);
useEffect(() => {
console.log("didUpdate - input", input);
}, [input]);
useEffect(() => {
if (isRefresh) {
setTimeout(() => {
setIsRefresh(false);
}, 2000);
}
}, [isRefresh]);
return (
<View style={{ alignItems: "center" }}>
<Text>You clicked {count} times</Text>
<Button title="Click me" onPress={() => setCount(count + 1)} />
<Text style={{ marginVertical: 15 }}>
-------------------------------------------------
</Text>
<Switch value={isOn} onValueChange={setIsOn} />
<Text style={{ marginVertical: 15 }}>
-------------------------------------------------
</Text>
<Text>input: {input}</Text>
<TextInput
value={input}
onChangeText={setInput}
style={{ borderBottomWidth: 1, borderColor: "grey" }}
/>
<Text style={{ marginVertical: 15 }}>
-------------------------------------------------
</Text>
<Button
title="새로고침!"
onPress={() => {
setIsRefresh(true);
}}
/>
{isRefresh && <ActivityIndicator />}
</View>
);
};
export default Component;
제공되는 것 말고 내가 맘대로 하고 싶어 - custom hook
useState 는 1개의 변수(스테이트)와 1개의 함수만 설정 가능
함수를 여러개로 확장하고, 내가 지은 이름으로 만들고 싶을 때 커스텀 훅 사용
이름은 항상 use 로 시작해야 함
App.js
import { StyleSheet, View } from "react-native";
import CustomHook from "./CustomHook";
export default function App() {
return (
<View style={styles.container}>
<CustomHook />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
CustomHook.js
import React, { useState } from "react";
import { Button, TextInput, View } from "react-native";
const InputBox = (props) => {
return (
<View style={{ flexDirection: "row" }}>
<TextInput
value={props.value}
onChangeText={props.onChangeText}
style={{ borderBottomWidth: 1, width: 200 }}
placeholder={props.placeholder}
/>
<Button title="초기화" onPress={props.onReset} />
</View>
);
};
const useInput = (initialValue) => {
const [value, setValue] = useState(initialValue);
const resetValue = () => setValue(initialValue);
return {
value,
setValue,
resetValue,
};
};
const CustomHook = () => {
const {
value: name,
setValue: setName,
resetValue: resetName,
} = useInput("");
const { value: age, setValue: setAge, resetValue: resetAge } = useInput("");
const {
value: city,
setValue: setCity,
resetValue: resetCity,
} = useInput("");
return (
<View>
<InputBox
value={name}
onChangeText={setName}
placeholder="이름을 입력해 주세요"
onReset={resetName}
/>
<InputBox
value={age}
onChangeText={setAge}
placeholder="나이를 입력해 주세요"
onReset={resetAge}
/>
<InputBox
value={city}
onChangeText={setCity}
placeholder="사는 곳을 입력해 주세요"
onReset={resetCity}
/>
</View>
);
};
export default CustomHook;