AWS EC2 서버 구입

  • Ubuntu Server 10.04 LTS 선택
  • Key Pair 생성 후 다운로드

 

AWS EC2 접속하기

  • Git bash 실행해서 다음과 같이 입력
ssh -i 받은키페어드롭 ubuntu@AWS에서EC2의IP
  • Key fingerprint 관련 메세지 → Yes 입력
  • Git bash 종료 시 exit 입력해서 ssh 접속 끊기

 

AWS 보안그룹 설정

  • 해당 EC2 인스턴스 보안그룹 설정

→ 80포트: HTTP 접속을 위한 기본포트

→ 5000포트: flask 기본포트

→ 27017포트: 외부에서 mongoDB 접속을 하기위한 포트

 

AWS EC2 세팅하기

FileZilla 접속

  • new site 만들기
  • SFTP 프로토콜로 설정, Host에 IP 주소 입력, 포트 번호 22
  • user 이름 입력, key file 찾기

EC2 한번에 세팅하기

sudo chmod 755 initial_ec2.sh
./initial_ec2.sh

 

AWS 배포하기

Robo 3T

  • Create 클릭해서 접속 정보 세팅
  • Connection : Name, Address - IP주소, 포트 번호 - 27017
  • Authentication : Databse 이름, User Name과 Password (현재는 test, test임)

FileZilla로 작업한 파일 업로드

  • app.py의 pymongo 세팅 부분을 바꿔서 업로드
client = MongoClient('mongodb://test:test@localhost', 27017)

Flask 패키지 설치 후 실행

pip install flask pymongo
python app.py

 

nohup 설정하기

  • SSH 접속을 종료해도 서버가 계속 실행되도록 함
# 실행하기
nohup python app.py &

# 종료하기
ps -ef | grep 'app.py' # pid값 확인
kill -9 [pid값] # 특정 프로세스 끝내기

나홀로일기장 만들기

 

서버-클라이언트 연결 코드 만들기

  • GET : 데이터 조회(Read) 요청 시 사용
  • POST : 데이터 생성(Create), 변경(Update), 삭제(Delete) 요청 시 사용
  • 기본 Flask 코드
from flask import Flask, render_template, jsonify, request
app = Flask(__name__)

@app.route('/')
def home():
    return render_template('index.html')

if __name__ == '__main__':
    app.run('0.0.0.0', port=5000, debug=True)

GET 요청

  • Javascript 로딩 후 실행
$(document).ready(function () {
	alert("!!");
})
  • GET 요청 Ajax 코드
$.ajax({
	type: "GET",
	url: "/diary?sample_give=샘플데이터",
	data: {},
	success: function(response){
		alert(response["msg"])
	}
})
  • GET 요청 API 코드
@app.route('/diary', methods=['GET'])
def show_diary():
	sample_receive = request.args.get('sample_give')
	print(sample_receive)
	return jsonify({'msg': 'GET 연결 완료!'})

POST 요청

  • POST 요청 Ajax 코드
$.ajax({
	type: "POST",
	url: "/diary",
	data: {sample_give:'샘플데이터'},
	success: function(response){
		alert(response['msg'])
	}
})
  • POST 요청 API 코드
@app.route('/diary', methods=['POST'])
def save_diary():
	sample_receive = request.form['sample_give']
	print(sample_receive)
	return jsonify({'msg': 'POST 연결 완료!'})

 

포스팅 API, 리스팅 API 만들기

포스팅 API 만들기

  • 서버
@app.route('/posting', methods=['POST'])
def posting():
	title_receive = request.form['title_give']
	content_receive = request.form['content_give']

	doc = {
		'title': title_receive,
		'content': content_receive
	}
	db.articles.insert_one(doc)

	return jsonify({'msg': '업로드 완료!'})
  • 클라이언트
function posting() {
	let title = $('#title').val()
	let content = $('#content').val()

	$.ajax({
		type: "POST",
		url: "/posting",
		data: {'title_give': title, 'content_give': content},
		success: function (response) {
			alert(response['msg'])
			window.location.reload()
		}
	});
}

리스팅 API 만들기

  • 서버
@app.route('/listing', methods=['GET'])
def listing():
	articles = list(db.articles.find({}, {'_id':False}))
	return jsonify({'articles': articles})
  • 클라이언트
$(document).ready(function() {
	listing()
})

function listing() {
	$.ajax({
		type: "GET",
		url: "/listing",
		data: {},
		success: function (response) {
			let articles = response['articles']
			for (let i = 0; i < articles.length; i++) {
				let title = articles[i]['title']
				let content = articles[i]['content']
				let temp_html = `<div class="card">
	                         <div class="card-body">
	                           <h5 class="card-title">${title}</h5>
                             <p class="card-text">${content}</p>
                           </div>
                         </div>`
				
				$('#cards-box').append(temp_html)
			}
		}
	})
}

 


파일업로드 준비

  • 파일업로드 라이브러리
<script src="https://cdn.jsdelivr.net/npm/bs-custom-file-input/dist/bs-custom-file-input.js"></script>
  • 파일업로드 코드
bsCustomFileInput.init()

서버 쪽 파일 받기, 클라이언트 쪽 보내기

서버 쪽 파일 받기 코드

file = request.files["file_give"]
save_to = 'static/mypicture.jpg'
file.save(save_to)

