const start_half = <div>
<h1>안녕하세요!</h1>
<p>시작이 반이다!</p>
</div>;
그럼 input처럼 닫힘 태그가 필요없는 것들은?
이것도 마찬가지로 이렇게!
<input type='text/>
return (
<p>안녕하세요! 리액트 반입니다 :)</p>
<div className="App">
<input type='text'/>
</div>
);
const cat_name= 'fall';
return (
<div>
hello {cat_name}!
</div>
);
값을 가져올 때 뿐만 아니라, map, 삼항연산자 등 자바스크립트 문법을 JSX 안에 쓸 때도 {}를 이용할 수 있다.
<div className="App"> <p>안녕하세요! 리액트 반입니다 :)</p> {/* JSX 내에서 코드 주석은 이렇게 씁니다 :) */} {/* 삼항 연산자를 사용했어요 */} <p>{number > 10 ? number+'은 10보다 크다': number+'은 10보다 작다'}</p> </div>
<div className="App">
클래스 외 다른 명 id같은 아이들은 그대로 사용!
<p style={{color: 'orange', fontSize: '20px'}}>orange</p>
중괄호가 두 개인 이유는?
- 딕셔너리도 자바스크립트이기 때문!
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<header>
...
</header>
<div class="container">
<div id="image-banner">
...
</div>
<div id="contents-1">
...
</div>
</div>
<footer>
...
</footer>
</body>
</html>
이런 html 파일이 있을 때 이 코드들을 조각내보면
이렇게 나눌 수 있다.
즉, 이 웹 사이트는,
크게 header, container, footer 세 개의 컴포넌트가 있고,
container 컴포넌트는,
imagebanner, contents1 컴포넌트로 이루어져 있는 것!
import React from 'react';
import logo from './logo.svg';
import './App.css';
// BucketList 컴포넌트를 import 해옵니다.
// import [컴포넌트 명] from [컴포넌트가 있는 파일경로];
import BucketList from './BucketList';
// 클래스형 컴포넌트는 이렇게 생겼습니다!
class App extends React.Component {
constructor(props){
super(props);
// App 컴포넌트의 state를 정의해줍니다.
this.state = {
list: ['영화관 가기', '매일 책읽기', '수영 배우기'],
};
}
// 랜더 함수 안에 리액트 엘리먼트를 넣어줍니다!
render() {
return (
<div className="App">
<h1>내 버킷리스트</h1>
{/* 컴포넌트를 넣어줍니다. */}
<BucketList/>
</div>
);
}
}
export default App;
// 리액트 패키지를 불러옵니다.
import React from 'react';
// 함수형 컴포넌트는 이렇게 쓸 수도 있고
// function Bucketlist(props){
// return (
// <div>버킷 리스트</div>
// );
// }
// 이렇게 쓸 수도 있어요. =>가 들어간 함수를 화살표 함수라고 불러요.
// 저희는 앞으로 화살표 함수를 사용할거예요.
// 앗 () 안에 props! 부모 컴포넌트에게 받아온 데이터입니다.
// js 함수가 값을 받아오는 것과 똑같이 받아오네요.
const BucketList = (props) => {
// 컴포넌트가 뿌려줄 ui 요소(리엑트 엘리먼트라고 불러요.)를 반환해줍니다.
return (
<div>
버킷 리스트
</div>
);
}
// 우리가 만든 함수형 컴포넌트를 export 해줍니다.
// export 해주면 다른 컴포넌트에서 BucketList 컴포넌트를 불러다 쓸 수 있어요.
export default BucketList;
이런 헤더 컴포넌트가 있을 때 이 안에 들어가있는 (로고 이미지 경로, 메뉴 이름 - 온라인, 오프라인, 기업교육, 내 강의실)
<container> <imagebanner/> <contents1/> </container>
- 컴포넌트만 컴포넌트한테 필요한 이미지 경로를 가지고 있다고 가정(state로 가지고 있음)
가 가지고 있는 이미지 경로를 에게 전달해주면, 이 이미지 경로가 컴포넌트의 props
{/* <컴포넌트 명 [props 명]={넘겨줄 것(리스트, 문자열, 숫자, ...)}/> */}
<BucketList list={this.state.list}/>
// 앗 () 안에 props! 부모 컴포넌트에게 받아온 데이터입니다.
// js 함수가 값을 받아오는 것과 똑같이 받아오네요.
const BucketList = ({list}) => {
// Quiz 1: my_list에 ['a', 'b', 'c'] 대신 부모 컴포넌트가 넘겨준 값을 넣으려면 어떻게 해야할까요?
const my_lists = ['a', 'b', 'c'];
// 컴포넌트가 뿌려줄 ui 요소(리엑트 엘리먼트라고 불러요.)를 반환해줍니다.
return (
<div>
{
// js의 내장 함수 중 하나인 map입니다. 리스트의 갯수만큼 => 오른쪽 구문을 반복해요.
// 자세한 사용법은 아래 링크를 확인해주세요.
// https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/map
my_lists.map((list, index) => {
// 콘솔을 확인해봅시다 :)
console.log(list);
return (<div key={index}>{list}</div>);
})
}
</div>
);
}
⭐️ 리액트 코딩 룰 1:
폴더는 소문자로 시작하는 카멜케이스를 사용
JS파일, 컴포넌트 이름은 대문자로 시작하는 카멜케이스를 사용
☝🏻 DOM?
- html 단위 하나하나를 객체로 생각하는 모델
- 트리 구조
☝🏻
- 생성 : 처음 컴포넌트를 불러오는 단계
- 수정(업데이트) : 사용자의 행동(클릭, 입력)으로 데이터가 바뀌거나 부모 컴포넌트가 렌더링할 때
-> props, state가 바뀔 때
-> 부모 컴포넌트가 업데이트 되었을 때(=리렌더링)
-> 강제로 업데이트 했을 경우(=forceUpdate())- 제거 : 페이지를 이동하거나 사용자의 행동(삭제 버튼)으로 컴포넌트가 화면에서 사라지는 단계
☝🏻 라이프 사이클 함수는 클래스형 컴포넌트에서만 사용할 수 있다.
리액트 공식 매뉴얼에서 함수형 컴포넌트를 더 권장하기 때문에 라이프 사이클을 아는 건 중요하지만 함수형 컴포넌트를 더 많이 씀!
(리액트 16.8버전부터 등장한 React Hooks로 라이프 사이클 함수 대체 가능
// 클래스형 컴포넌트
class LifecycleEx extends React.Component {
// 생성자 함수
constructor(props) {
super(props);
this.state = {
cat_name: '가을',
};
}
changeCatName = () => {
// componentDidUpdate()를 보기 위해 쓰는 것
this.setState({cat_name: '마요네즈'});
}
componentDidMount(){
}
componentDidUpdate(prevProps, prevState){
}
componentWillUnmount(){
}
render() {
return (
<div>
<h1>제 고양이 이름은 {this.state.cat_name}입니다.</h1>
<button onClick={this.changeCatName}>고양이 이름 바꾸기</button>
</div>
);
}
}
☝🏻 마운트?
: 컴포넌트가 화면에 나타나는 것
☝🏻 prevProps, prevState는 각각 업데이트 되기 전 props, state!
-> 이전 데이터와 비교할 일이 있을 때 사용
const input_text = React.useRef(null);
...
//현재 입력된 값(뷰에서)
input_text.current.value
1) 컴포넌트 만들기
2) App에서 import
3) useState()로 state 등록
4) 뷰 만들기
5) 함수 만들기
6) 연결
7) 완성!
// count에는 state 값이, setCount는 count라는 state 값을 수정하는 함수가 될거예요.
// useState(초기값): () 안에 초기값을 넣어줍니다.
const [count, setCount] = React.useState(3);
const addNemo = () => {
// setCount를 통해 count에 저장된 값을 + 1 해줍니다.
setCount(count + 1);
};
const removeNemo = () => {
// setCount를 통해 count에 저장된 값을 - 1 해줍니다.
// 삼항 연산자
setCount(count > 0 ? count - 1 : 0);
};
<div>
{/* 함수를 호출합니다. */}
<button onClick={addNemo}>하나 추가</button>
<button onClick={removeNemo}>하나 빼기</button>
</div>
: html이 1개 뿐인 어플리케이션 > 딱 한번만 정적 자원을 받아온다.
-> 페이지를 이동할 때마다 서버에서 주는 html로 화면을 바꾸다보면 상태 유지가 어렵고, 바뀌지 않은 부분까지 새로 불러와 사용성에서 매우 비효율적이다.
-> SPA는 처음에 모든 컴포넌트를 받아와 사용자가 안들어가 볼 페이지까지 전부 가지고 오기 때문에 아주아주 많은 컴포넌트가 있다면 첫 로딩 속도가 조금 느리다 (스피너주기!)
: 라우팅을 통해 브라우저 주소창대로 다른 페이지를 보여줌
-> 라이브러리 사용
react-router-dom 공식문서
import { BrowserRouter } from "react-router-dom";
...
// BrowserRouter은 웹 브라우저가 가지고 있는 주소 관련 정보를 props로 넘겨주기 때문에 현재 내가 어느 주소를 보고 있는지 쉽게 알려준다.
<BrowserRouter>
<App />
</BrowserRouter>,
<Route path="주소[/home 처럼 /와 주소를 적어요]" component={[보여줄 컴포넌트]}/>
<Route path="주소[/home 처럼 /와 주소를 적어요]" render={(props) => (<BucketList list={this.state.list} />)} />
☝🏻 exact : '/'기호가 '/주소1' '/주소2'에 포함되어있어 다른 주소를 넘어가도 기본 '/'페이지가 뜬다. 이 때 exact를 메인 홈페이지에 적어주면 해결!
파라미터 주는 방법
<Route path="/cat/:cat_name" component={Cat}/>
: a태그와 비슷한 역할로 페이지 전환을 도와준다.
<Link to="주소">[텍스트]</Link>
//해당 주소로 이동
props.history.push('/주소');
//뒤로가기
props.history.goBack();
1) NotFound.js파일 만들고 빈 컴포넌트
2) App.js에서 불러오기
3) 라우팅 임포트에 Switch추가
4) Route에 주소없이 연결!
...
<Switch>
<Route
path="/"
exact
render={(props) => (
<BucketList
list={this.state.list}
history={this.props.history}
/>
)}
/>
<Route path="/detail" component={Detail} />
<Route component={NotFound} />
</Switch>
...
: 상태관리 라이브러리
리덕스 공식문서
: 저장하고 있는 상태값(=데이터)로 딕셔너리 형태({[key]:value})로 보관
: 상태에 변화가 필요할 때(=데이터를 변경) 발생
// 액션은 객체, type은 이름- 임의의 문자열
{type: 'CHANGE_STATE', data: {...}}
: 액션 생성 함수
//이름 그대로 함수
const changeState = (new_data) => {
// 액션을 리턴
return {
type: 'CHANGE_STATE',
data: new_data
}
}
: 리덕스에 저장된 상태(=데이터)를 변경하는 함수
☝🏻 액션 생성 함수를 부른다 > 액션을 만든다 > 리듀서가 현재 상태(데이터)와 액션 객체를 받는다 > 새로운 데이터를 만든다 > 리턴한다
// 임의로 정한 기본 상태값(나중에 firebase에서 데이터를 불러오기 전까지 요게 표출됨)
const initialState = {
name: '가을'
}
function reducer(state = initialState, action) {
switch(action.type){
// action의 타입마다 케이스문을 걸어주면,
// 액션에 따라서 새로운 값을 돌려줌!
case CHANGE_STATE:
return {name: '가을이'};
default:
return false;
}
}
: 액션을 발생시키는 스토어의 내장 함수
// 간단히 표현하자면 이런 식으로 우리가 발생시키고자 하는 액션을 파라미터로 넘겨서 사용합니다.
dispatch(action);
☝🏻 데이터가 마구잡이로 변하지 않도록 불변성을 유지하기 위함
리덕스에 저장된 데이터 = 상태 = state >> 읽기 전용
이건 js의 문법 특징이랑도 비슷한 개념인듯?
☝🏻 리듀서는 순수한 함수여야 한다.
? 순수한 함수?
- 파라미터 외의 값에 의존하지 않아야 한다.
- 이전 상태는 수정하지 않고 변화를 준 새로운 객체를 return한다.
- 파라미터가 같으면 항상 같은 값을 반환한다.
- 리듀서는 이전 상태와 액션을 파라미터로 받는다.
1) 리덕스 Store를 컴포넌트에 연결한다.
2) 컴포넌트에서 상태 변화가 필요할 때 Action을 부른다.
3) Reducer를 통해서 새로운 상태 값을 만든다.
4) 새 상태값을 Store에 저장한다.
5) 컴포넌트는 새로운 상태값을 받아온다(props를 통한 것이기 때문에 새로 랜더링 됨)
const LOAD = 'bucket/LOAD';
const CREATE = 'bucket/CREATE';
: 초기 상태값(기본 값)
const initialState = {
list: ["영화관 가기", "매일 책읽기", "수영 배우기"],
};
export const loadBucket = (bucket) => {
return { type: LOAD, bucket };
}
export const createBucket = (bucket) => {
return {type: CREATE, bucket};
}
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
// do reducer stuff
case "bucket/LOAD":
return state;
case "bucket/CREATE":
const new_bucket_list = [...state.list, action.bucket];
return { list: new_bucket_list };
default:
return state;
}
}
//configStore.js
import { createStore, combineReducers } from "redux";
import bucket from './modules/bucket';
import { createBrowserHistory } from "history";
// 브라우저 히스토리를 만들어줍니다.
export const history = createBrowserHistory();
// root 리듀서를 만들어줍니다.
// 나중에 리듀서를 여러개 만들게 되면 여기에 하나씩 추가해주는 거예요!
const rootReducer = combineReducers({ bucket });
// 스토어를 만듭니다.
const store = createStore(rootReducer);
export default store;
//index.js
...
// 리덕스를 주입해줄 프로바이더를 불러옵니다!
import { Provider } from "react-redux";
// 연결할 스토어도 가지고 와요.
import store from "./redux/configStore";
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById("root")
);
reportWebVitals();
...
// redux hook을 불러옵니다.
import {useDispatch, useSelector} from 'react-redux';
...
// 버킷리스트를 리덕스 훅으로 가져오기
const bucket_list = useSelector(state => state.bucket.list);
...
//list를 나열한 map이 있다면 여기에 bucket_list로 적용
<ListStyle>
{bucket_list.map((list, index) => {
return (
<ItemStyle
className="list_item"
key={index}
onClick={() => {
props.history.push("/detail");
}}
>
{list}
</ItemStyle>
);
})}
</ListStyle>
☝🏻 상세 페이지를 확인하기 위해서 URL파라미터를 적용할 수 있다.
//App.js <Route path="/detail/:index" render={(props) => <Detail match={props.match} history={props.history} />} /> //해당 js <ItemStyle className="list_item" key={index} onClick={() => { // 배열의 몇번째 항목을 눌렀는 지, url 파라미터로 넘겨줍니다. props.history.push("/detail/"+index); }}> {list} </ItemStyle>
...
// redux hook을 불러옵니다.
import { useDispatch, useSelector } from "react-redux";
// 내가 만든 액션 생성 함수를 불러옵니다.
import {deleteBucket} from "./redux/modules/bucket";
const Detail = (props) => {
//dispatch > 액션 발생시키는 내장함수
const dispatch = useDispatch();
...
<button onClick={() => {
// dispatch(); <- 괄호안에는 액션 생성 함수
// 예를 들면 이렇게요.
dispatch(deleteBucket(bucket_index));
props.history.goBack();
}}>삭제하기</button>
☝🏻 firebase, redux-thunk(미들웨어) 패키지 설치
- 미들웨어?
: 리덕스 데이터를 수정할 때 액션이 디스패치 되고 > 리듀서에서 처리하는 과정 사이에 미리 사전 작업을 할 수 있도록 하는 중간 다리같은 역할
즉, 액션이 일어나고 > 미들웨어가 할 일 하고 > 리듀서에서 처리- redux-thunk?
: 객체 대신 함수를 생성하는 액션 생성함수를 작성할 수 있게 해준다.
-> 왜 필요할까?
: 리덕스는 기본적으로 액션 객체를 디스패치 한다. 즉, 함수를 생성하면 특정 액션이 발생하기 전에 조건을 주거나, 어떤 행동을 사전에 처리할 수 있다.
1) firebase.js 파일 만들기
//firebase.js
import firebase from "firebase/app";
import "firebase/firestore";
const firebaseConfig = {
// firebase 설정과 관련된 개인 정보
};
// firebaseConfig 정보로 firebase 시작
firebase.initializeApp(firebaseConfig);
// firebase의 firestore 인스턴스를 변수에 저장
const firestore = firebase.firestore();
// 필요한 곳에서 사용할 수 있도록 내보내기
export { firestore };
2) 필요한 곳에서 firestore 가져오기
import { firestore } from "./firebase";
3) 파이어 베이스랑 통신하는 함수 만들기
const bucket_db = firestore.collection("bucket");
// 파이어베이스랑 통신하는 부분
export const loadBucketFB = () => {
return function (dispatch) {
bucket_db.get().then((docs) => {
let bucket_data = [];
docs.forEach((doc) => {
// 도큐먼트 객체를 확인해보자!
console.log(doc);
// 도큐먼트 데이터 가져오기
console.log(doc.data());
// 도큐먼트 id 가져오기
console.log(doc.id);
if(doc.exists){
bucket_data = [...bucket_data, {id: doc.id, ...doc.data()}];
}
});
console.log(bucket_data);
// 이제 액션 생성 함수한테 우리가 가져온 데이터를 넘겨줘요! 그러면 끝!
dispatch(loadBucket(bucket_data));
});
};
};
4) 필요하다면 리듀서 수정
case "bucket/LOAD": {
if(action.bucket.length >0){
return { list: action.bucket };
}
return state;
}
5) 불러서 쓰기!
⭐️ configStore.js에 미들웨어 추가하기
import { createStore, combineReducers, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import bucket from "./modules/bucket";
import { createBrowserHistory } from "history";
export const history = createBrowserHistory();
const middlewares = [thunk];
const enhancer = applyMiddleware(...middlewares);
const rootReducer = combineReducers({ bucket });
const store = createStore(rootReducer, enhancer);
export default store;
세상에.