AWS S3 버킷

S3 버킷이란?

  • S3(Simple Storage Service) : 단순 스토리지 서비스
  • 이미지, html, css, js 같은 정적 파일을 저장할 수 있고 정적 웹 호스팅이 가능함

정적 웹 사이트란?

  • 서버 측 스크립트(PHP, JSP, ASP 등) 사용 유무를 기준으로 동적 웹 페이지와 정적 웹 페이지로 나눌 수 있음
  • 정적 웹 사이트는 html, css, js 같은 정적 자원으로만 이루어진 웹 사이트

 

S3 버킷 설정하기

1. 버킷 생성하기 (사이트 도메인과 버킷 이름이 같아야함)

2. 권한 탭에서 ARN([ARN: arn:aws:s3:::[도메인 주소]])를 복사

3. 정책 생성기를 눌러서 정책 생성

  • Step 1) Select Type of Policy : S3 Bucket Policy
  • Step 2) Actions : GetObjcet

4. 생성한 정책을 복사해서 버킷 정책 편집기에 붙여넣기

  • arn 뒤에 반드시 /* 써주기!!

5. S3 버킷에 결과물 올리기

  • 빌드하기
yarn build
  • build 폴더 내의 파일을 버킷에 올리기

6. 정적 웹 사이트 호스팅 설정하기

7. 엔드포인트 주소를 클릭해서 호스팅 확인

 

도메인 연결하기

  • Route 53에서 호스팅 영역 생성
  • 도메인 이름 작성, 유형은 퍼블릭 호스팅 영역 선택
  • 네임서버를 가비아(도메인을 산 곳)에 등록함 (레코드 이름, 값/트래픽 라우팅 대상)
  • 레코드 생성

Firebase로 배포하기

Firebase 호스팅

  • Firebase 대시보드에서 호스팅 신청
  • 프로젝트에 cli 설치
yarn global firebase-tools
  • Firebase에 로그인 후 init 실행
yarn firebase login #웹브라우저에서 내 구글 계정으로 로그인
yarn firebase init
  • Hosting 선택, Use an existing project, Firebase 프로젝트 선택
  • What do you want to use as your public directory? → public

 

Firebase에 결과물 올리기

  • firebase.json 확인
  • 빌드한 결과물 올리기
yarn firebase deploy
  • Firebase 대시보드 → 호스팅 이동, 도메인으로 들어가서 확인

Material UI

공식 문서

https://material-ui.com/

 

Material-UI: A popular React UI framework

React components for faster and easier web development. Build your own design system, or start with Material Design.

material-ui.com

 

설치하기

yarn add @material-ui/core @material-ui/icons

 

사용하기

Detail.js

import Button from '@material-ui/core/Button';
import ButtonGroup from '@material-ui/core/ButtonGroup';
...

const Detail = (props) => {
	...
	return (
		...
		<ButtonGroup>
			<Button
				variant="outlined"
				onClick={() => { /*생략*/ }}
			>삭제하기</Button>
			<Button
				variant="outlined"
				onClick={() => { /*생략*/ }}
			>완료하기</Button>
		</ButtonGroup>
		...
	);
};

export default Detail;

 


페이지 의도적으로 가리기

페이지 가리기가 필요한 이유

  • 현재는 redux에 넣어둔 초깃값 (가짜 데이터) 이 먼저 보인다.
  • Firestore의 데이터만 제대로 보여주기 위해 데이터를 가져오기 전까지는 페이지를 가린다.
  • 수정 또는 추가하기 버튼을 눌렀을 때 API를 여러번 호출하는 현상도 방지할 수 있다.

 

로딩 스피너 만들기

로딩 스피너 컴포넌트 만들기

  • 머터리얼 UI 아이콘 이용
import React from 'react';
import styled from 'styled-components';
import {Eco} from '@material-ui/icons';

const Spinner = (props) => {
	return (
		<Outter>
			<Eco style={{color: '#673ab7', fontSize: '150px'}}/>
		</Outter>
	);
}

const Outter = styled.div`
	position: fixed;
	top: 0;
	left: 0;
	width: 100vw;
	height: 100vh;
	display: flex;
	align-items: center;
	justify-content: center;
	background-color: #ede2ff;
`;

export default Spinner;

 

bucket.js

  • initialState에 is_loaded 변수를 추가해서 firestore에서 데이터를 받아오면 갱신하도록 함
const initialState = {
	is_loaded: false,
	list: [
		{text: '영화관 가기', completed: false},
		{text: '매일 책읽기', completed: false},
		{text: '수영 배우기', completed: false},
	],
};

