안경잡이개발자

728x90
반응형

  가장 먼저 material-ui 라이브러리를 설치합니다.


  yarn add @material-ui/core

  yarn add @material-ui/icons


  이후에 내비게이션 바의 틀을 만들 수 있습니다.


▶ ./src/components/Appshell.js


import React from 'react';
import { withStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Drawer from '@material-ui/core/Drawer';
import MenuItem from '@material-ui/core/MenuItem';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';

const styles = {
root: {
flexGrow: 1,
},
menuButton: {
marginRight: 'auto'
},
};

class AppShell extends React.Component {
constructor(props) {
super(props);
this.state = {
toggle: false
};
}
handleDrawerToggle = () => this.setState({toggle: !this.state.toggle})
render() {
const { classes } = this.props;
return (
<div className={classes.root}>
<AppBar position="static">
<IconButton className={classes.menuButton} color="inherit" onClick={this.handleDrawerToggle}>
<MenuIcon/>
</IconButton>
</AppBar>
<Drawer open={this.state.toggle}>
<MenuItem onClick={this.handleDrawerToggle}>Home</MenuItem>
</Drawer>
</div>
);
}
}

export default withStyles(styles)(AppShell);


▶ ./src/components/App.js


import React from 'react';
import AppShell from './AppShell';

class App extends React.Component {
render() {
return (
<AppShell/>
);
}
}

export default App;


※ 실행 결과 ※


1. 내비게이션 바(Close)



2. 내비게이션 바(Open)



728x90
반응형

728x90
반응형

1.  VSC(Visual Studio Code) 개발 환경을 열어서 특정한 폴더(Folder)를 열어 줍니다.


  (이 때 가능하면, 관리자 권한으로 개발환경을 열 수 있도록 합니다.)



2. yarn init 명령어를 통해 패키지(Package) JSON 파일을 생성합니다.



3. yarn add 명령어를 통해 리액트 라이브러리를 설치합니다.


  리액트 개발을 위하여 react와 react-dom이 모두 필요합니다.



4. yarn add 명령어를 통해 webpack을 설치합니다.


  webpack을 이용해 추후에 앱을 배포할 수 있어요. 또한 webpack-dev-server를 이용해 실시간 로딩으로 빠르게 개발할 수 있습니다.



5. yarn add 명령어를 통해 바벨(Babel)을 설치합니다.


  바벨 또한 개발 환경에서 필요하기 때문에 --dev 옵션으로 설치합니다.



6. yarn add 명령어를 통해 웹팩(Webpack) 클라이언트 모듈을 설치합니다.


  yarn add --dev webpack-cli


  (--dev 혹은 -D라고 옵션을 붙여서 개발 종속성으로 설치합니다.)



7. 소스코드를 작성하기 위해 다음과 같이 프로젝트를 구성합니다.



▶ index.html


<!DOCTYPE html>
<html>
<head>
<title>Word Cloud Project</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#E2E2E2">
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="main.js"></script>
</body>
</html>


▶ App.js


import React from 'react';

class App extends React.Component {
render() {
return (
<div>
<h3>Hello World</h3>
</div>
);
}
}

export default App;


▶ main.js


import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';

ReactDOM.render(<App/>, document.getElementById('app'));


▶ webpack.config.js


'use strict'
const path = require('path');

module.exports = {
entry: {
main: ['./src/main.js']
},
output: {
path: path.resolve(__dirname, './build'),
filename: '[name].js'
},
module: {
rules: [{
test: /\.js$/,
include: path.resolve(__dirname, './src'),
loaders: 'babel-loader'
}]
},
plugins: [],
devServer: {
contentBase: './public',
host: 'localhost',
port: 8080
}
}


▶ .babelrc


{
"presets": ["react-app"]
}


▶ package.json


{
"name": "ReactProject",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"react": "^16.8.6",
"react-dom": "^16.8.6"
},
"scripts": {
"start": "NODE_ENV=development webpack-dev-server",
"build": "NODE_ENV=production webpack -p"
},
"devDependencies": {
"babel-core": "^6.26.3",
"babel-loader": "^8.0.5",
"babel-preset-react-app": "^7.0.2",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.0",
"webpack-dev-server": "^3.3.1"
}
}


윈도우의 경우에는 다음과 같은 방식으로 start 및 build에 대한 옵션을 설정합니다.


"scripts": {
"start": "set NODE_ENV=development&&webpack-dev-server",
"build": "set NODE_ENV=production&&webpack -p"
},


8. yarn start 명령어를 이용해 프로젝트를 동작시킵니다.



9. 실행 결과를 통해 구성된 프로젝트 확인



10. 구성된 프로젝트를 깃 허브(Git Hub)에 올리기


▶ .gitignore 파일 생성 및 소스코드 작성


node_modules
build


11. 깃 허브(Git Hub)에 리포지터리 생성



12. 생성된 리포지터리의 주소 복사



13. 터미널(Terminal)에서 Git 명령어로 소스코드 업로드


git init

git add .

git commit -m "Initialize React Project"

git remote add origin https://github.com/ndb796/ReactWordCloudWebApp-React.git

git push --set-upstream origin master


728x90
반응형

