안경잡이개발자

React와 Node.js로 만드는 고객 관리 시스템 개발 강좌 +18
728x90
반응형

  이번 시간에는 필터(Filter) 함수를 이용하여 고객(Customer) 검색 기능을 구현하는 방법에 대해서 알아보도록 하겠습니다.


▶ App.js


  본적으로 검색 창에 사용자가 입력한 문자열을 스테이트(State)에 반영하기 위해서는 다음과 같은 값 변경 처리 함수가 필요합니다. 바로 함수를 만들어 보도록 하겠습니다.


handleValueChange(e) {
let nextState = {};
nextState[e.target.name] = e.target.value;
this.setState(nextState);
}


  이후에 생성자(Constructor)에서 이를 바인딩(Binding) 처리해주시면 됩니다.


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


  이후에 <InputBase>에서 다음과 같이 작성합니다.


<InputBase
placeholder="검색하기"
classes={{
root: classes.inputRoot,
input: classes.inputInput,
}}
name="searchKeyword"
value={this.state.searchKeyword}
onChange={this.handleValueChange}
/>


  지금까지는 단순히 고객 스테이트가 변경되면 전체 고객을 바로 보여주는 식으로 코드가 동작했습니다. 이제는 전체 고객 컴포넌트를 생성하는 부분을 render() 함수의 가장 윗 부분에서 새롭게 명시해주도록 하겠습니다.


const filteredComponents = (data) => {
data = data.filter((c) => {
return c.name.indexOf(this.state.searchKeyword) > -1;
});
return data.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} />
});
}


  이후에 <TableBody> 부분을 다음과 같이 작성하면 됩니다.


<TableBody>
{this.state.customers ?
filteredComponents(this.state.customers) :
<TableRow>
<TableCell colSpan="6" align="center">
<CircularProgress className={classes.progress} variant="determinate" value={this.state.completed} />
</TableCell>
</TableRow>
}
</TableBody>


  마지막으로 새로운 고객이 추가된 경우 검색 창 내용도 비워질 수 있도록 처리하겠습니다.


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


※ 실행 결과 ※



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
반응형

  이번 시간에는 사용자가 보낸 고객 데이터를 Node.js Express로 처리하여 서버의 특정한 폴더에 파일 업로드 처리를 하는 방법에 대해서 알아보도록 하겠습니다. 이후에 고객 정보를 실제 MySQL 데이터베이스(Database)에 삽입하고, 업로드 폴더를 클라이언트에서 접근할 수 있도록 하여 고객이 이를 확인할 수 있도록 처리하는 시간을 가져 볼 것입니다.


▶ CustomerAdd.js


  따라서 가장 먼저 CustomerAdd.js의 handleFormSubmit() 함수를 수정해주도록 합시다. 데이터를 전송한 이후에는 고객 추가 양식(Form)을 비운 뒤에 페이지를 새로고침(Refresh)하여 등록된 고객 데이터를 확인하는 것입니다. 실제 배포 버전에서는 전체 페이지를 새로고침 하는 방향으로 코딩을 하면 안 되지만 빠른 테스트를 위해서 잠시 이와 같이 코딩해주도록 하겠습니다.


handleFormSubmit(e) {

e.preventDefault()

this.addCustomer()

.then((response) => {

console.log(response.data);

})

this.setState({

file: null,

userName: '',

birthday: '',

gender: '',

job: '',

fileName: ''

})

window.location.reload();

}



  이제 node.js에서 이러한 파일 업로드 요청을 처리하시면 됩니다. 이 때는 multer 라이브러리를 사용하시면 됩니다. 따라서 루트 폴더로 이동하셔서 npm install --save multer 명령어를 수행하신 뒤에 다음 소스코드를 작성합니다.


▶ server.js


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',