...
	case 'bucket/LOAD': {
		if (action.bucket.length > 0) {
			return {list: action.bucket, is_loaded: true};
		}
		return state;
	}

 

App.js

  • is_loaded 값에 따라 조건부 렌더링을 한다.
const mapStateToProps = (state) => {
	bucket_list: state.bucket.list,
	is_loaded: state.bucket.is_loaded,
});
...
render() {
	<div className='App'>
		<Container>
			<Title>내 버킷리스트</Title>
			{!this.props.is_loaded ? (
				<Spinner/>
				) : (
					<React.Fragment>
						<Progress />
						<Line />
						<Switch>
							...
						</Switch>
						</React.Fragment>
				)}
			</Container>
		...

redux-thunk란?

  • Firestore에서 데이터를 가져올 때 비동기 통신을 함
  • 리덕스에서 비동기 통신을 할 때 미들웨어가 필요함
  • 일반 액션 생성 함수는 객체를 반환하는데 redux-thunk는 객체 대신 함수를 생성하는 액션 생성함수를 작성할 수 있게 해줌
  • 함수를 생성하면 특정 액션이 발생하기 전에 조건을 주거나 행동을 할 수 있음

미들웨어

  • 리덕스 데이터를 수정할 때 액션 디스패치 → 리듀서에서 처리
  • 미들웨어가 있으면 액션 디스패치 → 미들웨어가 할일 → 리듀서에서 처리

설치하기

yarn add redux-thunk

configStore.js

  • redux-thunk 적용 전 configStore.js
import {createStore, combineReducers} from 'redux';
import bucket from './modules/bucket';
import {createBrowserHistory} from 'history';

export const history = createBrowserHistory();
const rootReducer = combineReducers({bucket});
const store = createStore(rootReducer);

export default store;
  • redux-thunk 적용 후 ⬇
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;

 

Load하기

bucket.js 수정

  • Firebase랑 통신하는 함수 생성
const bucket_db = firestore.collection('bucket');

export const loadBucketFB = () => {
	return function (dispatch) {
		bucket_db.get().then((docs) => {
			let bucket_data = [];
			docs.forEach((doc) => {
				if(doc.exists){
					bucket_data = [...bucket_data, {id: doc.id, ...doc.data()}];
				}
			});

			dispatch(loadBucket(bucket_data));
		});
	};
};
  • 리듀서 수정
case 'bucket/LOAD': {
	if (action.bucket.length > 0) {
		return {list: action.bucket};
	}

	return state;
}

App.js 수정

const mapDispatchToProps = (dispatch) => ({
	load: () => {
		dispatch(loadBucketFB());
	},
	create: (new_item) => {
		dispatch(createBucket(new_item));
	}
});

 

Create 하기

bucket.js 수정

  • Firebase와 통신하는 함수 생성
export const addBucketFB = (bucket) => {
	return function (dispatch) {
		let bucket_data = {text: bucket, completed: false};
		bucket_db
			.add(bucket_data)
			.then((docRef) => {
				bucket_data = {...bucket_data, id: docRef.id};
				dispatch(createBucket(bucket_data));
			});
			.catch((err) => {
				window.alert('오류 발생');
			});
	};
};
  • 리듀서 수정
case 'bucket/CREATE': {
	const new_bucket_list = [
		...state.list,
		action.bucket,
	];
	return {list: new_bucket_list};
}

App.js

const mapDispatchToProps = (dispatch) => ({
	load: () => {
		dispatch(loadBucketFB());
	},
	create: (new_item) => {
		dispatch(addBucketFB(new_item));
	}
});

 

Update 하기

bucket.js

export const updateBucketFB = (bucket) => {
	return function (dispatch, getState) {
		const _bucket_data = getState().bucket.list[bucket];
		if (!_bucket_data.id) {
			return;
		}

		let bucket_data = {..._bucket_data, completed: true};
		bucket_db
			.doc(bucket_data.id)
			.update(bucket_data)
			.then((res) => {
				dispatch(updateBucket(bucket));
			})
			.catch((err) => {
			});
	};
};

Detail.js

<button onClick={() => {
		dispatch(updateBucketFB(bucket_index));
		props.history.goBack();
	}}>완료하기</button>

 

Delete 하기

bucket.js

export const deleteBucketFB = (bucket) => {
	return function (dispatch, getState) {
		const _bucket_data = getState().bucket.list[bucket];
		if (!_bucket_data.id) {
			return;
		}
		bucket_db
			.doc(_bucket_data.id)
			.delete()
			.then((res) => {
				dispatch(deleteBucket(bucket));
			})
			.catch((err) => {
			});
	};
};

Detail.js

<button onClick={() => {
		dispatch(deleteBucketFB(bucket_index));
		props.history.goBack();
	}}>삭제하기</button>

서버와 서버리스

웹의 동작 방식

  • 클라이언트가 서버에게 요청을 하면, 서버가 클라이언트에게 응답함
  • 서버는 데이터 관리, 분산 처리, 웹 어플리케이션 작동 등의 역할을 함
  • 서버를 직접 관리하지 않고 필요한 기능만 빌려서 쓴다. → 서버리스 (서버가 존재하지 않는다 X)

Firebase란?

  • BaaS (Backend as a Service) : 데이터베이스, 소셜 서비스 연동, 파일 시스템 등 백엔드에서 처리할 작업들을 API로 제공 (빌려옴)
  • Firebase는 머신러닝, 인증, 호스팅 등 다양한 기능을 제공하는 BaaS

Firebase 설정하기

  • Firebase 사이트에서 프로젝트 만들기 → 이름을 정하고 애널리틱스 설정 → 나라 선택 후 프로젝트 만들기 완료

Firestore란?

  • Firebase에 포함되어 있는 서비스 중 하나
  • 유연하고 확장 가능한 NoSQL 클라우드 데이터베이스
  • 구조
    1. Collection : 문서(Document)의 집합
    2. Document : JSON 형식으로 데이터 저장

Firestore 설정하기

  • 생성된 프로젝트 클릭 → Cloud Firestore 추가 → 데이터베이스 만들기 클릭 → 보안 규칙 설정 → 위치 설정 후 완료
  • 보안 규칙 설정할 때 test 모드로 해야 로컬호스트에서 데이터 요청이 가능하다.
  • 대시보드에서 컬렉션 시작 버튼 → 문서 ID, 필드, 값 넣고 저장

리액트에 Firebase 연동하기

Firebase 패키지 설치

yarn add firebase

Config 가져오기

  • Firebase 대시보드에서 버튼 클릭 → 앱 이름 적고 완료하면 Firebase SDK 추가 스크립트 생김
  • src 하위 폴더에 firebase.js 파일 만들고 스크립트 내용 중 firebaseConfig 부분 붙여넣기
  • App.js에서 firebase.js의 firestore 내보내기
import { firestore } from "./firebase";
  • componentDidMount에서 테스트 해보기
const bucket = firestore.collection("buckets");
	//하나만 확인
	bucket
		.doc("bucket_item")
		.get()
		.then((doc) => {
			if(doc.exists){
				console.log(doc.data());
			}
		});

	//전체 확인
	bucket
		.get()
		.then((docs) => {
			let bucket_data = [];
			docs.forEach((doc) => {
				console.log(doc); //document 객체
				console.log(doc.data()); //document 데이터
				console.log(doc.id); //document id

				if(doc.exists){
					bucket_data = [...bucket_data, {id: doc.id, ...doc.data() }];
				}
			});
			console.log(bucket_data);

Firestore 사용해보기

데이터 추가하기

  • 콜렉션에 add
bucket.add({text: "수영 배우기", completed: false});

데이터 수정하기

  • 콜렉션에 document id로 update
bucket.doc("bucket_item").update({text: "수영 배우기", completed: false});

데이터 삭제하기

  • 콜렉션에 document id로 delete
bucket.doc("bucket_item").delete([도큐먼트 id]);

새로운 콜렉션 추가하기

const bucket = firestore.collection("buckets"); bucket.doc("bucket_item").set({text: "수영 배우기", completed: false});

keyframes

  • styled-components 안에 들어있음
  • 웹에서 애니메이션을 구현할 때 transition과 animation이라는 스타일 속성을 많이 사용함
  • transition은 단순한 엘리먼트 상태 변화 시, animation은 다이나믹한 효과를 줄 때 적합
  • keyframes는 animation에서 사용하는 속성 중 하나!
  • css에서 keyframes를 사용하는 방법
@keyframes boxFade {
	0% {
		opacity: 1;
	}
	50% {
		opacity: 0;
	}
	100% {
		opacity: 1;
	}
}

 

styled-components에서 keyframes 사용하기

styled-components 패키지 설치

yarn add styled-components

위 아래로 움직이는 애니메이션

const Box = styled.div`
	width: 100px;
	height: 100px;
	border-radius: 50px;
	background: green;
	position: absolute;
	top: 20px;
	left: 20px;
	animation: ${boxFade} 2s 1s infinite linear alternate;
`;
  • styled 안에서 변수를 이용하기 위해 ${변수이름} 형식으로 작성
  • animation duration : 2s, 1s
  • animation-iteration-count : infinite
const boxFade = keyframes`
	0% {
		opacity: 1;
		top: 20px;
	}
	50% {
		opacity: 0;
		top: 400px;
	}
	100% {
		opacity: 1;
		top: 20px;
	}
`;

 

스크롤 움직이기

  • window.scrollTo() 이용
<button onClick={() => { window.scrollTo(0, 0); }}>위로가기</button>
  • 웹페이지 상단으로 스크롤x 이동함
<button onClick={() => { window.scrollTo({top: 0, left: 0, behavior: "smooth"}); }}>위로가기</button>
  • behavior을 추가해서 자연스러운 스크롤 구현
  • 좌표뿐만 아니라 ref를 통해 특정 엘리먼트의 위치로 스크롤 시킬 수도 있다.
  • 리덕스 패키지 설치하기
yarn add redux react-redux

리덕스란?

  • 데이터를 한 곳에 몰아넣고 이곳저곳에서 꺼내볼 수 있게 하는, 상태 관리

State

  • 저장하고 있는 상태값(=데이터)
  • 딕셔너리 형태({[key]: value}) 형태로 보관

Action

  • 상태에 변화가 필요할 때 (가지고 있는 데이터를 변경할 때) 발생하는 것
{type: 'CHANGE_STATE', data: {...}} //type = 이름

ActionCreator

  • 액션 생성 함수, 액션을 만들기 위해 사용함
const changeState = (new_data) => { //액션을 리턴한다 return { type: 'CHANGE_STATE', data: new_data, } }

Reducer

  • 리덕스에 저장된 상태(=데이터)를 변경하는 함수
  • 액션 생성 함수 호출 → 액션을 만들면 → 리듀서가 현재 상태(=데이터)와 액션 객체를 받아서 → 새로운 데이터를 만들고 → 리턴
//기본 상태값을 임의로 지정했음
const initialState = {
	name: 'yewon'
}

function reducer(state = initialState, action) {
	switch(action.type){
		//action의 type마다 케이스문을 작성하면
		//액션에 따라 새로운 값을 리턴할 수 있음
		case CHANGE_STATE:
			return {name: 'garam'};
		default:
			return false;
	}
}

Store

  • 프로젝트에 리덕스를 적용하기 위해 만드는 것
  • 리듀서, 현재 애플리케이션 상태, 리덕스에서 값을 가져오고 액션을 호출하기 위한 내장 함수 포함

dispatch

  • 액션을 발생시키는, 스토어의 내장 함수

리덕스의 특징 3가지

  1. store은 프로젝트 하나 당 1개만 쓴다.
  2. store의 state는 오직 action으로만 변경할 수 있다.
  3. 어떤 요청이 와도 리듀서는 같은 동작을 해야한다.
  • 리듀서는 순수한 함수여야 한다.
    • 파라미터 외의 값에 의존하지 않는다.
    • 이전 상태는 수정하지(=건드리지) 않는다.
    • 파라미터가 같으면 항상 같은 값을 반환
    • 리듀서는 이전 상태와 액션을 파라미터로 받는다.

리덕스를 통한 리액트 상태 관리

  • 여러 컴포넌트가 동일한 상태를 보고 있을 때 유용하게 사용
  • 데이터를 관리하는 로직을 컴포넌트에서 빼면, 컴포넌트에서는 뷰만 관리할 수 있음 → 유지보수하기 좋다

상태 관리 흐름도

  1. 리덕스 store을 component에 연결
  2. component에서 상태 변화가 필요할 때 action 호출
  3. reducer을 통해 새로운 상태 값을 만듦
  4. 새 상태값을 store에 저장
  5. component는 새로운 상태값을 받아옴 (props를 통해 받아오므로 다시 렌더링됨)


덕스(ducks) 구조

  • 보통 리덕스를 사용할 때, action, actionCreator, reducer를 분리해서 작성한다.
  • 덕스 구조는 모양새로 묶는 대신 기능으로 묶어 작성한다.
  • (ex. 버킷리스트의 action, actionCreator, reducer을 한 파일에 넣는다.)

모듈 만들기

  • src 폴더 - redux 폴더 - modules 폴더 안에 bucket.js 만들기
  • bucket.js
//Action
const LOAD = 'bucket/LOAD';
const CREATE = 'bucket/CREATE';

//initialState
const initialState = {
	list: ['영화관 가기', '매일 책읽기', '수영 배우기'],
};

//Action Creator
export const loadBucket = (bucket) => {
	return {type: LOAD, bucket};
}

export const createBucket = (bucket) => {
	return {type: CREATE, bucket};
}

//Reducer
export default function reducer(state = initialState, action = {}) {
	switch (action.type) {
		case 'bucket/LOAD':
			return state;

		case 'bucket/CREATE':
			const new_bucket_list = [...state.list, action.bucket];
			return {list: new_bucket_list};

		default:
			return state;
	}
}

 

  • src 폴더 - redux 폴더 - configStore.js 만들기
  • 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 React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import {BrowserRouter} from 'react-router-dom';

//버킷리스트에 리덕스를 주입해줄 프로바이더
import {Provider} from 'react-redux';
//연결할 스토어
import store from './redux/configStore';

ReactDOM.render(
	<Provider store={store}>
		<BrowserRouter>
			<App />
		</BrowserRouter>
	</Provider>,
	document.getElementById('root')
);

컴포넌트에서 리덕스 데이터 사용하기

1. 클래스형 컴포넌트에서 리덕스 데이터 사용하기

  • 리덕스 모듈과 connect 함수 불러오기
  • 상태값을 가져오는 함수(mapStateToProps)와 액션 생성 함수를 부르는 함수(mapDispatchToProps) 만들기
  • connect로 컴포넌트와 스토어 엮기
  • this.state에 있는 list를 지우고 스토어에 있는 값으로 바꾸기
  • setState를 this.props.create로 바꾸기
  • App.js
import React from 'react';
import {withRouter} from 'react-router';
import {Route, Switch} from 'react-router-dom';

//리덕스 스토어와 연결하기 위해 connect 호출
import {connect} from 'react-redux';
//리덕스 모듈에서 액션 생성 함수 가져오기
import {loadBucket, createBucket} from './redux/modules/bucket';

//스토어가 가진 상태값을 props로 받아오기 위해
const mapStateToProps = (state) => ({
	bucket_list: state.bucket.list,
});

//액션 생성 함수를 props로 받아오기 위한 함수
const mapDispatchToProps = (dispatch) => ({
	load: () => {
		dispatch(loadBucket());
	},
	create: (new_item) => {
		dispatch(createBucket(new_item));
	}
});

class App extends React.Component {
	constructor(props){
		super(props);
		this.state = {
		};
		this.text = React.createRef();
	}

	componentDidMount(){
		console.log(this.props);
	}

	addBucketList = () => {
		const new_item = this.text.current.value;
		this.props.create(new_item);
	};

	render(){
		return(
			<div className="App">
				<Container>
					<Title>내 버킷리스트</Title>
					<Line/>
					<Switch>
						<Route path="/" exact
							render={(props) => (
								<BucketList
									list={this.props.bucket_list}
									history={this.props.history}
								/>
							)}
						/>
						<Route path="/detail" component={Detail}/>
						<Route
							render={(props) => <NotFound history={this.props.history} />}
						/>
					</Switch>
				</Container>
				<Input>
					<input type='text' ref={this.text}/>
					<button onClick={this.addBucketList}>추가하기</button>
				</Input>
			</div>
		);
	}
}

//connect로 묶기
export default connect(mapStateToProps, mapDispatchProps)(withRouter(App));

 

2. 함수형 컴포넌트에서 리덕스 데이터 사용하기

  • 훅을 사용해서 액션 생성 함수를 부르고 스토어에 저장된 값을 가져온다
  • BucketList.js에 useSelector() 적용
import React from 'react';
//reudx hook 불러오기
import {useDispatch, useSelector} from 'react-redux';

const BucketList = (props) => {
	const bucket_list = useSelector(state => state.bucket.list);

	return (
		<ListStyle>
			{bucket_list.map((list, index) => {
				return (
					<ItemStyle
						className='list_item'
						key={index}
						onClick={() => {
							props.history.push('/detail');
						}}
					>
						{list}
					</ItemStyle>
				);
			})}
		</ListStyle>
	);
};

export default BucketList;

 

3. 상세페이지에서 버킷리스트 내용 띄우기

  • Detail.js
import React from 'react';
//redux hook 불러오기
import {useDispatch, useSelector} from 'react-redux';

const Detail = (props) => {
	//스토어에서 상태값 가져오기
	const bucket_list = useSelector((state) => state.bucket.list);
	//url 파라미터에서 인덱스 가져오기
	let bucket_list = parseInt(props.match.params.index);

	return <h1>{bucket_list[bucket_index]}</h1>;
};

export default Detail;

SPA (Single Page Application)

  • 서버에서 주는 html이 1개만 있는 어플리케이션
  • 전통적인 웹사이트 : 페이지를 이동할 때마다 정적자원들(html, css, js)을 내려줌
  • SPA : 딱 한번만 정적자원을 받아옴

라우팅

  • 브라우저 주소에 따라 다른 페이지를 보여주는 것
  • SPA에서 html은 딱 하나만 있지만 브라우저 주소창대로 다른 페이지를 보여줄 수 있음

리액트에서 라우팅 처리하기

  • react-route-dom 패키지 설치하기
yarn add react-router-dom

적용하기

1. index.js에 BrowserRouter 적용하기

import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom';

ReactDOM.render(
	<BrowserRouter>
		<App />
	</BrowserRouter>
	document.getElementById("root")
);
  • BrowserRouter : 웹 브라우저가 가지고 있는 주소 정보를 props로 넘겨줌

 

2. 세부 화면 만들기

  • Home.js
const Home = (props) => {
	return (
		<div>메인 화면</div>
	)
}

export default Home;
  • Cat.js
const Cat = (props) => {
	return (
		<div>고양이</div>
	)
}

export default Cat;
  • Dog.js
const Dog = (props) => {
	return (
		<div>강아지</div>
	)
}

export default Dog;

 

3. App.js에서 Route 적용하기

  • Route 사용 방법 1) 넘겨줄 props가 없을 때
<Route path="주소[ex. /home]" component={[보여줄 컴포넌트]}/>
  • Route 사용 방법 2) 넘겨줄 props가 있을 때
<Route path="주소" render={(props) => (<BucketList list={this.state.list}/>)} />
  • App.js에 적용하기
import {Route} from 'react-router-dom';

class App extends React.Component {
	constructor(props){
		super(props);
		this.state={};
	}

	render(){
		return (
			<div className="App">
				<Route path="/" exact component={Home}/>
				<Route path="/cat" component={Cat}/>
				<Route path="/dog" component={Dog}/>
			</div>
		);
	}
}

export default App;
  • "/"가 "/cat"와 "/dog"에 포함되어 있기 때문에 exact 추가

4. URL 파라미터 사용하기

  • 파라미터 : /cat/nabi
  • 쿼리 : /cat?name=nabi
  • App.js
<Route path="/cat/:cat_name" component={Cat}/>
  • Cat.js에서 console.log(props.match); 콘솔로 파라미터 확인 가능

5. 링크 이동시키기

1) <Link/> 사용하기

<Link to="주소">[텍스트]</Link>
  • App.js
import {Route, Link} from 'react-router-dom';

class App extends React.Component {
	constructor(props){
		super(props);
		this.state={};
	}

	render(){
		return (
			<div className="App">
				<div>
					<Link to="/">Home으로 가기</Link>
					<Link to="/cat">Cat으로 가기</Link>
					<Link to="/dog">Dog으로 가기</Link>
				</div>

				<hr/>
				<Route path="/" exact component={Home}/>
				<Route path="/cat" component={Cat}/>
				<Route path="/dog" component={Dog}/>
			</div>
		);
	}
}

export default App;

2) history 사용하기

  • App.js