728x90
반응형

  이번 시간에는 안정적인 UI/UX를 위해 AppBar을 적용하는 시간을 가져보도록 하겠습니다. AppBar는 검색 바, 내비게이션 바 등의 목적으로 사용됩니다. 따라서 가장 먼저 client 폴더로 이동해서 다음과 같은 명령어를 입력해서 icons 라이브러리를 받으면 됩니다.


  ▶ Material UI icons 다운로드 명령어: npm i @material-ui/icons


  이제 코딩을 진행해보도록 하겠습니다. 우리가 적용할 AppBar는 React.js의 Material UI 공식 사이트에서 제공하고 있는 기본 예제입니다. 말 그대로 기본 예제라는 점에서 한글 웹 폰트(Web Font) 등은 적용되어 있지 않습니다. 따라서 우리가 직접 웹 폰트 등을 적용해야 합니다.


  ▶ Material UI AppBar 예제: https://material-ui.com/demos/app-bar/


  우리는 아래에 보이는 App Bar with search field 예제를 넣어보도록 하겠습니다.



※ AppBar 적용하기 ※


▶ App.js


import React, { Component } from 'react';
import Customer from './components/Customer'
import './App.css';
import Paper from '@material-ui/core/Paper';
import Table from '@material-ui/core/Table';
import TableHead from '@material-ui/core/TableHead';
import TableBody from '@material-ui/core/TableBody';
import TableRow from '@material-ui/core/TableRow';
import TableCell from '@material-ui/core/TableCell';
import CircularProgress from '@material-ui/core/CircularProgress';
import { withStyles } from '@material-ui/core/styles';
import CustomerAdd from './components/CustomerAdd';
import MenuIcon from '@material-ui/icons/Menu';
import SearchIcon from '@material-ui/icons/Search';
import InputBase from '@material-ui/core/InputBase';
import { fade } from '@material-ui/core/styles/colorManipulator';
import Typography from '@material-ui/core/Typography';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import IconButton from '@material-ui/core/IconButton';

const styles = theme => ({
root: {
width: "100%",
minWidth: 1080
},
menu: {
marginTop: 15,
marginBottom: 15,
display: 'flex',
justifyContent: 'center'
},
paper: {
marginLeft: 18,
marginRight: 18
},
progress: {
margin: theme.spacing.unit * 2
},
grow: {
flexGrow: 1,
},
tableHead: {
fontSize: '1.0rem'
},
menuButton: {
marginLeft: -12,
marginRight: 20,
},
title: {
display: 'none',
[theme.breakpoints.up('sm')]: {
display: 'block',
},
},
search: {
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: fade(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: fade(theme.palette.common.white, 0.25),
},
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing.unit,
width: 'auto',
},
},
searchIcon: {
width: theme.spacing.unit * 9,
height: '100%',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
inputRoot: {
color: 'inherit',
width: '100%',
},
inputInput: {
paddingTop: theme.spacing.unit,
paddingRight: theme.spacing.unit,
paddingBottom: theme.spacing.unit,
paddingLeft: theme.spacing.unit * 10,
transition: theme.transitions.create('width'),
width: '100%',
[theme.breakpoints.up('sm')]: {
width: 120,
'&:focus': {
width: 200,
},
},
}
});

class App extends Component {

constructor(props) {
super(props);
this.state = {
customers: '',
completed: 0
}
this.stateRefresh = this.stateRefresh.bind(this);
}

stateRefresh() {
this.setState({
customers: '',
completed: 0
});
this.callApi()
.then(res => this.setState({customers: res}))
.catch(err => console.log(err));
}

componentDidMount() {
this.timer = setInterval(this.progress, 20);
this.callApi()
.then(res => this.setState({customers: res}))
.catch(err => console.log(err));
}

componentWillUnmount() {
clearInterval(this.timer);
}

callApi = async () => {
const response = await fetch('/api/customers');
const body = await response.json();
return body;
}

progress = () => {
const { completed } = this.state;
this.setState({ completed: completed >= 100 ? 0 : completed + 1 });
};

render() {
const { classes } = this.props;
const cellList = ["번호", "프로필 이미지", "이름", "생년월일", "성별", "직업", "설정"]
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<IconButton className={classes.menuButton} color="inherit" aria-label="Open drawer">
<MenuIcon />
</IconButton>
<Typography className={classes.title} variant="h6" color="inherit" noWrap>
고객 관리 시스템
</Typography>
<div className={classes.grow} />
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<InputBase
placeholder="검색하기"
classes={{
root: classes.inputRoot,
input: classes.inputInput,
}}
/>
</div>
</Toolbar>
</AppBar>
<div className={classes.menu}>
<CustomerAdd stateRefresh={this.stateRefresh} />
</div>
<Paper className={classes.paper}>
<Table>
<TableHead>
<TableRow>
{cellList.map(c => {
return <TableCell className={classes.tableHead}>{c}</TableCell>
})}
</TableRow>
</TableHead>
<TableBody>
{this.state.customers ?
this.state.customers.map(c => {
return <Customer stateRefresh={this.stateRefresh} key={c.id} id={c.id} image={c.image} name={c.name} birthday={c.birthday} gender={c.gender} job={c.job} />
}) :
<TableRow>
<TableCell colSpan="6" align="center">
<CircularProgress className={classes.progress} variant="determinate" value={this.state.completed} />
</TableCell>
</TableRow>
}
</TableBody>
</Table>
</Paper>
</div>
);
}
}

export default withStyles(styles)(App);


▶ index.css


  index.css에서는 Note Sans KR 글씨체를 웹 폰트 형태로 불러오게 됩니다.