(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, ?, ?, ?, ?, ?)';

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.listen(port, () => console.log(`Listening on port ${port}`));


  이제 루트 폴더에서 upload 폴더를 생성해 주세요. 또한 사용자가 업로드한 파일은 깃 허브(Git Hub)에 공개적으로 올라가면 안 된다는 점에서 .gitignore에도 이 내용을 추가해주도록 합니다.


# upload

/upload


※ 실행 결과 ※




  실제로 고객 데이터와 파일이 업로드 된 이후에 새로고침이 자동으로 이루어지면서 추가된 고객 정보를 확인할 수 있습니다.



  결과적으로 다음과 같이 upload 폴더에서도 업로드 된 파일을 확인할 수 있습니다. 파일의 이름은 multer 라이브러리에 의해서 중복되지 않는 형태로 자동으로 바뀌어서 올라가게 됩니다.



  (+ 추가)


  실제로는 이러한 이미지 업로드 기능은 AWS S3과 같은 서비스를 이용해서 저장하면 매우 효과적입니다. 현재 소스코드 상으로는 데이터베이스와 서버에 업로드 된 이미지가 완전히 일치한다는 것을 보장하기 어렵기 때문입니다. 그리고 현재 소스코드에서는 새로운 고객 데이터가 등록된 이후에, 이를 확인하기 위해 전체 페이지를 새로고침(Refresh)하지만 실제로는 다시 고객 목록 데이터를 가져오는 식으로 코드를 동작시켜야 합니다.

728x90
반응형

728x90
반응형

  React에서 Form을 처리하기 위해서는 이벤트 처리(Event Handling)를 수행해야 합니다. 이번 시간에는 그러한 이벤트 처리 방법에 대해서 이해하고, 결과적으로 서버로 고객 데이터를 전송하여 데이터베이스에 신규 고객 정보를 등록하는 방법까지 알아보도록 하겠습니다. 따라서 가장 먼저 React 클라이언트의 components 폴더에 CustomerAdd.js를 만들어 보도록 하겠습니다.



  이제 디자인을 입히지 않은 상태로 하나의 <form> 태그를 작업하는 시간을 가져보도록 하겠습니다. 이를 위해 가장 먼저 서버와의 통신 목적의 라이브러리인 axios를 설치해주도록 하겠습니다. client 폴더로 이동하여 npm install --save axios 명령어를 입력하시면 됩니다. 이제 CustomerAdd.js 컴포넌트를 작업해 줍니다.


▶ CustomerAdd.js


import React from 'react'

import { post } from 'axios';


class CustomerAdd extends React.Component {


constructor(props) {

super(props);

this.state = {

file: null,

userName: '',

birthday: '',

gender: '',

job: '',

fileName: ''

}

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

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

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

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

}


handleFormSubmit(e) {

e.preventDefault()

this.addCustomer()

.then((response) => {

console.log(response.data);

})

}


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)

}


render() {

return (

<form onSubmit={this.handleFormSubmit}>

<h1>고객 추가</h1>

프로필 이미지: <input type="file" name="file" file={this.state.file} value={this.state.fileName} onChange={this.handleFileChange} /><br/>

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

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

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

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

<button type="submit">추가하기</button>

</form>

)

}

}


export default CustomerAdd


▶ App.js


  이제 작업한 CustomerAdd 컴포넌트를 화면에 출력해 보도록 하겠습니다. 따라서 App.js를 수정해 봅시다.


import CustomerAdd from './components/CustomerAdd';


  위와 같이 CustomerAdd 컴포넌트를 추가해주신 이후에 다음과 같이 렌더링 되는 부분을 수정합니다.


<div>

<Paper className={classes.root}>

<Table className={classes.table}>

<TableHead>

<TableRow>

<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 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/>