import {withRouter} from 'react-router';
import {Route, Link} from 'react-router-dom';

class App extends React.Component {
	constructor(props){
		super(props);
		this.state={};
	}

	componentDidMount(){
		console.log(this.props);
	}

	render() {
		return (
			<div className="App">
				<div>
					<Link to="/">Home으로 가기</Link>
					<Link to="/cat">Cat으로 가기</Link>
					<Link to="/dog">Dog으로 가기</Link>
				</div>
	
				<hr/>
				<Route path="/" exact component={Home}/>
				<Route path="/cat" component={Cat}/>
				<Route path="/dog" component={Dog}/>
	
				<button onClick={() => {
					this.props.history.push("/cat");
				}}>
					cat으로 가기
				</button>
				<button onClick={() => {
					this.props.history.goBack();
				}}>뒤로가기</button>
			</div>
		);
	}
}
//내보내는 부분에서 withRouter로 감싸주기
export default withRouter(App);
  • push([이동할 주소]) : 페이지 이동시킴
  • goBack() : 뒤로가기

 

잘못된 주소 처리하기 (Switch 이용)

  • NotFound.js
const NotFound = (props) => {
	return <h1>주소가 올바르지 않습니다</h1>;
);

export default NotFound;
  • App.js
render() {
	return (
		<div className="App">
			...
			<Switch>
				<Route
					path="/"
					exact
					render={(props) => {
						<BucketList
							list={this.state.list}
							history={this.props.history}
						/>
					}}
				/>
				<Route path="/detail" component={Detail}/>
				<Route component={NotFound}/>
			</Switch>
			...
		</div>
	);
}
  • NotFound 컴포넌트를 주소 없이 연결함
  • Switch는 첫번째로 매칭되는 path를 가진 컴포넌트를 렌더링함