@import url(http://fonts.googleapis.com/earlyaccess/notosanskr.css);

body {
margin: 0;
padding: 0;
font-family: "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}


▶ index.js


  이제 index.js에서는 Material UI의 테마 폰트로 "Note Sans KR"을 적용주면 됩니다. 이렇게 해주지 않으면 곳곳에 사용된 Material UI에 전체적인 폰트 적용이 안 될 수도 있습니다.


import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';

const theme = createMuiTheme({
typography: {
fontFamily: '"Noto Sans KR", serif',
},
});

ReactDOM.render(<MuiThemeProvider theme={theme}><App /></MuiThemeProvider>, document.getElementById('root'));

serviceWorker.unregister();


※ 실행 결과 ※


  결과적으로 다음과 같이 전체적인 UI가 Material한 형태로 업데이트 되었습니다.



728x90
반응형

728x90
반응형

  이번 시간에는 모달(Modal) 기능을 이용해서 고객 추가(Customer Add) 기능을 모달 창에서 띄우는 방법에 대해서 알아보도록 하겠습니다. 따라서 Material UI의 Dialog를 import하여 디자인 요소를 활용해야 합니다. 우리는 모달 중에서 다이얼로그(Dialog)를 활용할 것이며 이를 위해서 Dialog 컴포넌트를 사용해야 합니다.


▶ CustomerAdd.js


import React from 'react'

import { post } from 'axios';

import Dialog from '@material-ui/core/Dialog';

import DialogActions from '@material-ui/core/DialogActions';

import DialogContent from '@material-ui/core/DialogContent';

import DialogTitle from '@material-ui/core/DialogTitle';

import TextField from '@material-ui/core/TextField';

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

import { withStyles } from '@material-ui/core/styles';


const styles = theme => ({

hidden: {

display: 'none'

}

});


class CustomerAdd extends React.Component {


constructor(props) {

super(props);

this.state = {

file: null,

userName: '',

birthday: '',

gender: '',

job: '',

fileName: '',

open: false

}

this.handleFormSubmit = this.handleFormSubmit.bind(this)

this.handleFileChange = this.handleFileChange.bind(this)

this.handleValueChange = this.handleValueChange.bind(this)

this.addCustomer = this.addCustomer.bind(this)

this.handleClickOpen = this.handleClickOpen.bind(this)

this.handleClose = this.handleClose.bind(this);

}


handleFormSubmit(e) {

e.preventDefault()

this.addCustomer()

.then((response) => {

console.log(response.data);

this.props.stateRefresh();

})

this.setState({

file: null,

userName: '',

birthday: '',

gender: '',

job: '',

fileName: '',

open: false

})

}


handleFileChange(e) {

this.setState({

file: e.target.files[0],

fileName: e.target.value

});

}

handleValueChange(e) {

let nextState = {};

nextState[e.target.name] = e.target.value;

this.setState(nextState);

}


addCustomer(){

const url = '/api/customers';

const formData = new FormData();

formData.append('image', this.state.file)

formData.append('name', this.state.userName)

formData.append('birthday', this.state.birthday)

formData.append('gender', this.state.gender)

formData.append('job', this.state.job)

const config = {

headers: {

'content-type': 'multipart/form-data'

}

}

return post(url, formData, config)

}


handleClickOpen() {

this.setState({

open: true

});

}


handleClose() {

this.setState({

file: null,

userName: '',

birthday: '',

gender: '',

job: '',

fileName: '',

open: false

})

}


render() {

const { classes } = this.props;

return (

<div>

<Button variant="contained" color="primary" onClick={this.handleClickOpen}>

고객 추가하기

</Button>

<Dialog open={this.state.open} onClose={this.handleClose}>

<DialogTitle>고객 추가</DialogTitle>

<DialogContent>

<input className={classes.hidden} accept="image/*" id="raised-button-file" type="file" file={this.state.file} value={this.state.fileName} onChange={this.handleFileChange} />

<label htmlFor="raised-button-file">

<Button variant="contained" color="primary" component="span" name="file">

{this.state.fileName === ''? "프로필 이미지 선택" : this.state.fileName}

</Button>

</label><br/>

<TextField label="이름" type="text" name="userName" value={this.state.userName} onChange={this.handleValueChange} /><br/>

<TextField label="생년월일" type="text" name="birthday" value={this.state.birthday} onChange={this.handleValueChange} /><br/>

<TextField label="성별" type="text" name="gender" value={this.state.gender} onChange={this.handleValueChange} /><br/>

<TextField label="직업" type="text" name="job" value={this.state.job} onChange={this.handleValueChange} /><br/>

</DialogContent>

<DialogActions>

<Button variant="contained" color="primary" onClick={this.handleFormSubmit}>추가</Button>

<Button variant="outlined" color="primary" onClick={this.handleClose}>닫기</Button>

</DialogActions>

</Dialog>

</div>

)

}

}


export default withStyles(styles)(CustomerAdd)


▶ CustomerDelete.js


import React from 'react';

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

import Dialog from '@material-ui/core/Dialog';

import DialogTitle from '@material-ui/core/DialogTitle';

import DialogContent from '@material-ui/core/DialogContent';

import DialogActions from '@material-ui/core/DialogActions';

import Typography from '@material-ui/core/Typography';


class CustomerDelete extends React.Component {


constructor(props) {

super(props);

this.state = {

open: false

}

this.handleClickOpen = this.handleClickOpen.bind(this)

this.handleClose = this.handleClose.bind(this);

}


handleClickOpen() {

this.setState({

open: true

});

}

handleClose() {

this.setState({

open: false

})

}


deleteCustomer(id){

const url = '/api/customers/' + id;

fetch(url, {

method: 'DELETE'

});

this.props.stateRefresh();

}


render() {

return (

<div>

<Button variant="contained" color="secondary" onClick={this.handleClickOpen}>

삭제

</Button>

<Dialog onClose={this.handleClose} open={this.state.open}>

<DialogTitle onClose={this.handleClose}>

삭제 경고

</DialogTitle>

<DialogContent>

<Typography gutterBottom>

선택한 고객 정보가 삭제됩니다.

</Typography>

</DialogContent>

<DialogActions>

<Button variant="contained" color="primary" onClick={(e) => {this.deleteCustomer(this.props.id)}}>삭제</Button>

<Button variant="outlined" color="primary" onClick={this.handleClose}>닫기</Button>

</DialogActions>

</Dialog>

</div>

)

}

}


export default CustomerDelete;


※ 실행결과 ※



  고객을 추가합시다.



  이어서 삭제도 해봅시다.



  삭제도 잘 되네요.


728x90
반응형

728x90
반응형

  이번 시간에는 고객(Customer) 정보를 삭제하는 기능을 구현하는 시간을 가져보도록 하겠습니다. 특정한 데이터의 삭제 기능을 구현하는 방법은 굉장히 다양합니다. 몇 가지 예제를 살펴보자면 ① 삭제를 수행했을 때 실제 테이블에서 데이터를 삭제하는 방식이 있으며 ② 삭제를 수행했을 때 삭제 체크만 한 뒤에 실제 데이터베이스에는 남겨 놓는 방식이 있습니다.


  우리는 두 번째 방식으로 구현하겠습니다. 따라서 Customer 테이블의 정보부터 수정하도록 하겠습니다.


USE management;

ALTER TABLE CUSTOMER ADD createdDate DATETIME;

ALTER TABLE CUSTOMER ADD isDeleted INT;


  또한 기본적으로 현재 들어있던 데이터에는 새로운 속성(Property)에 대한 데이터가 없으므로 이를 채워넣어줄 수 있도록 합니다.


USE management;

UPDATE CUSTOMER SET createdDate = now();

UPDATE CUSTOMER SET isDeleted = 0;


  위와 같이 MySQL 테이블 설정 명령어를 수행한 이후에 DESC 명령어로 테이블(Table) 정보를 확인해 보시면 다음과 같습니다.



  따라서 모든 데이터는 방금 생성이 된 것으로 처리 되었고, 현재 삭제 되지 않은 상태로 남아있게 되었습니다. 이제 새롭게 CustomerDelete.js를 생성해주도록 합니다.



▶ CustomerDelete.js


import React from 'react';

class CustomerDelete extends React.Component {

deleteCustomer(id){
const url = '/api/customers/' + id;
fetch(url, {
method: 'DELETE'
});
this.props.stateRefresh();
}

render() {
return (
<button onClick={(e) => {this.deleteCustomer(this.props.id)}}>삭제</button>
)
}
}

export default CustomerDelete;


▶ Customer.js


  이제 위에서 만든 Customer Delete 뷰(View)를 한 명의 고객 정보를 출력할 때 함께 보여줄 수 있도록 처리하면 됩니다.


import React from 'react';
import TableRow from '@material-ui/core/TableRow';
import TableCell from '@material-ui/core/TableCell';
import CustomerDelete from './CustomerDelete'

class Customer extends React.Component {
render() {
return (
<TableRow>
<TableCell>{this.props.id}</TableCell>
<TableCell><img src={this.props.image} alt="profile"/></TableCell>
<TableCell>{this.props.name}</TableCell>
<TableCell>{this.props.birthday}</TableCell>
<TableCell>{this.props.gender}</TableCell>
<TableCell>{this.props.job}</TableCell>
<TableCell><CustomerDelete stateRefresh={this.props.stateRefresh} id={this.props.id}/></TableCell>
</TableRow>
)
}
}

export default Customer;


▶ App.js


  이후에 다음과 같이 App.js에서 각 고객 컴포넌트에게 stateRefresh 함수를 넘겨 줄 수 있도록 합니다.


<div>
<Paper className={classes.root}>
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell>번호</TableCell>
<TableCell>이미지</TableCell>
<TableCell>이름</TableCell>
<TableCell>생년월일</TableCell>
<TableCell>성별</TableCell>
<TableCell>직업</TableCell>
<TableCell>설정</TableCell>
</TableRow>
</TableHead>
<TableBody>
{this.state.customers ?
this.state.customers.map(c => {
return <Customer stateRefresh={this.stateRefresh} key={c.id} id={c.id} image={c.image} name={c.name} birthday={c.birthday} gender={c.gender} job={c.job} />
}) :
<TableRow>
<TableCell colSpan="6" align="center">
<CircularProgress className={classes.progress} variant="determinate" value={this.state.completed} />
</TableCell>
</TableRow>
}
</TableBody>
</Table>
</Paper>
<CustomerAdd stateRefresh={this.stateRefresh} />
</div>


▶ server.js


  이제 실제로 Express 서버에 고객 데이터 삭제 모듈을 만들어주면 됩니다. 이 때 기존에 존재하던 고객 정보 삽입 모듈과 고객 목록 불러오기 모듈도 조금씩 바꾸어주시면 됩니다.


const fs = require('fs');
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const port = process.env.PORT || 5000;
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const data = fs.readFileSync('./database.json');
const conf = JSON.parse(data);
const mysql = require('mysql');

const multer = require('multer');
const upload = multer({dest: './upload'})

const connection = mysql.createConnection({
host: conf.host,
user: conf.user,
password: conf.password,
port: conf.port,
database: conf.database
});
connection.connect();

app.get('/api/customers', (req, res) => {
connection.query(
'SELECT * FROM CUSTOMER WHERE isDeleted = 0',
(err, rows, fields) => {
res.send(rows);
}
)
});

app.use('/image', express.static('./upload'));

app.post('/api/customers', upload.single('image'), (req, res) => {
let sql = 'INSERT INTO CUSTOMER VALUES (null, ?, ?, ?, ?, ?, now(), 0)';
let image = '/image/' + req.file.filename;
let name = req.body.name;
let birthday = req.body.birthday;
let gender = req.body.gender;
let job = req.body.job;
let params = [image, name, birthday, gender, job];
connection.query(sql, params,
(err, rows, fields) => {
res.send(rows);
}
)
});

app.delete('/api/customers/:id', (req, res) => {
let sql = 'UPDATE CUSTOMER SET isDeleted = 1 WHERE id = ?';
let params = [req.params.id];
connection.query(sql, params,
(err, rows, fields) => {
res.send(rows);
}
)
});

app.listen(port, () => console.log(`Listening on port ${port}`));


  실행 결과는 다음과 같습니다. 다음과 같이 가장 오른쪽에 [삭제] 버튼이 추가된 것을 확인할 수 있습니다.



  그래서 결과적으로 다음과 같이 삭제 요청(Request)이 전송되는 것을 확인할 수 있습니다. REST API의 성격에 맞게 DELETE 메소드(Method)로 삭제 요청이 보내졌으며 성공적으로 삭제가 완료된 것입니다.



  결과적으로 다음과 같이 고객 목록이 갱신된 것을 확인할 수 있습니다.




728x90
반응형

728x90
반응형

  지난 시간까지 작성되었던 소스코드 상으로는 새로운 고객(Customer) 정보가 추가되면 페이지 새로고침(Refresh)를 통해서 새롭게 등록된 고객에 대한 정보를 확인할 수 있습니다. 하지만 기본적으로 리액트(React)는 SPA(Single Page Application)의 구조로 동작합니다. 그러므로 전체 페이지를 새로고침하는 것은 비효율적이며 고객 추가(Customer Add) 컴포넌트에서 부모 컴포넌트의 상태(State)를 변경하는 식으로 필요한 부분만 새로고침 되도록 설정하면 됩니다.


  이 때는 기본적으로 부모 컴포넌트에서 자식 컴포넌트로 함수를 props로 건내주는 방식으로 구현합니다.


전체 고객 목록 다시 불러오기 ※


  고객 목록 추가 이후에 새로고침 하는 과정을 구현하는 가장 대표적인 방법은 전체 고객 목록을 다시 불러옴으로써 State를 갱신하는 방식입니다. App.js에 있던 기존의 state 초기화 변수를 제거해주신 뒤에, 윗 부분을 다음과 같은 소스코드로 채우시면 됩니다.


class App extends Component {

constructor(props) {

super(props);

this.state = {

customers: '',

completed: 0

}

this.stateRefresh = this.stateRefresh.bind(this);

}


stateRefresh() {

this.setState({

customers: '',

completed: 0

});

this.callApi()

.then(res => this.setState({customers: res}))

.catch(err => console.log(err));

}


  이후에 Customer Add 컴포넌트로 stateRefresh() 함수를 보내주시면 돼요. 그러면 Customer Add 컴포넌트가 해당 함수를 호출해서 부모 컴포넌트인 App 컴포넌트의 State를 갱신하게 됩니다.


<CustomerAdd stateRefresh={this.stateRefresh} />


  마지막으로 CustomerAdd.js로 가서 window.location.reload() 함수를 제거하고 다음의 구문을 넣습니다.


this.props.stateRefresh();


  다만 고객 목록 데이터를 불러오는 과정은 비동기적으로 수행된다는 점에서 항상 고객 데이터를 추가(Add)한 이후에 고객 목록 데이터를 불러온다는 것을 보장하지 못합니다. 따라서 다음과 같이 고객 추가 이후에 서버로부터 응답을 받고 나서 비로소 고객 목록을 다시 불러오도록 설정해야 합니다.


handleFormSubmit(e) {

e.preventDefault()

this.addCustomer()

.then((response) => {

console.log(response.data);

this.props.stateRefresh();

})

this.setState({

file: null,

userName: '',

birthday: '',

gender: '',

job: '',

fileName: ''

})

}


  이제 테스트를 진행 해보시면 다음과 같이 정상적으로 동작합니다.



  추가 이후에는 전체 페이지 새로고침 없이 다음과 같이 고객 목록이 출력됩니다. 또한 고객 추가 창은 비워지게 됩니다.



※ 고객 정보 갱신 방법에 대해서 ※


  위에서 구현한 방식은 고객 데이터가 많을 때에는 매 번 새로고침을 거치는 과정에서 리소스를 비효율적으로 사용하게 됩니다. 그래서 실제로 상용 서비스에 적용할 때는 최근 10개의 고객 목록만 가져오도록 하여, 이후의 고객 정보에 대해서는 스크롤을 통해 새롭게 불러오는 식으로 구현할 수 있습니다.


  물론 이렇게 만든 고객 관리 시스템을 자기 혼자 사용하는 경우에는 매 번 서버와 통신하지 않아도 됩니다. 고객을 추가한 이후에 자신의 화면에서 바로 추가된 고객 정보만을 추가적으로 보여주면 됩니다.


  하지만 일반적인 고객 관리 시스템은 여러 명의 관리자가 동시에 작업할 수 있다는 점에서 현재의 방법이 더 효율적일 수 있습니다.

728x90
반응형

728x90
반응형

  기본적으로 리액트(React) 요소에 대한 이벤트 핸들링은 기본적인 자바스크립트에서의 방식과 매우 흡사합니다. 다만 리액트 이벤트는 카멜 케이스(Camel Case) 네이밍 규칙을 사용한다는 특징이 있으며 JSX 문법을 사용해 함수를 호출할 수 있습니다.


class EventHandling extends React.Component {

  constructor(props) {

    super(props);

  }

  

  handleClick() {

    console.log('이벤트 처리');

  }

  

  render() {

    return (

      <button onClick={this.handleClick}>버튼</button>

    );

  }

  

}


ReactDOM.render(

  <EventHandling />, 

  document.getElementById('root')

);



  이후에 가장 대표적인 예제인 버튼 토글 예제에 대해서 알아보도록 하겠습니다.


class EventHandling extends React.Component {

  constructor(props) {

    super(props);

    this.state = {

      isToggleOn: true

    }

    this.handleClick = this.handleClick.bind(this);

  }

  

  handleClick() {

    this.setState(state => ({

      isToggleOn: !state.isToggleOn

    }))

  }

  

  render() {

    return (

      <button onClick={this.handleClick}>

        {this.state.isToggleOn ? 'ON' : 'OFF'}

      </button>

    );

  }

  

}


ReactDOM.render(

  <EventHandling />, 

  document.getElementById('root')

);



  자바스크립트(JavaScript)에서는 바인딩(Binding)이 기본 설정으로 제공되지 않습니다. 그래서 만약 바인딩 처리를 해주는 것을 잊으면 소스코드가 정상적으로 동작하지 않습니다

728x90
반응형

'리액트(React)' 카테고리의 다른 글

React의 LifeCycle과 API 호출  (3) 2019.01.04
React의 State  (0) 2018.12.25
React의 Component와 Props  (0) 2018.12.25
React에서 JSX란?  (0) 2018.12.25
코드펜(Codepen)을 이용한 React 개발환경 구축하기  (0) 2018.12.25

728x90
반응형

  이번 시간에는 React의 LifeCycle의 개념에 대해서 이해한 뒤에 API 호출에 대해서 알아보도록 하겠습니다. React는 말 그대로 프론트 엔드(Front-End) 라이브러리입니다. 따라서 서버 개발자와 API를 이용해서 소통할 수 있어야 합니다. 하지만 React를 처음 공부하시는 분은 정확히 언제 API를 호출해서 데이터를 가져와 화면에 보여주어야 할 지 감이 안 잡힐 수 있어요. 이번 시간에는 그러한 내용에 대해서 다룰 거예요.


초기 구성 ※


  React 컴포넌트 객체가 DOM에 실제로 삽입되기 전까지의 과정은 다음과 같습니다. 이러한 과정을 마운팅(Mounting)이라고 부릅니다.


  1) contructor()

  2) componentWillMount()

  3) render()

  4) componentDidMount()


  기본적으로 컴포넌트가 모두 구성된 직후인 componentDidMount() 함수에서 API 호출을 수행하면 효과적이라는 것만 기억하시면 됩니다. SNS에 처음 로그인 했을 때 최근 타임라인 데이터를 API로부터 호출해서 화면에 보여주는 것이 그 예시입니다.