클라이언트 쪽 보내기 코드

function posting() {
	let title = $('#title').val()
	let content = $('#content').val()

	let file = $('#file')[0].files[0]
	let form_data = new FormData()

	form_data.append("file_give", file)
	form_data.append("title_give", title)
	form_data.append("content_give", content)

	$.ajax({
		type: "POST",
		url: "/diary",
		data: form_data,
		cache: false,
		contentType: false,
		processData: false,
		success: function (response) {
			alert(response["msg"])
			window.location.reload()
		}
	});
}

파일 이름 설정 (서버)

f-string

myname = '정예원'
text = f'내 이름은 {myname}'

datetime

from datetime import datetime
now = datetime.now() # 현재 날짜 시간
date_time = now.strftime("%Y-%m-%d-%H-%M-%S") # 원하는 형태로 변환하기

파일 이름 변경해서 저장하기

  • 확장자 추출
extension = file.filename.split('.')[-1]
  • 새로운 이름 짓고 저장하기
now = datetime.now() # 현재 날짜 시간
mytime = now.strftime("%Y-%m-%d-%H-%M-%S") # 원하는 형태로 변환하기
filename = f'file-{mytime}'

save_to = f'static/{filename}.{extension}'
file.save(save_to)
  • 변경된 파일 이름으로 DB에 저장하기
doc = {
	'title': title_receive,
	'content': content_receive,
	'file': f'{filename}.{extension}',
}
db.diary.insert_one(doc)

카드 목록 출력 (클라이언트)

function listing() {
    $.ajax({
        type: "GET",
        url: "/listing",
        data: {},
        success: function (response) {
            if (response["result"] == "success") {
                let articles = response['articles']
                for (let i = 0; i < articles.length; i++) {
                    let title = articles[i]['title']
                    let content = articles[i]['content']
                    let file = articles[i]['file']

                    let temp_html = `<div class="card">
                                        <img src="../static/${file}" class="card-img-top">
                                        <div class="card-body">
                                            <h5 class="card-title">${title}</h5>
                                            <p class="card-text">${content}</p>
                                        </div>
                                    </div>`

                    $('#cards-box').append(temp_html)
                }
            }
        }
    });
}

웹서비스 동작 원리

  • 클라이언트가 요청하면 서버가 요청을 받아서 무언가를 돌려준다

API란?

  • 서버가 요청을 받기 위해 뚫어놓은 창구
  • POST (주로 데이터 수정 시), GET (주로 데이터 가져올 때) 등 여러 타입의 요청이 존재

jQuery란?

  • Javascript의 라이브러리 중 하나로 HTML 조작을 쉽게 한다
  • 사용하기 위해서 import가 필요하다

Ajax란?

  • 서버 통신을 위해 쓰인다
$.ajax({
	type: "GET",
	url: "요청할 url",
	data: {},
	success: function(response) {
		// 서버가 준 데이터가 response에 담긴다
	}
})

Flask란?

  • 서버를 만드는 프레임워크
  • 아래 코드를 run 하면 localhost 5000으로 접속 가능
from flask import Flask, render_template, jsonfiy, request
app = Flask(__name__)

@app.route('/')
def home():
	return render_template('index.html')

if __name__ == '__main__':
	app.run('0.0.0.0', port=5000, debug=True)

 

프로젝트 세팅

  • 프론트엔드 → Bootstrap, 백엔드 → Python으로 된 Flask 라이브러리 이용
  • templates, static 폴더와 app.py 생성
  • Windows : file → settings → Python Interpreter → + 버튼
  • Mac : pycharm → preferences → Python Interpreter → + 버튼
  • requests, bs4, flask, pymongo 패키지 설치

PyCharm 라이센스 등록하기

PyCharm 라이센스 코드 발급

  • 스파르타코딩클럽을 통해 라이센스 코드 발급
  • 4개월 간 PyCharm Professional 버전을 무료로 사용 가능

JetBrains 로그인

  • JetBrains 접속해서 로그인 완료 : https://account.jetbrains.com/licenses
  • Purchase Product license(s) 클릭 → PyCharm 오른쪽 끝의 Buy new license 클릭
  • Proceed as new customer 클릭 → Have a discount code? 클릭
  • 파이참 라이센스 코드 입력 완료 후 Place Order 클릭
  • PyCharm Pro 실행해서 email, password 입력 후 Activate

 

필수 프로그램 설치

PyCharm Professional 설치

JetBrains 회원가입

MongoDB 설치

  • 다운로드 링크 : https://www.mongodb.com/try/download/community
  • MongoDB Community Server 탭 → Version 4.4.1 / Platform : Windows / Package : MSI
  • 설치 진행 시 Custom 클릭해서 C:\data\db\ 선택
  • Install MongoDB Compass 선택 해제 후 설치
  • 환경 변수 - 시스템 변수 - Path 편집해서 C:\data\db\bin 추가
  • cmd 창에서 연결 확인
mongod --install --serviceName MongoDB --serviceDisplayName MongoDB --dbpath C:\data\db --logpath C:\data\db\log\mongoservice.log --logappend
mongo

Robo 3T 설치

Filezilla 설치

기타

  • AWS 가입하기
  • Python 설치
  • Git bash 설치

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});

+ Recent posts