SCSS

  • CSS를 편하게 쓸 수 있도록 도와줌
  • node-sass 설치하기
yarn add node-sass@4.14.1 open-color sass-loader classnames
  • 중첩된 속성
div {
	p {
		color: #888888;
		font: {
			family: sans-serif;
			size: 14px;
		}
	}

	img {
		width: 400px;
	}
}
  • 상위 요소 이어쓰기 (& 사용)
div {
	background-color: green
	&:hover { background-color: blue }
}

.div {
	background-color: green
	&_blue { background-color: blue }
}
  • 변수 사용
$defaultSize: 20px;
$className: blue;

p {
	font-size: #{$defaultSize};
	&.#{$className} {color: #{$className}}
}

styled-components

  • 컴포넌트에 스타일을 직접 입히는 방식
  • styled-components 패키지 설치하기
yarn add styled-components
  • class 이름 지을 필요 없어짐, 간단하고 직관적임
import React from 'react';
import styled from 'styled-components';

function App() {
	return (
		<div className="App">
			<MyStyled bgColor={"red"}>hello react!</MyStyled>
		</div>
	);
}

const MyStyled = styled.div`
	width: 50vw;
	min-height: 150px;
	padding: 10px;
	border-radius: 15px;
	color: #fff;
	&:hover {
		background-color: #ddd;
	}
	background-color: ${(props) => (props.bgColor ? "red" : "purple")};
`;