※ 데이터 변경 ※


  기본적으로 화면에 특정한 객체를 렌더링하기 위해서는 props 혹은 state를 사용해야 합니다. props와 state는 이러한 과정이 서로 다르지만 대략적으로 다음과 같은 순서로 수행된다는 것을 기억하시면 됩니다.


  1) shouldComponentUpdate()

  2) componentWillUpdate()

  3) render()

  4) componentDidUpdate()


  기본적으로 컴포넌트의 데이터와 화면(View)에 출력된 내용이 다를 때 shouldComponentUpdate() 함수가 동작합니다. 이 함수는 기본적으로 true 값을 반환합니다. 또한 화면에 출력되는 화면 구성을 변경하고자 할 때는 componentDidUpdate()를 많이 사용합니다. 실제로 데이터 변경과 관련한 함수들을 우리가 처리하게 되는 일은 많지 않습니다. 


※ 컴포넌트 해제 ※


  컴포넌트가 마운팅 해제 될 때 수행되는 함수는 componentWillUnmount() 함수입니다. 우리는 이 함수를 이용해서 컴포넌트의 동작을 위해 사용되었던 메소드들의 리소스를 제거해주게 됩니다. 애플리케이션의 성능 향상을 위해서 사용되는 경우가 많은 함수입니다.