</div>


  실제로 이를 브라우저의 개발자 도구에서 테스트를 해보시면 서버로 데이터를 전송하는 것을 알 수 있습니다.



  아래쪽에 고객 추가 양식이 존재하며 여기에 데이터를 입력하여 실제로 고객을 추가할 수 있습니다.



  데이터를 입력해 [추가하기] 버튼을 누른 결과 다음과 같이 서버로 데이터가 전송되는 것을 알 수 있습니다. 실제로 이미지 파일은 바이너리 코드 형태로 서버로 전달됩니다. 따라서 다음 시간에 이러한 사용자의 요청 데이터를 적절히 처리하여 데이터베이스에 등록한 뒤에 그에 맞는 응답을 보내주는 작업을 수행해야 하빈다.





728x90
반응형

728x90
반응형

  지난 시간에는 AWS RDS 서비스를 이용해서 우리 고객 관리 시스템 프로젝트의 데이터베이스를 구축하는 시간을 가졌습니다. 이번 시간에는 실제로 우리가 지난 시간에 만든 MySQL 데이터베이스에 고객(Customer) 테이블을 구축한 뒤에 데이터를 삽입하는 시간을 가져보도록 하겠습니다.


※ 데이터베이스 테이블 구축하기 ※


  데이터베이스 이름을 management로 설정했으므로 여기에 들어가서 고객 테이블을 구축하여 데이터를 넣도록 소스를 작성해보겠습니다.


USE management;


CREATE TABLE CUSTOMER (

id INT PRIMARY KEY AUTO_INCREMENT,

image VARCHAR(1024),

name VARCHAR(64),

birthday VARCHAR(64),

gender VARCHAR(64),

job VARCHAR(64)

) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;


INSERT INTO CUSTOMER VALUES (1, 'https://placeimg.com/64/64/1', '홍길동', '960508', '남자', '대학생');

INSERT INTO CUSTOMER VALUES (2, 'https://placeimg.com/64/64/2', '나동빈', '961222', '남자', '프로그래머');

INSERT INTO CUSTOMER VALUES (3, 'https://placeimg.com/64/64/3', '이순신', '961127', '남자', '디자이너');


SELECT * FROM CUSTOMER;



※ .gitignore ※


  이제 데이터베이스와 연동하는 작업을 처리하도록 하겠습니다. 기본적으로 데이터베이스 관련 정보는 Git Ignore 처리를 해서 깃 허브(Git Hub)에 올라가지 않도록 처리해야 합니다. 따라서 서버 프로그램의 .gitignore 파일을 다음과 같이 수정합니다.


# database

/database.json


  이후에 database.json에서 데이터베이스 정보를 설정하시면 됩니다.


{

    "host": 데이터베이스 주소,

    "user": 데이터베이스 사용자,

    "password": 데이터베이스 비밀번호,

    "port": "3306",

    "database": 데이터베이스 이름

}



  그러면 위와 같이 프로젝트가 구성됩니다.


※ Node.js와 MySQL 연동하기 ※


  이제 Node.js와 MySQL을 연동해 봅시다. 가장 먼저 NPM을 이용하여 MySQL 라이브러리를 설치하셔야 합니다.


▶ npm install -S mysql


  이후에 설치된 mysql 라이브러리를 활용하여 node.js와 MySQL을 연동할 수 있습니다.


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 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',

(err, rows, fields) => {

res.send(rows);

}

)

});


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


  결과적으로 다음과 같이 서버 API가 정상적으로 동작하는 것을 확인할 수 있습니다.



  이후에 깃 허브(Git Hub)에 소스코드를 반영하면 다음과 같습니다.



  데이터베이스 설정 파일인 database.json 파일은 깃 허브에 올라가지 않는 것을 확인할 수 있습니다.


728x90
반응형

728x90
반응형

  이번 시간에는 AWS RDS 서비스를 이용하여 MySQL DB를 구축해보는 시간을 가져보도록 하겠습니다. 실제로 고객에 대한 정보를 저장하기 위해서는 MySQL과 같은 데이터베이스 시스템이 필요합니다. 따라서 이번 시간에는 AWS(Amazon Web Service)를 이용해서 데이터베이스를 구축하도록 하겠습니다. 일반적으로 AWS RDS를 이용하면 MySQL과 같은 관계형 데이터베이스 시스템을 쉽고 빠르게 구축할 수 있어요. 


  심지어 프리티어(Free-Tier) 서비스를 이용하면 무료로 이용할 수 있다는 점에서 매우 효과적이랍니다.