export default App;

 


가상돔

DOM (Document Object Model)

  • HTML의 단위 하나하나를 객체로 생각하는 모델
  • 트리 구조를 가짐
  • 수정이 일어날 때마다 모든 DOM을 조회해서 수정할 것을 찾고 전부 수정하면 연산이 많이 일어남 → 가상돔의 필요성

가상돔 (Virtual DOM)

  • 메모리 상에서 돌아가는 가짜 DOM
  • 기존 DOM과 새로 그린 DOM을 비교해서 바뀐 부분만 수정

라이프 사이클

  • 컴포넌트가 렌더링을 준비하는 순간 ~ 페이지에서 사라질 때까지

  • 컴포넌트 생성수정(업데이트)제거
  • 생성 : 처음으로 컴포넌트를 불러오는 단계
  • 수정(업데이트) : 사용자의 행동으로 데이터가 바뀌거나 부모 컴포넌트가 렌더링할 때 발생
  • ex. props가 바뀔 때, state가 바뀔 때, 부모 컴포넌트가 업데이트 되었을 때 (리렌더링), 강제로 업데이트했을 때 (forceUpdate())
  • 제거 : 페이지를 이동하거나 사용자의 행동으로 인해 컴포넌트가 화면에서 사라지는 단계

라이프 사이클 함수

  • constuctor() : 생성자 함수, 컴포넌트가 생성되면 가장 처음 호출되는 함수
  • render() : 컴포넌트의 모양을 정의하는 함수
  • componentDidMount() : 첫번째 렌더링을 마친 후에 실행, 리렌더링시에는 호출되지 않음 (컴포넌트가 화면에 나타나는 것 → 마운트(Mount) 한다고 표현함)
  • componentDidUpdate(prevProps, prevState, snapshot) : 리렌더링을 완료한 후 실행되는 함수, prevProps/prevState는 업데이트 전 props/state
  • componentWillUnmount() : 컴포넌트가 DOM에서 제거될 때 실행하는 함수, 컴포넌트에 이벤트 리스너를 등록했다면 반드시 해제해야 함