※ API 호출 예제 ※


  이제 실제로 위와 같은 라이프 사이클의 개념을 활용하여 API를 호출하는 예제를 다루어 보도록 하겠습니다. API 호출을 연습하고자 할 때는 가짜(Fake) 온라인 REST API 사이트를 이용하시면 됩니다. 


  ▶ JSON Placeholder 사이트: https://jsonplaceholder.typicode.com/

  


  사이트에 접속해서 [Try it]을 클릭해 보시면 그 결과 응답(Response) JSON 데이터를 확인할 수 있습니다.



  이제 우리는 결과 JSON 데이터에서 title 정보만 받아서 화면에 출력해보도록 하겠습니다.


※ JavaScript 소스코드 ※


class ApiExample extends React.Component {

  constructor(props) {

    super(props);

    this.state = {

      data: ''

    }

  }

  

  callApi = () => {

    fetch("https://jsonplaceholder.typicode.com/todos/1")

      .then(res => res.json())

      .then(json => {

        this.setState({

          data: json.title

        })

      })

  }

  

  componentDidMount() {

    this.callApi();

  }

  

  render() {

    return (

      <h3>{this.state.data ? this.state.data : '데이터 불러오는 중'}</h3>

    )

  }

  

}


ReactDOM.render(

  <ApiExample />, 

  document.getElementById('root')

);