※ AWS RDS로 데이터베이스 만들기 ※


  AWS RDS 서비스를 검색하여 관계형 데이터베이스를 관리할 수 있습니다.



  이후에 [데이터베이스 생성]란에 들어가서 데이터베이스를 만들어 주시면 됩니다.



  저는 MySQL 서비스를 이용하도록 하겠습니다. 이 때 [RDS 프리 티어에 적용되는 옵션만 사용] 란에 체크해주시면 됩니다.



  이후에 DB 세부 정보를 지정할 수 있습니다. 저는 MySQL 5.6 버전을 이용하도록 하겠습니다.



  이후에 기본 스토리지 크기인 20GiB를 사용하시면 됩니다. 그리고 이후에 데이터베이스에 접속하기 위한 계정 정보를 넣어주세요.



  이제 [고급 설정 구성]을 진행하시면 됩니다. 일반적으로 DB 보안 관리를 꼭 해주셔야 하지만 저는 강의를 위해서 일단 외부에서 쉽게 접속할 수 있도록 [퍼블릭 액세스 가능성]을 예(Yes)로 설정하겠습니다.



  포트(Port) 번호 또한 기본 포트인 3306번 포트를 사용하시면 됩니다. DB 이름도 설정하시면 돼요.



  이후에 다음과 같이 [데이터베이스 생성]을 진행할 수 있습니다.



  인스턴스를 생성한 이후에는 [DB 인스턴스 세부 정보 보기]를 눌러서 세부 정보를 확인할 수 있습니다.



  기본적으로 MySQL은 영어로 설정되어 있으므로 한글을 넣고자 한다면 UTF-8으로 인코딩 설정을 해야 합니다. 따라서 [파라미터 그룹] 탭으로 이동해서 [파라미터 그룹 생성]을 진행하시면 됩니다.



  저는 다음과 같이 utf8-parameter라는 이름으로 파라미터 그룹을 생성해보았습니다.



  가장 먼저 char를 검색해서 나오는 모든 속성에 대해서 값으로 UTF-8 설정을 해주세요.



  이후에 collation을 검색하여 값으로 utf8_general_ci를 설정하시면 됩니다.




  이제 이렇게 만들어진 파라미터 그룹을 우리의 데이터베이스에 적용해보도록 하겠습니다.



  해당 데이터베이스에서 [수정] 탭으로 들어가 적용해주시면 됩니다.




  결과적으로 [변경 중] 표시가 뜨면서 약간의 시간이 흐른 뒤에 파라미터 그룹이 적용됩니다.



  혹여 파라미터 그룹 설정이 적용이 되지 않는 경우에는 데이터베이스를 재시작하시면 됩니다.


※ HeidiSQL을 이용해 데이터베이스에 접속하기 ※


  이제 HeidiSQL을 이용해서 데이터베이스에 접속하는 방법에 대해서 알아보도록 하겠습니다.


  ▶ HeidiSQL 설치 경로: https://www.heidisql.com/download.php


  위 경로에 접속하여 HeidiSQL을 설치할 수 있습니다.



  기본적인 설치 프로그램으로 설치를 진행하시면 됩니다. 이후에 해당 프로그램을 실행해 주시면 됩니다.



  저는 위와 같이 신규 세션(Session)을 만들어 주었습니다. MySQL 접속 경로는 AWS RDS 관리 페이지에서 확인할 수 있습니다.



  이전에 자신이 설정했던 계정 정보로 그대로 로그인을 해주시면 됩니다.



  로그인 이후에는 다음과 같이 데이터베이스가 구성되어 있는 것을 확인할 수 있습니다. 왼쪽 탭에서는 데이터베이스 정보가 출력됩니다. 아까 전에 우리가 만들었던 management라는 이름의 데이터베이스가 존재합니다.



  간단히 쿼리(Query) 탭으로 이동하여 select version(); 명령어를 실행해 MySQL 버전을 확인할 수 있습니다.