import React from 'react';

class LifecycleEx extends React.Component {
	//생성자 함수
	constructor(props) {
		super(props);
		this.state = {
			cat_name: '나비',
		};
		console.log('in constructor!');
	}

	componentDidMount() {
		console.log('in componentDidMount!');
	}

	componentDidUpdate(prevProps, prevState) {
		console.log(prevProps, prevState);
		console.log('in componentDidUpdate!');
	}

	componentWillUnmount() {
		console.log('in componentWillUnmount!');
	}

	render() {
		return (
			<div>
				<h1>고양이 이름은 {this.state.cat_name}</h1>
				<button onClick={this.changeCatName}>고양이 이름 바꾸기</button>
			</div>
		);
	}
}

export default LifecycleEx;
  • 라이프 사이클 함수는 클래스형 컴포넌트에서만 사용 가능함

Ref

  • 리액트에서 DOM 요소를 가져올 때 사용
class App extends React.Component {
	constructor(props) {
		super(props);
		this.state = {
			list: ['영화관 가기', '매일 책읽기', '수영 배우기']
		};
		this.text = React.createRef(); //ref 선언하기
	}

	componentDidMount() {
		console.log(this.text);
		console.log(this.text.current);
	}

	render() {
		return (
			<div className="App">
				...
				<input type="text" ref={this.text}/>
			</div>
		);
	}
}

 