728x90
반응형

'리액트(React)' 카테고리의 다른 글

React 이벤트 처리(Event Handling)  (2) 2019.01.04
React의 State  (0) 2018.12.25
React의 Component와 Props  (0) 2018.12.25
React에서 JSX란?  (0) 2018.12.25
코드펜(Codepen)을 이용한 React 개발환경 구축하기  (0) 2018.12.25

728x90
반응형

  이번 시간에는 AWS EC2에 AWS RDS를 연동하는 방법에 대해서 알아보도록 하겠습니다. 기존에 운영하는 AWS EC2 서비스가 있을 때 이에 안전한 방법으로 데이터베이스를 연결해야 하는 경우가 존재할 수 있습니다. 따라서 그럴 때를 대비하여 한 번 AWS EC2에 AWS RDS를 연동해보도록 하겠습니다.


※ AWS EC2 인스턴스의 VPC 정보 확인 ※


  일단 자신이 현재 가지고 있는 AWS EC2 정보를 먼저 파악하는 것이 중요합니다. 우리는 이 EC2에 AWS RDS 서비스를 연결할 것이기 때문에 EC2와 동일한 VPC에 AWS RDS를 설치해야 해요. 그러므로 VPC ID를 확인하는 것이 첫 번째입니다. 이후에는 [보안 그룹]을 확인합니다. 현재 제 AWS EC2 인스턴스의 보안 그룹은 launch-wizard-2입니다.