※ 데이터베이스 접속이 안 되는 경우 ※


  혹시 데이터베이스 접속이 안 되는 경우 데이터베이스의 [보안 그룹]을 확인하시는 것이 중요합니다.



  보안 그룹의 [인바운드] 규칙을 확인했을 때 다음과 같이 접속하고자 하는 소스(Source)가 0.0.0.0(위치 무관)으로 설정되어 있어야 언제 어디서든 해당 데이터베이스에 접근할 수 있습니다.



728x90
반응형

728x90
반응형

  흔히 소프트웨어에서는 프로그레스 바(Progress Bar)를 구현하여 API 로딩 메시지를 웹 사이트 상에 출력할 수 있습니다. 이번 시간에는 고객 목록을 불러오는 API를 요청했을 때 서버에서 응답을 늦게 하는 경우 로딩 메시지를 화면에 띄워주는 방법에 대해서 알아보도록 하겠습니다. 프로그레스 바 UI 또한 Material UI에 이미 구현이 되어 있습니다. 따라서 이를 단순히 사용해주면 됩니다.


  ▶ Material UI 프로그레스 바: https://material-ui.com/demos/progress/


  우리는 이 예제 중에서 [Determinate]을 선택해 구현해 보도록 하겠습니다.



※ React의 라이프 사이클(Life Cycle) ※


  기본적으로 리액트 라이브러리가 처음 컴포넌트를 실행할 때는 다음의 순서를 따릅니다.


1) constructor()

2) componentWillMount()

3) render()

4) componentDidMount()


  그리고 컴포넌트의 props 혹은 state가 변경될 때는 shouldComponentUpdate() 함수 등이 사용되어 실질적으로 다시 render() 함수를 불러와 뷰(View)를 갱신하게 됩니다. 또한 컴포넌트가 삭제될 때는 componentWillUnmount() 함수가 실행된다는 특징이 있습니다.


  따라서 일반적으로 API를 불러와서 웹 사이트 화면에 특정한 뷰(View)를 출력하고자 한다면 componentDidMount() 함수에서 API를 비동기적으로 호출하면 됩니다. 이후에 API에서 응답(Response)이 돌아왔을 때 비로소 뷰(View)가 갱신되므로 화면에 API 응답 결과를 출력할 수 있는 것입니다. 비동기적으로 호출한다는 점에서 API 서버에서 응답을 하지 않으면 사용자에게 로딩 화면만 출력이 됩니다.


▶ App.js


  따라서 Circular Progress 라이브러리를 이용하여 프로그레스 바를 API 로딩 메시지 용도로 사용하도록 하겠습니다.


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';


const styles = theme => ({

root: {

width: "100%",

marginTop: theme.spacing.unit * 3,

overflowX: "auto"

},

table: {

minWidth: 1080

},

progress: {

margin: theme.spacing.unit * 2

}

});


class App extends Component {

state = {

customers: '',

completed: 0

}


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;

return (

<Paper className={classes.root}>

<Table className={classes.table}>

<TableHead>

<TableRow>

<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 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>

);

}

}


export default withStyles(styles)(App);


※ 실행 결과 ※


  API를 불러올 때의 화면입니다.



API가 불러와 졌을 때의 결과 화면입니다.



(+ 추가)


  효과적인 로딩 테스트를 위해서는 서버 API의 지연을 발생시키는 것이 좋습니다. 서버에서 응답(Response) 데이터를 보내주는 코드에 Timeout을 적용해 보세요. setTimeout(function() { 소스 코드 }, 지연 시간)의 문법으로 간단히 사용해 볼 수 있습니다.


728x90
반응형