클래스형 컴포넌트 state 관리

  • setState() : 클래스형 컴포넌트의 state를 업데이트할 때 사용하는 함수
  • 네모 추가/삭제 예제
더보기
import React from 'react';

class App extends React.Component {
	constructor(props) {
		super(props);
		this.state = {
			count: 3,
		};
	}

	componentDidMount() {}

	addNemo = () => {
		this.setState({count: this.state.count + 1});
	};

	removeNemo = () => {
		if (this.state.count > 0) {
			this.setState({count: this.state.count - 1});
		} else {
			window.alert('네모가 없습니다');
		}
	};

	render() {
		const nemo_count = Array.from({length: this.state.count}, (v, i) => i);
		console.log(nemo_count);
		return (
			<div className="App">
				{nemo_count.map((num, idx) => {
					return (
						<div
							key={idx}
							style={{
								width: '150px',
								height: '150px',
								backgroundColor: '#ddd',
								margin: '10px',
							})
						></div>
					);
		}}}

			<div>
				<button onClick={this.addNemo}>하나 추가</button>
				<button onClick={this.removeNemo}하나 빼기</button>
			</div>
		);
	}
}

export default App;
  • addNemo, removeNemo에서 count 상태를 수정함

 

함수형 컴포넌트에서 state 관리

  • 함수형 컴포넌트는 state가 없음
  • 그러나 react hooks를 사용하면 state를 가질 수 있음
  • useState()를 이용하여 state 관리
  • 네모 추가/삭제 예제