※ DB 보안 그룹 설정하기 ※


  따라서 [보안 그룹] 탭으로 이동해보도록 합시다. 그러면 아까 확인했던 launch-wizard-2라는 이름의 보안 그룹이 눈에 보입니다. 이 보안 그룹의 ID 값을 기록하세요. 우리는 이 보안 그룹에서만 DB에 접근할 수 있도록 설정할 것입니다. 이후에 [보안 그룹 생성] 버튼을 눌러보도록 합시다. 



  이제 DB Security Group이라는 이름으로 우리가 만들 DB가 들어갈 보안 그룹을 만들어 주시면 됩니다. 이 때 [인바운드] 보안 그룹 규칙으로 방금 우리가 확인했던 EC2의 보안 그룹에서만 접근할 수 있도록 설정해주시면 됩니다. 이 때 저는 MySQL DB를 사용할 것이므로 MySQL 포트인 3306으로 설정했습니다.



※ DB 서브넷 그룹 설정하기 ※


  이제 AWS RDS의 [서브넷 그룹] 탭으로 이동하여 [DB 서브넷 그룹 생성] 버튼을 눌러서 서브넷 그룹을 만들어 보도록 하겠습니다.



  다음과 같이 DB Subnet Group이라는 이름으로 서브넷 그룹을 만든 이후에 EC2와 동일한 VPC를 선택합니다.  그리고 [이 VPC와 관련된 모든 서브넷 추가] 버튼을 누르시면 됩니다.




  결과적으로 DB 서브넷 그룹을 생성해주시면 됩니다. 그러면 다음과 같이 하나의 서브넷 그룹 생성이 완료됩니다.



※ DB 한글 설정을 위한 파라미터 그룹 설정하기 ※


  MySQL 데이터베이스는 기본적으로 한글을 지원하지 않습니다. 그래서 [파라미터 그룹] 탭으로 이동하여 한글 지원을 위한 [파라미터 그룹 생성]을 진행하셔야 합니다.



  다음과 같이 MySQL 5.6 버전의 파라미터 그룹을 만들어 줍시다.



  이제 만들어진 파라미터 그룹으로 들어가 편집을 수행합니다.



  가장 먼저 [char]를 검색하여 모든 캐릭터 인코딩 관련 값을 UTF8으로 변경해주세요.



  이후에 [collation]을 검색하여 모든 값을 utf8_general_ci로 변경해 주시면 됩니다.




  이후에는 꼭 [변경 사항 저장] 버튼을 눌러서 저장해 주시면 됩니다.



