내일배움단 App3.2
[앱개발 종합반] 3주 2일차 개발일지(3주 2강)
📱TIL App.302
앱 필수, 리액트(React) 기초지식
Props가 Component에 데이터를 전달해주면, Component 안의 State가 데이터를 관리한다.
0. 배울 내용 미리보기
① 컴포넌트(Component)
정해진 elements(요소들)을 사용해 만든 화면의 일부분
ㄴ 내가 만든 MainPage, AboutPage, DetailPage 파일(페이지) 자체
ㄴ 페이지를 구분해 따로 코드를 작성해 관리하는 것
② 상태(State)
Component(하나의 파일) 안에서만 관리할 데이터(데이터를 유지하고 관리하기 위한 유일한 방법)
③ 속성(Props)
상위 Component에서 하위 Component로 데이터를 전달하는 방식
ㄴ <ScrollView>
태그에 style
속성을 줄 때(옷을 입힐 때), 실제로는 해당 key 값의 딕셔너리(Object)를 태그에 전달해 주는 것!
④ useEffect
화면에 Component가 그려지면 처음 실행해야 하는 함수들을 모아두는 곳.
ㄴ 앱이 켜졌을 때 가장 먼저 해야 하는 것들(로그인 유무 체크, 외부 데이터 준비 등)
1. 컴포넌트(Component)
화면의 모든 부분을 뜻하며, 화면 내의 버튼, 이미지, 텍스트, 영역을 비롯해 전체 화면까지 모두 Component라고 할 수 있다. 컴포넌트는 UI의 요소를 재사용 가능한 부분으로 조각내서 운영하는 기법이다. 또한 App.js
의 App
함수와 같이 코드 전체를 감싸고 있는 함수를 뜻하기도 한다. App.js
를 App
Component라고 부를 수 있다.
앞에서 pages
폴더에 여러 페이지를 따로 관리하면서부터 각 페이지와 그 컨텐츠들은 Component 속성을 갖게 된다. Component는 쉽게 말해 ‘코드를 분할해서 따로 관리한다‘는 뜻! 화면을 구성하는 각 개체를 하나하나 쪼개서 관리하면 유지보수가 편리하다는 장점이 있다.
- 메인화면 Component화 해보기
메인 폴더에 components
폴더를 만들고, 폴더 안에 Card.js
파일을 만든다. Card.js
파일에는 아래와 같은 코드가 들어간다.
// Card.js
import React from "react"
import {View,Text,Image,StyleSheet} from "react-native";
// 비구조 할당 방식으로 넘긴 속성 데이터를 꺼내 사용함
export default function Card({content}) {
return (
<View style={styles.card}>
<Image style={styles.cardImage} source={{uri:content.image}} />
<View style={styles.cardText}>
<Text style={styles.cardTitle} numberOfLines={1}>{content.title}</Text>
<Text style={styles.cardDesc} numberOfLines={3}>{content.desc}</Text>
<Text style={styles.cardDate}>{content.date}</Text>
</View>
</View>
)
}
const styles = StyleSheet.create({
card:{
flex:1,
flexDirection:"row",
margin:10,
borderBottomWidth:0.5,
borderBottomColor:"#eee",
paddingBottom:10
},
cardImage: {
flex:1,
width:100,
height:100,
borderRadius:10
},
cardText: {
flex:2,
flexDirection:"column",
marginLeft:10
},
cardTitle: {
fontSize:20,
fontWeight:"700"
},
cardDesc: {
fontSize:15
},
cardDate: {
fontSize:10,
color:"#A6A6A6"
}
})
코드를 살펴보면, Card.js
파일은 MainPage 화면의 카드 부분을 구현하는 태그와 스타일 코드를 담고 있다. 카드 부분을 구현하는 코드를 Card.js
파일로 분리했기 때문에, 다른 페이지(Component)에서도 해당 파일을 불러와 사용할 수 있게 된다. 해당 코드를 담고 있는 MainPage.js
파일의 코드는 아래와 같이 수정된다.
// MainPage.js
// 수정 전
{
tip.map((content,i)=>{
return(
<View style={styles.card}>
<Image style={styles.cardImage} source={{uri:content.image}} />
<View style={styles.cardText}>
<Text style={styles.cardTitle} numberOfLines={1}>{content.title}</Text>
<Text style={styles.cardDesc} numberOfLines={3}>{content.desc}</Text>
<Text style={styles.cardDate}>{content.date}</Text>
</View>
</View>
)
})
}
// 수정 후
import Card from '../components/Card';
{
tip.map((content,i)=>{
return (<Card content={content} key={i}/>)
})
}
카드 부분의 코드를 새로운 파일로 만들었기 때문에, MainPage.js
에서는 Card.js
파일을 import
해오기만 하면 파일의 코드를 태그 형식으로 쓸 수 있다. 이렇듯 어떠한 화면에서 내가 만든 태그를 직접 쓰는 일을 컴포넌트화라고 한다.
// App.js
import React from 'react';
import MainPage from './pages/MainPage';
import AboutPage from './pages/AboutPage';
import DetailPage from './pages/DetailPage';
export default function App(
return (<MainPage/>)
)
2주차 숙제 때 여러 페이지들을 pages
폴더에 생성하고, App.js
파일에서 각 페이지 파일을 import
해온 후 태그 형식으로 사용한 것도 컴포넌트화였다.
Card.js
파일을 보면, Card
함수는 parameter로 중괄호로 감싸진 {content}
데이터를 받아 함수 안에서 사용하고 있다. 이전 MainPage.js
파일에서 보이던 map
함수도 보이지 않는다.
// Card.js
export default function Card({content}) {
return (<View style={styles.card}>
<Image style={styles.cardImage} source={{uri:content.image}} />
<View style={styles.cardText}>
<Text style={styles.cardTitle} numberOfLines={1}>{content.title}</Text>
<Text style={styles.cardDesc} numberOfLines={3}>{content.desc}</Text>
<Text style={styles.cardDate}>{content.date}</Text>
</View>
</View>)
}
// MainPage.js
import data from '../data.json';
let tip = data.tip;
{
tip.map((content,i)=>{
return (<Card content={content} key={i}/>)
})
}
이는 Props(속성)가 데이터를 전달하는 모습이다. 위의 MainPage.js
파일을 보면, Card
태그의 content
속성값이 {content}
로 지정되어 있다. map
함수를 돌릴 때 tip
이라는 딕셔너리(Array)의 요소 하나하나를 content
라는 변수로 설정했기 때문에, 해당 변수에 속성값을 줘서 해당 데이터를 Card
컴포넌트 함수에 전달한다. 전달할 데이터(data
)는 MainPage.js
파일에 있으며, map
함수는 데이터 Array(tip
)의 element(content
)와 index(i
)를 가져와서 <Card>
태그에 content
, key
속성을 통해 값을 넘기는 것이다. 반복문을 돌릴 때 각 부모 태그는 고유한 key 값을 가져야 하므로, map
함수 parameter에서 받은 i
(index)를 똑같이 컴포넌트에 넣는다. 이처럼 MainPage.js
에서 Card.js
로 데이터를 넘기는 것을 속성을 넘긴다라고 한다.
2. 속성(Props)
속성은 key-value 형태로 Component에 데이터를 전달하는 것이다. 내가 직접 데이터를 전달해줄 수도 있고, 공식 문서의 Props
란에서 특정 태그에 전달할 수 있는 키나 데이터를 참고해 정해진 속성을 전달해줄 수도 있다. 예를 들어, <Text>
태그의 numberOfLines
속성에 {3}
을 전달했을 때, 3줄 이상에서는 말줄임표가 적용되었다.
내가 만든 <Card>
Component에도 속성이 존재하며, 내가 부여하는 대로 속성에서 key-value 값을 갖게 된다. 예를 들어, <Card>
태그 안에 image={"이미지 주소"}
의 속성을 전달하면, 해당 태그는 image
: {"이미지 주소"}
라는 key-value 값을 갖는다. 속성을 부여받은 Component에서는 해당 속성 값을 받아서 사용할 수 있다. 앞에서 <Card>
태그에 content
속성을 부여하고 {content}
값을 준 것도 이러한 방식으로 데이터를 전달한 것이다. content
라는 이름은 tip
의 각 element, <Card>
태그의 속성, <Card>
태그의 속성에 전달되는 값 모두를 가리키며 서로 동일하다. Card.js
파일의 Card
함수에서는 이렇게 전달된 {content}
값을 parameter로 받아서 {content.image}
, {content.title}
처럼 필요한 값을 꺼내 사용한다.
Component에 속성(데이터)을 부여해 전달할 때는, key-value 형태(
content={content}
)로 전달하기!
Component에서 반복문을 돌릴 때, 각 Component는 고유한 key값을 가져야 하므로map
에서 index값을 가져와key = {i}
형태로 속성 전달하기!
// Card.js
export default function Card({content}) {
return (
...
)
}
데이터를 넘겨받아 사용할 때, 속성 데이터는 위 코드와 같이 비구조 할당 방식으로 넘겨진다. 비구조 할당 방식(destructuring)은 1주차 때 다뤘던 개념으로, 따로 변수를 생성하고 할당할 필요 없이 딕셔너리(Object) 구조 자체를 변수 그대로 꺼내서 쓰는 방식이다. Object에서 key값을 바로 취해서, 변수로써 함수 안에서 바로 사용할 수 있는 방식이다.
- 비구조 할당 방식은 실제로 어떻게 작용할까?
// Card.js
// 비구조 할당 방식
export default function Card(props) {
{
props: {
content: {
// JSON 데이터
/* {
"tip":[
{
"idx":0,
"category":"생활",
"title":"먹다 남은 피자를 촉촉하게!",
"image":"https://storage.googleapis.com/sparta-image.appspot.com/lecture/pizza.png",
"desc":"먹다 남은 피자는 ...",
"date":"2020.09.09"
},...
} */
key: {
}
}
}
}
Card
함수의 parameter에는 props
라는 키가 항상 넘어와 있으며, 이 props
에는 MainPage.js
에서 <Card>
태그에 준 content
와 key
속성의 key-value 값들이 딕셔너리(Object) 형태로 들어 있다.
// Card.js
// 비구조 할당 방식 X
export default function Card(props) {
let content = props.content;
{
props: {
content: {
// JSON 데이터
},
key: {
}
}
}
}
비구조 할당 방식을 사용하지 않는다면, 위 코드와 같이 변수를 새로 설정해서 딕셔너리(Object) 값에 접근해야 한다. 비구조 할당 방식을 사용하면, parameter로는 딕셔너리(Object) 자체가 넘어오기 때문에 필요한 값, 즉 JSON 데이터에 해당하는 content
키의 값만 사용할 수 있게 된다. content
키 안의 여러 key-value 값들은 연결 연산자를 사용해 쉽게 접근해 사용할 수 있다.
3. 상태(State)*
상태(State)란, 컴포넌트 안에만 사용되는 데이터를 관리하는 방법이다. 앞의 과정에서 여러 페이지들을 만드는 페이지화, 그리고 한 페이지 안에서의 구역을 나누는 컴포넌트화를 진행했었다. 이러한 component 안에서만 사용되는 데이터는 변수를 통해 쉽게 활용할 수 있었다. 예를 들어, DetailPage.js
파일에서는 const tip={...}
형태로 데이터를 변수 형태로 정의해 사용했다.
이렇듯 데이터를 변수로 바로 사용할 수도 있지만, React에서는 상태(state)가 바뀌어야 화면이 바뀐다. 변수 형태로 데이터를 사용할 때는, 함수 안에서 사용하는 변수의 데이터가 바뀐다 하더라도 화면은 바뀌지 않는다. 데이터에 따라서 화면이 바뀌려면 데이터가 ‘상태’로 관리되어야 한다. 주식 차트나 그래프처럼 변화되는 데이터에 따라 화면이 실시간으로 바뀌는 앱들은 주로 React로 구현된다. 서버가 실시간으로 앱에 새로운 데이터를 보내면, 앱은 새로운 데이터를 받고 상태 관리를 다시 해서 화면을 다시 그려주는 것이다. React에서 상태(state)는 React 라이브러리에서 제공해주는 useState
로 생성하고, setState
함수로 정하거나 변경할 수 있다.
UI = component(state)
‘사용자 화면(User Interface)은 Component(페이지, 페이지의 부분들)에 어떤 데이터(state)값이 전달되고 관리되는지에 따라서 달라진다’를 나타낸 수식. 복잡해 보이던 개념을 이렇게 간단명료하게 함수처럼 표현하다니 신기했다. 상태(state)로 관리하던 데이터가 변경되면 화면이 바뀐다!
4. useEffect
useEffect(()=>{
// 화면이 그려진 다음 가장 먼저 실행되어야 할 코드
}, [])
useEffect
는 React 기본 제공 함수로, 위와 같이 형식이 정해져 있다. 함수 안에는 화면이 그려진 다음 가장 먼저 실행될 코드를 작성하는데, 주로 console.log
로 값을 먼저 찍어보거나, 외부에서 데이터를 불러와서 준비하는 일을 수행한다. 예를 들어, 카카오톡은 앱을 켜자마자 로그인 유무를 체크해서 각기 다른 화면을 보여준다.
데이터를 준비한다는 것은, 데이터를 서버로부터 받아 상태(state)에 반영한다는 것을 뜻한다. 화면이 그려진 다음, useEffect
가 데이터를 준비하고(서버에게 필요한 데이터를 요청해 받고), 업데이트된 상태 데이터에 따라 화면이 다시 그려지는 순서로 실행된다.
// MainPage.js
import data from '../data.json';
export default function MainPage() {
let tip = data.tip;
...
}
데이터를 상태(state)로 관리하기 전에는, 위의 코드처럼 data.json
파일을 import
해온 다음 tip
이라는 변수를 설정해 데이터를 사용했었다. 이 방식은 데이터를 변수에 받은 다음에 화면에 뿌려주는 것이기 때문에, 데이터가 관리되고 있다기보다는 그냥 변수에 담아서 쓰는 것에 가깝다. 즉 data.json
파일에서 변경 사항이 있더라도 보이는 앱 화면에는 반영되지 않는다.
// MainPage.js
import React,{useState,useEffect} from 'react';
import data from '../data.json';
import Card from '../components/Card';
export default function MainPage() {
// useState 사용법
// [state,setState] 에서 state는 이 컴포넌트에서 관리될 상태 데이터를 담고 있는 변수
// setState는 state를 변경시킬때 사용해야하는 함수
// 모두 다 useState가 선물해줌
// useState()안에 전달되는 값은 state 초기값
const [state,setState] = useState([])
// 하단의 return문이 실행되어 화면이 그려진 다음 실행되는 useEffect 함수
// 내부에서 data.json으로 부터 가져온 데이터를 state 상태에 담고 있음
useEffect(()=>{
setState(data)
},[])
// let tip = data.tip;
// data.json 데이터는 state에 담기므로 상태에서 꺼내옴
let tip = state.tip;
return (
...
{
tip.map((content,i)=>{
return (<Card content={content} key={i}/>)
})
}
...
)
데이터를 상태(state)로 관리하게 될 때의 코드를 살펴보자. data.json
파일을 import
해오는 것까지는 동일하다. 실제 Component에서의 상태 관리는 React가 제공하는 도구를 이용하기 때문에, React 에서 useState
와 useEffect
를 꺼내와야 한다. 하단의 return
문이 실행되어 앱 화면이 그려진 다음, useEffect
함수가 실행된다. useEffect
안에 있는 setState()
를 통해 data.json
파일에서 가져온 데이터(data
)가 상태 관리되기 시작한다.
const [state, setState] = useState([])
useState
는 함수로, parameter로 전달된 []
는 상태(state) 변수의 초기값이다. useState
는 [state, setState]
를 반환하며, state
는 이 컴포넌트에서 관리될 상태 데이터를 담고 있는 변수, setState
는 데이터를 변화시킬 때, 즉 state를 변경할 때 사용할 함수이다.
- 코드의 로직
하단의 return
문이 실행되어 앱 화면이 그려진 다음, useEffect
함수가 실행된다. useEffect
는 useState()
함수를 이용해 데이터들을 상태 변수인 state
에 업데이트한다. 상태(state)가 변경되었으니 화면이 다시 그려진다.
⚠️ 코드 스니펫을 실행시키면 ‘undefined is not an object (evaluating ‘tip.map’)’이라는 에러가 뜬다. 위의 코드를 살펴보자. 위에서 useState
의 parameter인 []
는 상태 변수인 state
의 초기값이라고 했다. 즉, 함수가 실행될 때 state = []
인 상태인 것이다. 빈 리스트(Array)에서 tip
이라는 데이터를 꺼내려고 하니, 정의되지 않은(undefined) 객체라는 에러가 뜬 것이다.
달리 말해, 앱 화면이 그려진 직후 상태 변수인 state
에 데이터가 아직 담기지 않은 상태이기 때문에 데이터를 꺼내올 수 없다는 오류다. 이 문제를 해결하려면 useEffect
함수를 거쳐 state
에 데이터를 담아야 한다. 로딩 화면을 만들어서, 데이터가 준비될 때까지는 로딩 화면을 보여주고, 데이터가 준비되어 상태 관리에 들어가면 반복문(map
)을 돌려서 카드 리스트를 보여주는 방안으로 문제를 해결할 수 있다.
Leave a comment