더보기
import React from "react";

const Nemo = (props) => {
	//setCount로 count라는 state 값을 수정함
  const [count, setCount] = React.useState(3); //useState로 초깃값 설정

  const addNemo = () => {
    setCount(count + 1); //setCount로 count 1 증가
  };

  const removeNemo = () => {
    setCount(count > 0 ? count - 1 : 0); //setCount로 count 1 감소
  };

  const nemo_count = Array.from({ length: count }, (v, i) => i);
  return (
    <div className="App">
      {nemo_count.map((num, idx) => {
        return (
          <div
            key={idx}
            style={{
              width: "150px",
              height: "150px",
              backgroundColor: "#ddd",
              margin: "10px",
            }}
          >
            nemo
          </div>
        );
      })}

      <div>
        <button onClick={addNemo}>하나 추가</button>
        <button onClick={removeNemo}>하나 빼기</button>
      </div>
    </div>
  );
};

export default Nemo;

 


이벤트 리스너 (Event Listener)

  • 사용자가 어떤 행동(이벤트)을 하는지 지켜보고 알려줌
  • 마우스 클릭, 터치, 마우스 오버, 키보드 누름 등의 이벤트
  • onClick처럼 엘리먼트에 직접 넣어주는 방법 or addEventListener을 통해 추가하는 방법이 있음

클래스형 컴포넌트에서 Event Listener 구독하기

  • DOM 요소가 있어야 이벤트 발생하는지를 지켜볼 수 있음 → componentDidMount()에서 작성
  • addEventListener() : 이벤트 등록 함수
  • removeEventListener() : 이벤트 제거 함수
constructor(props) {
	super(props);
	this.state = {
		count: 3,
	};
	this.div = React.createRef();
}

hoverEvent = (e) => {
	console.log(e.target);
	if (e.target.className === "App") {
		e.target.style.background = "#eee";
	}
}

componentDidMount() {
	console.log(this.div);
	//addEventListener로 이벤트 등록
	this.div.current.addEventListener("mouseover", this.hoverEvent);
}

componentWillUnmount() {
	//컴포넌트가 제거될 때 이벤트 지우기
	this.div.current.removeEventListener("mouseover", this.hoverEvent);
}

return (
	<div className="App" ref={this.div}>
		<Nemo/>
	</div>
);

+ Recent posts