※ AWS RDS 데이터베이스 생성하기 ※


  이제 실질적으로 MySQL 데이터베이스를 생성해보도록 하겠습니다. 그리고 AWS EC2에서만 접속이 가능하도록 설정할 것입니다.



  다음과 같이 MySQL 엔진을 선택하여 [RDS 프리 티어에 적용되는 옵션만 사용]을 체크해주세요. 무료로 이용하는 편이 좋을 거예요.



  결과적으로 다음과 같이 MySQL 5.6을 선택하여 만들어 주시면 됩니다.




  다만 고급 설정 구성을 하실 때에는 서브넷 그룹으로는 우리가 만들었던 DB Subnet Group을 선택한 뒤에, 보안 그룹도 마찬가지로 우리가 만들었던 DB Security Group을 선택하시면 됩니다. 그리고 [퍼블릭 액세스 가능성]아니요로 선택해주셔야 VPC 내부의 EC2에서만 접근이 가능합니다.



  결과적으로 데이터베이스 옵션으로는 아까 만들었던 한글 전용 파라미터 그룹을 선택하시고, 나머지는 기본 설정으로 데이터베이스를 생성해주시면 됩니다.




  결과적으로 다음과 같이 DB 인스턴스가 생성되었습니다.



※ AWS RDS에 연결하기 ※


  이제 만들어진 데이터베이스의 정보를 확인해보도록 합시다.



  다음과 같이 [연결] 탭에서 엔드포인트를 확인하시면 됩니다. 이 엔드포인트 주소로 접속할 수 있어요.



  이제 한 번 접속을 해봅시다.


  ▶ MySQL 접속 명령어: mysql -u empo -p --host empo.crtzcfwok2ed.ap-northeast-2.rds.amazonaws.com


  다음과 같이 외부 IP에서 해당 MySQL에 접속하려고 하면 연결 오류 메시지가 출력됩니다. 보안 그룹이 우리를 지켜주고 있는 거예요.



  하지만 같은 VPC 내에 있는 EC2에서 접근을 시도하면 다음과 같이 성공적으로 연결이 이루어지는 것을 확인할 수 있습니다.



  따라서 성공적으로 AWS EC2에 AWS RDS를 연동하게 된 것입니다.

728x90
반응형

728x90
반응형

  기본적으로 AWS VPC 안에 다양한 AWS 서비스를 포함하여 전체 시스템을 구축하는 경우가 많습니다. 다만 AWS VPC를 제거하고자 할 때 여러 개의 서비스가 서로 복잡하게 연결되어 있으면 제거가 안 될 수도 있습니다. 따라서 이번 시간에는 AWS VPC 안에 포함되어 있는 서비스들을 통째로 제거하는 방법에 대해서 알아보도록 하겠습니다.


※ 제거 순서를 고려하여 제거하기 ※


  기본적으로 VPC, [보안 그룹] 등을 지우기 전에는 그 안에 포함되어 있는 EC2나 RDS와 같은 실제 서비스들을 먼저 제거하셔야 합니다. 저는 지난 포스트(http://ndb796.tistory.com/224)에서 EC2와 RDS를 하나의 VPC 안에 넣어서 관리하는 방법에 대해서 알아보았습니다. 저는 이렇게 만들어진 시스템 구성을 통째로 제거해 볼 것입니다. 당연히 EC2와 RDS를 먼저 제거해야 합니다.


  따라서 다음과 같이 EC2 관리 화면에서 인스턴스부터 [제거] 해주겠습니다. 제거할 때는 [인스턴스 상태] 탭에서 [종료] 버튼을 누르시면 됩니다. 실제로는 인스턴스를 종료했을 때 완전히 제거가 이루어집니다.



  인스턴스를 종료하면 자동으로 볼륨 저장소가 제거됩니다. 당연한 겁니다.



  이후에 해당 인스턴스의 공인 IP를 위해 사용되던 탄력적 IP가 있다면 [탄력적 IP] 탭에서 이 또한 제거해주시면 됩니다.



  다음과 같이 [주소 연결 해제]를 먼저 진행해 주시고, [주소 릴리즈]까지 해주시면 됩니다.




  이제 EC2가 완전히 VPC에서 제거되었으므로 RDS 또한 지워주도록 하겠습니다. AWS RDS 서비스에서 [데이터베이스] 탭에 가서 우리 VPC에 포함되어 있는 데이터베이스를 찾아서 [수정] 버튼을 눌러주시면 됩니다.



  기본적으로 데이터베이스는 [삭제 방지] 기능이 적용되기 때문에 이를 비활성화 처리 해주셔야 합니다.



  다음과 같이 삭제 방지 비활성화 처리를 [즉시 적용] 해주시면 됩니다.



  이후에 데이터베이스를 삭제할 수 있습니다. [삭제] 버튼을 눌러 삭제해주세요.



  데이터베이스는 EC2보다 삭제하는 과정이 귀찮습니다. 왜냐하면 실제로 상당수 서비스에서 가장 중요한 데이터는 모두 데이터베이스 안에 있기 때문입니다. 그래서 [delete me]라는 문구를 입력하여 삭제를 진행해주셔야 합니다.



  이제야 VPC로 돌아오셔서 VPC를 삭제 처리할 수 있습니다. VPC 안에 포함되어 있는 EC2 및 RDS가 모두 제거된 상태라면 어렵지 않게 VPC를 삭제할 수 있습니다.



  다음과 같이 [Delete VPC] 버튼을 눌러서 VPC를 제거해보세요.



  만약 네트워크 인터페이스 등이 존재하는 경우 VPC를 곧바로 제거하실 수 없습니다. 이럴 때는 네트워크 인터페이스를 먼저 제거해주시면 됩니다.



  다음과 같이 네트워크 인터페이스를 먼저 제거해주세요.



  이제 VPC를 다시 제거해주시면 안정적으로 제거가 완료됩니다.



  VPC를 지우면서 연관된 오브젝트를 한꺼번에 지우도록 처리하시면 깔끔하게 제거가 완료됩니다.



728x90
반응형