안경잡이개발자

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

  이번 시간에는 AWS EC2를 이용해 플라스크(Flask) 웹 서버를 구동시키도록 하겠습니다. 지금까지의 웹 서버는 로컬호스트(Localhost)에서만 동작했습니다. 하지만 실제로 배포된 React 프론트 엔드와 정상적으로 통신하기 위해서는 공인 주소를 가지는 서버가 필요합니다. 따라서 가장 빠르고 효과적으로 서버를 구축할 수 있도록 해주는 AWS를 이용하는 것입니다. AWS에 가입하여 다음과 같이 관리 콘솔(Management Console)로 들어갑니다.


▶ EC2 서비스 들어가서 인스턴스 만들기




  인스턴스를 만들 때는 우분투 리눅스 서버를 만드시면 됩니다.



  이후에 무료 서버를 이용하기 위해 프리 티어(Free Tier)로 서버를 생성하세요.




  [시작] 버튼을 누르면 다음과 같이 키 페어 설정 메시지가 나옵니다. 새로운 키 페어를 사용하도록 하겠습니다. 저는 [MyKeyPair]라는 이름으로 키 페어를 만들었어요.



  결과적으로 다음과 같이 인스턴스를 시작하시면 됩니다.



▶ EC2에 접속하기

  

  이제 방금 만든 키 페어를 이용해서 EC2에 접속할 수 있습니다.



  인스턴스에 접속할 때는 일반적으로 SSH 명령을 사용합니다.



  다만 접속하기 위해서는 방금 다운로드 받은 키 페어의 권한 설정을 바꿀 필요가 있습니다. 리눅스에서는 chmod 명령어로 쉽게 변경할 수 있으나, 윈도우는 상대적으로 복잡합니다. 일단 가장 먼저 키 페어 파일의 속성을 확인하여 [보안] 탭으로 들어가보도록 하겠습니다.



  이후에 [고급] 탭으로 들어가서 관리자 그룹을 제외한 모든 그룹의 상속을 없앤 뒤에 사용 권한을 제거하시면 됩니다.



  그러면 다음과 같은 관리자 그룹만 남게 됩니다.



  이제 명령 프롬프트(CMD)를 관리자 권한으로 실행하여 키 페어(Key Pair)를 이용해 인스턴스에 접속하시면 됩니다.



▶ 라이브러리 설치


  이제 파이썬 서버의 소스코드를 그대로 복제합니다.



  이후에 다음과 같이 필요한 라이브러리들을 설치합니다. 라이브러리 설치를 위하여 파이썬 패키지 관리 도구인 pip를 이용하면 됩니다. 또한 자연어 처리 라이브러리를 위해서 Java 설치를 진행해주시면 됩니다.


sudo apt update

sudo apt install python3-pip

sudo pip3 install wordcloud

sudo pip3 install konlpy

sudo pip3 install matplotlib

sudo pip3 install flask

sudo pip3 install flask_cors

sudo apt install openjdk-8-jre

sudo apt install openjdk-8-jdk


▶ 웹 서버 실행하기


sudo python3 word_cloud.py



  결과적으로 서버가 구동되었습니다. 이제 이에 접속하기 위하여 보안 그룹 설정을 해주시면 됩니다.



  보안 그룹의 인바운드 규칙을 [편집] 해주시면 됩니다.



  다음과 같이 플라스크 웹 서버의 포트 번호인 5000번 포트를 열어 주시면 됩니다.



  이제 퍼블릭(Public) IP를 이용해 접속하시면 됩니다.



  5000번 포트로 접속하니 다음과 같이 웹 서버가 구동 중인 것을 확인할 수 있었습니다.



  이로써 AWS EC2를 활용해 파이썬 웹 서버를 구동시키게 되었습니다.


※ 파이썬 서버 항상 실행시키기 ※


  다만 현재의 소스코드에서는 SSH 프롬프트에서 나오는 순간 파이썬 프로세스가 죽게 됩니다. 따라서 항상 파이썬 프로세스가 동작하도록 처리할 필요가 있습니다. 이 때는 파이썬 프로세스를 백그라운드에서 동작하도록 한 뒤에 소유권을 포기하면 됩니다.


  python3 word_cloud.py: 파이썬 서버 구동시키기

  Ctrl + Z: 프로세스 중지하기

  bg: 백그라운드에서 서버를 다시 구동시키기

  disown -h: 소유권 포기하기



※ 서버를 중지시키고 싶을 때는? ※


  서버를 중지시키고 싶을 때는 특정 포트 번호로 돌아가는 프로세스를 확인한 뒤에 해당 프로세스를 죽이면 됩니다.


  netstat -nap | grep {포트 번호}: 특정 포트 번호에서 돌아가는 프로세스를 확인하기

  kill -9 {프로세스 번호}: 특정한 프로세스를 종료시키기



728x90
반응형

728x90
반응형

  이번 시간에는 만들어진 워드 클라우드(Word Cloud) API를 웹 상에서 접근할 수 있는 방법에 대해서 알아보도록 하겠습니다. 웹 상에서 특정한 이미지 파일 등에 접근하기 위해서는 정적 폴더(Static Folder)를 설정해야 합니다.


▶ word_cloud.py


# 단어구름에 필요한 라이브러리를 불러옵니다.
from wordcloud import WordCloud
# 한국어 자연어 처리 라이브러리를 불러옵니다.
from konlpy.tag import Twitter
# 명사의 출현 빈도를 세는 라이브러리를 불러옵니다.
from collections import Counter
# 그래프 생성에 필요한 라이브러리를 불러옵니다.
import matplotlib.pyplot as plt
# Flask 웹 서버 구축에 필요한 라이브러리를 불러옵니다.
from flask import Flask, request, jsonify

# 플라스크 웹 서버 객체를 생성합니다.
app = Flask(__name__, static_folder='outputs')

# 폰트 경로 설정
font_path = 'NanumGothic.ttf'


def get_tags(text, max_count, min_length):
# 명사만 추출합니다.
t = Twitter()
nouns = t.nouns(text)
processed = [n for n in nouns if len(n) >= min_length]
# 모든 명사의 출현 빈도를 계산합니다.
count = Counter(processed)
result = {}
# 출현 빈도가 높은 max_count 개의 명사만을 추출합니다.
for n, c in count.most_common(max_count):
result[n] = c
# 추출된 단어가 하나도 없는 경우 '내용이 없습니다.'를 화면에 보여줍니다.
if len(result) == 0:
result["내용이 없습니다."] = 1
return result


def make_cloud_image(tags, file_name):
# 만들고자 하는 워드 클라우드의 기본 설정을 진행합니다.
word_cloud = WordCloud(
font_path=font_path,
width=800,
height=800,
background_color="white",
)
# 추출된 단어 빈도수 목록을 이용해 워드 클라우드 객체를 초기화 합니다.
word_cloud = word_cloud.generate_from_frequencies(tags)
# 워드 클라우드를 이미지로 그립니다.
fig = plt.figure(figsize=(10, 10))
plt.imshow(word_cloud)
plt.axis("off")
# 만들어진 이미지 객체를 파일 형태로 저장합니다.
fig.savefig("outputs/{0}.png".format(file_name))


def process_from_text(text, max_count, min_length, words, file_name):
# 최대 max_count 개의 단어 및 등장 횟수를 추출합니다.
tags = get_tags(text, max_count, min_length)
# 단어 가중치를 적용합니다.
for n, c in words.items():
if n in tags:
tags[n] = tags[n] * int(words[n])
# 명사의 출현 빈도 정보를 통해 워드 클라우드 이미지를 생성합니다.
make_cloud_image(tags, file_name)


@app.route("/process", methods=['GET', 'POST'])
def process():
content = request.json
words = {}
if content['words'] is not None:
for data in content['words'].values():
words[data['word']] = data['weight']
process_from_text(content['text'], content['maxCount'], content['minLength'], words, content['textID'])
result = {'result': True}
return jsonify(result)


@app.route('/outputs', methods=['GET', 'POST'])
def output():
text_id = request.args.get('textID')
return app.send_static_file(text_id + '.png')


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


▶ 이미지 생성


  이미지 생성에는 textID라는 파라미터를 추가적으로 넣어주세요.



▶ 이미지 접근


  이미지에 접근할 때는 다음과 같이 파라미터로 접근하면 됩니다.



▶ word_cloud.py


  이러한 이미지에 실제로 클라이언트에서 효과적으로 접근할 수 있도록 이미지가 존재하는지 확인하기 위한 validate() 함수를 추가적으로 만들어 주었습니다.


# 단어구름에 필요한 라이브러리를 불러옵니다.
from wordcloud import WordCloud
# 한국어 자연어 처리 라이브러리를 불러옵니다.
from konlpy.tag import Twitter
# 명사의 출현 빈도를 세는 라이브러리를 불러옵니다.
from collections import Counter
# 그래프 생성에 필요한 라이브러리를 불러옵니다.
import matplotlib.pyplot as plt
# Flask 웹 서버 구축에 필요한 라이브러리를 불러옵니다.
from flask import Flask, request, jsonify
# 테스트를 위하여 CORS를 처리합니다.
from flask_cors import CORS
# 파일에 접근하기 위한 라이브러리를 불러옵니다.
import os

# 플라스크 웹 서버 객체를 생성합니다.
app = Flask(__name__, static_folder='outputs')
CORS(app)

# 폰트 경로 설정
font_path = 'NanumGothic.ttf'


def get_tags(text, max_count, min_length):
# 명사만 추출합니다.
t = Twitter()
nouns = t.nouns(text)
processed = [n for n in nouns if len(n) >= min_length]
# 모든 명사의 출현 빈도를 계산합니다.
count = Counter(processed)
result = {}
# 출현 빈도가 높은 max_count 개의 명사만을 추출합니다.
for n, c in count.most_common(max_count):
result[n] = c
# 추출된 단어가 하나도 없는 경우 '내용이 없습니다.'를 화면에 보여줍니다.
if len(result) == 0:
result["내용이 없습니다."] = 1
return result


def make_cloud_image(tags, file_name):
# 만들고자 하는 워드 클라우드의 기본 설정을 진행합니다.
word_cloud = WordCloud(
font_path=font_path,
width=800,
height=800,
background_color="white",
)
# 추출된 단어 빈도수 목록을 이용해 워드 클라우드 객체를 초기화 합니다.
word_cloud = word_cloud.generate_from_frequencies(tags)
# 워드 클라우드를 이미지로 그립니다.
fig = plt.figure(figsize=(10, 10))
plt.imshow(word_cloud)
plt.axis("off")
# 만들어진 이미지 객체를 파일 형태로 저장합니다.
path = "outputs/{0}.png".format(file_name)
# 이미 파일이 존재하는 경우 덮어쓰기합니다.
if os.path.isfile(path):
os.remove(path)
fig.savefig(path)


def process_from_text(text, max_count, min_length, words, file_name):
# 최대 max_count 개의 단어 및 등장 횟수를 추출합니다.
tags = get_tags(text, max_count, min_length)
# 단어 가중치를 적용합니다.
for n, c in words.items():
if n in tags:
tags[n] = tags[n] * int(words[n])
# 명사의 출현 빈도 정보를 통해 워드 클라우드 이미지를 생성합니다.
make_cloud_image(tags, file_name)


@app.route("/process", methods=['GET', 'POST'])
def process():
content = request.json
words = {}
if content['words'] is not None:
for data in content['words'].values():
words[data['word']] = data['weight']
process_from_text(content['text'], content['maxCount'], content['minLength'], words, content['textID'])
result = {'result': True}
return jsonify(result)


@app.route('/outputs', methods=['GET', 'POST'])
def output():
text_id = request.args.get('textID')
return app.send_static_file(text_id + '.png')


@app.route('/validate', methods=['GET',' POST'])
def validate():
text_id = request.args.get('textID')
path = "outputs/{0}.png".format(text_id)
result = {}
# 해당 이미지 파일이 존재하는지 확인합니다.
if os.path.isfile(path):
result['result'] = True
else:
result['result'] = False
return jsonify(result)


if __name__ == '__main__':
app.run('0.0.0.0', port=5000, threaded=True) # 처리 속도 향상을 위해 쓰레드를 적용합니다.

▶ Detail.js


  이제 Detail.js 파일을 수정합니다. 기본적으로 React에서 이미지(Image) 태그의 정보가 수정될 때에는 브라우저 캐시(Cache)가 동작하지 않도록, 별도의 랜덤 파라미터를 추가적으로 붙여 줄 수 있습니다.


import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import '../index.css';
import { withStyles } from '@material-ui/core/styles';
import Fab from '@material-ui/core/Fab';
import UpdateIcon from '@material-ui/icons/Update';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogTitle from '@material-ui/core/DialogTitle';
import Button from '@material-ui/core/Button';

const databaseURL = "https://react-example-55161.firebaseio.com/";
const apiURL = "http://localhost:5000";

const styles = theme => ({
fab: {
position: 'fixed',
bottom: '20px',
right: '20px'
},
});


class Detail extends React.Component {

constructor(props) {
super(props);
this.state = {
dialog: false,
textContent: '',
words: {},
imageUrl: null
}
}

componentDidMount() {
this._getImage();
this._getText();
this._getWords();
}

_getText() {
fetch(`${databaseURL}/texts/${this.props.match.params.textID}.json`).then(res => {
if(res.status != 200) {
throw new Error(res.statusText);
}
return res.json();
}).then(text => this.setState({textContent: text['textContent']}));
}

_getWords() {
fetch(`${databaseURL}/words.json`).then(res => {
if(res.status != 200) {
throw new Error(res.statusText);
}
return res.json();
}).then(words => this.setState({words: (words == null) ? {} : words}));
}

_getImage() {
fetch(`${apiURL}/validate?textID=${this.props.match.params.textID}`).then(res => {
if(res.status != 200) {
throw new Error(res.statusText);
}
return res.json();
}).then(data => {
if(data['result'] == true) {
this.setState({imageUrl: apiURL + "/outputs?textID=" + this.props.match.params.textID})
} else {
this.setState({imageUrl: 'NONE'});
}
});
}

handleDialogToggle = () => this.setState({
dialog: !this.state.dialog
})

handleSubmit = () => {
this.setState({imageUrl: 'READY'});
const wordCloud = {
textID: this.props.match.params.textID,
text: this.state.textContent,
maxCount: 30,
minLength: 1,
words: this.state.words
}
this.handleDialogToggle();
if (!wordCloud.textID ||
!wordCloud.text ||
!wordCloud.maxCount ||
!wordCloud.minLength ||
!wordCloud.words) {
return;
}
this._post(wordCloud);
}
_post = (wordCloud) => {
return fetch(`${apiURL}/process`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(wordCloud)
}).then(res => {
if(res.status != 200) {
throw new Error(res.statusText);
}
return res.json();
}).then(data => {
this.setState({imageUrl: apiURL + "/outputs?textID=" + this.props.match.params.textID})
});
}

render() {
const { classes } = this.props;
return (
<div>
<Card>
<CardContent>
{
(this.state.imageUrl)?
((this.state.imageUrl == 'READY')?
'워드 클라우드 이미지를 불러오고 있습니다.':
((this.state.imageUrl == 'NONE')?
'해당 텍스트에 대한 워드 클라우드를 만들어 주세요.':
<img key={Math.random()} src={this.state.imageUrl + '&random=' + Math.random()} style={{width: '100%'}}/>)):
''
}
</CardContent>
</Card>
<Fab color="primary" className={classes.fab} onClick={this.handleDialogToggle}>
<UpdateIcon />
</Fab>
<Dialog open={this.state.dialog} onClose={this.handleDialogToggle}>
<DialogTitle>워드 클라우드 생성</DialogTitle>
<DialogActions>
<Button variant="contained" color="primary" onClick={this.handleSubmit}>
{(this.state.imageUrl == 'NONE')? '만들기' : '다시 만들기'}
</Button>
<Button variant="outlined" color="primary" onClick={this.handleDialogToggle}>닫기</Button>
</DialogActions>
</Dialog>
</div>
);
}
}

export default withStyles(styles)(Detail);


※ 실행 결과 ※


  먼저 하나의 텍스트를 추가해보도록 하겠습니다.




  이어서 만들어진 텍스트를 확인해보도록 하겠습니다.



  처음에는 워드 클라우드가 존재하지 않는 상황입니다.



  워드 클라우드를 생성한 이후에는 다음과 같이 정상적으로 출력됩니다.




  또한 만들어진 이후에 [다시 만들기] 버튼을 누르면 새롭게 워드 클라우드가 생성됩니다.


728x90
반응형

728x90
반응형

  이번 시간에는 플라스크(Flask) 웹 서버를 이용하여 워드 클라우드 API 를 구현하는 시간을 가져보도록 하겠습니다. 클라이언트(Client)에 해당하는 리액트 프론트 엔드에서 워드 클라우드 이미지를 요청하기 위해서는, 클라이언트의 요청(Request)을 받아서 워드 클라우드를 그려주는 웹 서버를 작업해야 합니다.


  추가적으로 필요한 라이브러리는 다음과 같습니다.


▶ WordCloud(워드 클라우드를 그리는 라이브러리), konlpy Twitter(한국어 자연어 처리 라이브러리), Counter(명사의 출현 빈도를 세는 라이브러리), Matplotlib(워드 클라우드 이미지를 실제로 그림 파일에 표현하는 목적의 라이브러리)


pip3 install wordcloud
pip3 install konlpy
pip3 install matplotlib
pip3 install flask
pip3 install flask_cors


  일단 프로젝트를 다음과 같이 구성합니다. 파이참(PyCharm)으로 열거나 혹은 단순한 폴더 형태로 구비하시면 됩니다. 폴더에는 outputs(워드 클라우드가 저장되는 폴더), NanumGothic.ttf(나눔고딕 글씨체), word_cloud.py(전체 소스코드)가 포함됩니다.


NanumGothic.ttf





▶ word_cloud.py

# 단어구름에 필요한 라이브러리를 불러옵니다.
from wordcloud import WordCloud
# 한국어 자연어 처리 라이브러리를 불러옵니다.
from konlpy.tag import Twitter
# 명사의 출현 빈도를 세는 라이브러리를 불러옵니다.
from collections import Counter
# 그래프 생성에 필요한 라이브러리를 불러옵니다.
import matplotlib.pyplot as plt
# Flask 웹 서버 구축에 필요한 라이브러리를 불러옵니다.
from flask import Flask, request, jsonify

# 플라스크 웹 서버 객체를 생성합니다.
app = Flask(__name__)

# 폰트 경로 설정
font_path = 'NanumGothic.ttf'


def get_tags(text, max_count, min_length):
# 명사만 추출합니다.
t = Twitter()
nouns = t.nouns(text)
processed = [n for n in nouns if len(n) >= min_length]
# 모든 명사의 출현 빈도를 계산합니다.
count = Counter(processed)
result = {}
# 출현 빈도가 높은 max_count 개의 명사만을 추출합니다.
for n, c in count.most_common(max_count):
result[n] = c
# 추출된 단어가 하나도 없는 경우 '내용이 없습니다.'를 화면에 보여줍니다.
if len(result) == 0:
result["내용이 없습니다."] = 1
return result


def make_cloud_image(tags, file_name):
# 만들고자 하는 워드 클라우드의 기본 설정을 진행합니다.
word_cloud = WordCloud(
font_path=font_path,
width=800,
height=800,
background_color="white",
)
# 추출된 단어 빈도수 목록을 이용해 워드 클라우드 객체를 초기화 합니다.
word_cloud = word_cloud.generate_from_frequencies(tags)
# 워드 클라우드를 이미지로 그립니다.
fig = plt.figure(figsize=(10, 10))
plt.imshow(word_cloud)
plt.axis("off")
# 만들어진 이미지 객체를 파일 형태로 저장합니다.
fig.savefig("outputs/{0}.png".format(file_name))


def process_from_text(text, max_count, min_length, words):
# 최대 max_count 개의 단어 및 등장 횟수를 추출합니다.
tags = get_tags(text, max_count, min_length)
# 단어 가중치를 적용합니다.
for n, c in words.items():
if n in tags:
tags[n] = tags[n] * int(words[n])
# 명사의 출현 빈도 정보를 통해 워드 클라우드 이미지를 생성합니다.
make_cloud_image(tags, "output")


@app.route("/process", methods=['GET', 'POST'])
def process():
content = request.json
words = {}
if content['words'] is not None:
for data in content['words'].values():
words[data['word']] = data['weight']
process_from_text(content['text'], content['maxCount'], content['minLength'], words)
result = {'result': True}
return jsonify(result)


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

  서버를 실행하면 자동으로 5000번 포트에 웹 서버 할당이 이루어집니다.



※ API 테스트 ※


  이제 만들어진 플라스크 웹 서버에 API 테스트를 진행해보도록 하겠습니다. 크롬(Chrome) 확장 프로그램인 Restlet을 이용하면 쉽게 Rest API를 테스트할 수 있습니다.


▶ 요청(Request) JSON 예제


{
    "text": "안녕하세요? 저는 한국교원대학교 나동빈입니다. 여러분들과 함께 다양한 공부를 진행하면서 스터디에 참여하고 싶어요. 한 번 공부를 할 때 제대로 공부를 하는 것이 목표입니다. 공부는 쉽지 않지만 열심히 하다 보면 재미를 느끼고 참여할 수 있을 것 같아요.",
    "maxCount": 15,
  "minLength": 2,
    "words":
{"1":{"weight":"7","word":"스터디"},"2":{"weight":"5","word":"참여"},"3":{"weight":"5","word":"분노"},"4":{"weight":"4","word":"치킨"}}
}


  이를 /process 경로로 전달하시면 됩니다.



  실행 결과 다음과 같이 성공 메시지가 출력됩니다.




  이후에 서버 폴더로 이동하여 output.png 파일을 확인해 보시면 다음과 같이 워드 클라우드 파일이 성공적으로 생성되어 있습니다.



※ 깃 허브에 업로드하기 ※


  이후에 다음과 같이 깃 허브에 리포지터리를 업로드 하시면 됩니다.



728x90
반응형

728x90
반응형

1. 텍스트 가지치기 모듈 다운로드


  yarn add react-text-truncate


  (설치가 안 된다면, 개발 환경을 관리자 권한으로 실행해보세요.)



2. 소스코드 작성하기


▶ ./src/components/Texts.js


import TextTruncate from 'react-text-truncate';
import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import { withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import Fab from '@material-ui/core/Fab';
import AddIcon from '@material-ui/icons/Add';
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 { Link as RouterLink } from 'react-router-dom';
import Link from '@material-ui/core/Link';

const styles = theme => ({
hidden: {
display: 'none'
},
fab: {
position: 'fixed',
bottom: '20px',
right: '20px'
},
});

const databaseURL = "https://react-example-55161.firebaseio.com/";

class Texts extends React.Component {

constructor(props) {
super(props);
this.state = {
fileName: '',
fileContent: null,
texts: '',
textName: '',
dialog: false
}
}

_get() {
fetch(`${databaseURL}/texts.json`).then(res => {
if(res.status != 200) {
throw new Error(res.statusText);
}
return res.json();
}).then(texts => this.setState({texts: (texts == null) ? {} : texts}));
}

_post(text) {
return fetch(`${databaseURL}/texts.json`, {
method: 'POST',
body: JSON.stringify(text)
}).then(res => {
if(res.status != 200) {
throw new Error(res.statusText);
}
return res.json();
}).then(data => {
let nextState = this.state.texts;
nextState[data.name] = text;
this.setState({texts: nextState});
});
}

_delete(id) {
return fetch(`${databaseURL}/texts/${id}.json`, {
method: 'DELETE'
}).then(res => {
if(res.status != 200) {
throw new Error(res.statusText);
}
return res.json();
}).then(() => {
let nextState = this.state.texts;
delete nextState[id];
this.setState({texts: nextState});
});
}

componentDidMount() {
this._get();
}

handleDialogToggle = () => this.setState({
dialog: !this.state.dialog,
fileName: '',
fileContent: '',
textName: ''
})

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

handleSubmit = () => {
const text = {
textName: this.state.textName,
textContent: this.state.fileContent
}
this.handleDialogToggle();
if (!text.textName || !text.textContent) {
return;
}
this._post(text);
}

handleDelete = (id) => {
this._delete(id);
}

handleFileChange = (e) => {
let reader = new FileReader();
reader.onload = () => {
let text = reader.result;
this.setState({
fileContent: text
})
}
reader.readAsText(e.target.files[0], "EUC-KR");
this.setState({
fileName: e.target.value
});
}

render() {
const { classes } = this.props;
return (
<div>
{Object.keys(this.state.texts).map(id => {
const text = this.state.texts[id];
return (
<Card key={id}>
<CardContent>
<Typography color="textSecondary" gutterBottom>
내용: {text.textContent.substring(0, 24) + '...'}
</Typography>
<Grid container>
<Grid item xs={6}>
<Typography variant="h5" component="h2">
{text.textName.substring(0, 14) + '...'}
</Typography>
</Grid>
<Grid item xs={3}>
<Typography variant="h5" component="h2">
<Link component={RouterLink} to={"detail/" + id}>
<Button variant="contained" color="primary">보기</Button>
</Link>
</Typography>
</Grid>
<Grid item xs={3}>
<Button variant="contained" color="primary" onClick={() => this.handleDelete(id)}>삭제</Button>
</Grid>
</Grid>
</CardContent>
</Card>
);
})}
<Fab color="primary" className={classes.fab} onClick={this.handleDialogToggle}>
<AddIcon />
</Fab>
<Dialog open={this.state.dialog} onClose={this.handleDialogToggle}>
<DialogTitle>텍스트 추가</DialogTitle>
<DialogContent>
<TextField label="텍스트 이름" type="text" name="textName" value={this.state.textName} onChange={this.handleValueChange} /><br /><br />
<input className={classes.hidden} accept="text/plain" 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 === ''? ".txt 파일 선택" : this.state.fileName}
</Button>
</label>
<TextTruncate
line={1}
truncateText="..."
text={this.state.fileContent}
/>
</DialogContent>
<DialogActions>
<Button variant="contained" color="primary" onClick={this.handleSubmit}>추가</Button>
<Button variant="outlined" color="primary" onClick={this.handleDialogToggle}>닫기</Button>
</DialogActions>
</Dialog>
</div>
);
}
}

export default withStyles(styles)(Texts);


▶ ./src/components/Detail.js


import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';

class Detail extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Card>
<CardContent>
{this.props.match.params.textID}
</CardContent>
</Card>
);
}
}

export default Detail;


▶ ./src/components/App.js


import React from 'react';
import { HashRouter as Router, Route } from 'react-router-dom';
import AppShell from './AppShell';
import Home from './Home';
import Texts from './Texts';
import Words from './Words';
import Detail from './Detail';

class App extends React.Component {
render() {
return (
<Router>
<AppShell>
<div>
<Route exact path="/" component={Home}/>
<Route exact path="/texts" component={Texts}/>
<Route exact path="/words" component={Words}/>
<Route exact path="/detail/:textID" component={Detail}/>
</div>
</AppShell>
</Router>
);
}
}

export default App;


※ 실행 결과 ※


  텍스트 페이지에서 텍스트 파일을 올려서 내용을 확인할 수 있습니다.





  [보기] 버튼을 누르면 상세 페이지로 이동됩니다. 현재는 ID 값만 보내도록 처리했습니다.



728x90
반응형

728x90
반응형

▶ 구글 파이어베이스 콘솔: https://console.firebase.google.com/


1. 프로젝트 생성하기




2. 데이터베이스 페이지 확인



3. 데이터베이스에 단어(Word) 데이터 구축


  (단, 원래는 ID 값으로 0, 1, 2와 같은 단순한 숫자를 넣지 않습니다. 이는 예시 데이터를 구성하기 위해 넣은 것이며, 실제 DB 연동 작업을 마쳤을 때에는 이러한 예시 데이터를 삭제해야 오류가 발생하지 않습니다.)



4. 규칙(Rule) 설정을 통한 외부 접속 허용



5. 단어(Word) API 호출 테스트



▶ ./src/components/Words.js


import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import Typography from '@material-ui/core/Typography';

const databaseURL = "https://react-example-55161.firebaseio.com/";

class Words extends React.Component {
constructor() {
super();
this.state = {
words: {}
};
}

_get() {
fetch(`${databaseURL}/words.json`).then(res => {
if(res.status != 200) {
throw new Error(res.statusText);
}
return res.json();
}).then(words => this.setState({words: words}));
}

shouldComponentUpdate(nextProps, nextState) {
return nextState.words != this.state.words
}

componentDidMount() {
this._get();
}

render() {
return (
<div>
{Object.keys(this.state.words).map(id => {
const word = this.state.words[id];
return (
<div key={id}>
<Card>
<CardContent>
<Typography color="textSecondary" gutterBottom>
가중치: {word.weight}
</Typography>
<Typography variant="h5" component="h2">
{word.word}
</Typography>
</CardContent>
</Card>
<br/>
</div>
);
})}
</div>

);
}
}

export default Words;


※ 실행 결과 ※


728x90
반응형

728x90
반응형

  가장 먼저 라우팅 기능을 위해 라이브러리를 설치합니다.



▶ ./src/components/Home.js


import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';

class Home extends React.Component {
render() {
return (
<Card>
<CardContent>
React 및 Firebase 기반의 워드 클라우드 프로젝트
</CardContent>
</Card>
);
}
}

export default Home;


▶ ./src/components/Texts.js


import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';

class Texts extends React.Component {
render() {
return (
<Card>
<CardContent>
Texts 페이지
</CardContent>
</Card>
);
}
}

export default Texts;


▶ ./src/components/Words.js


import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';

class Words extends React.Component {
render() {
return (
<Card>
<CardContent>
Words 페이지
</CardContent>
</Card>
);
}
}

export default Words;


▶ ./src/components/App.js


import React from 'react';
import { HashRouter as Router, Route } from 'react-router-dom';
import AppShell from './AppShell';
import Home from './Home';
import Texts from './Texts';
import Words from './Words';

class App extends React.Component {
render() {
return (
<Router>
<AppShell>
<div>
<Route exact path="/" component={Home}/>
<Route exact path="/texts" component={Texts}/>
<Route exact path="/words" component={Words}/>
</div>
</AppShell>
</Router>
);
}
}

export default App;


▶ ./src/components/AppShell.js


import React from 'react';
import { Link as RouterLink } from 'react-router-dom';
import Link from '@material-ui/core/Link';
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>
<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}>
<Link component={RouterLink} to="/">
Home
</Link>
</MenuItem>
<MenuItem onClick={this.handleDrawerToggle}>
<Link component={RouterLink} to="/texts">
Texts
</Link>
</MenuItem>
<MenuItem onClick={this.handleDrawerToggle}>
<Link component={RouterLink} to="/words">
Words
</Link>
</MenuItem>
</Drawer>
</div>
<div id="content" style={{margin: 'auto', marginTop: '20px'}}>
{React.cloneElement(this.props.children)}
</div>
</div>
);
}
}

export default withStyles(styles)(AppShell);


※ 실행 결과 ※





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

  기본적으로 리액트(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
반응형

  이번 시간에는 지난 시간에 설치한 Node.js Express를 활용하여 REST API를 구축하는 방법에 대해서 알아보도록 하겠습니다. REST API는 최근 상당수의 웹 서버 프레임워크에서 기본적으로 지원하는 기능으로 서버와 클라이언트가 웹 프로토콜을 기반으로 하여 효과적으로 데이터를 주고 받을 수 있도록 해줍니다. 이를 실습하기 위해 가장 먼저 기존의 서버 모듈에 전체 고객 목록을 불러오는 API를 구현해보도록 합시다.


▶ server.js


  실제로는 데이터베이스에 있는 고객 정보를 불러오는 형태로 개발이 되어야 합니다. 하지만 일단 다음과 같이 하드코딩을 해봅시다.


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

app.get('/api/customers', (req, res) => {
res.send([
{
'id': 1,
'image': 'https://placeimg.com/64/64/1',
'name': '홍길동',
'birthday': '961222',
'gender': '남자',
'job': '대학생'
},
{
'id': 2,
'image': 'https://placeimg.com/64/64/2',
'name': '나동빈',
'birthday': '960508',
'gender': '남자',
'job': '프로그래머'
},
{
'id': 3,
'image': 'https://placeimg.com/64/64/3',
'name': '이순신',
'birthday': '961127',
'gender': '남자',
'job': '디자이너'
}
]);
});

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


  우리가 만든 /api/customers 경로에 들어가 보면 다음과 같이 고객 목록이 출력됩니다.



  고객 목록 데이터가 정상인지 확인하기 위하여 JSON 검증 서비스를 이용해보도록 합시다.


▶ JSON 검증 서비스: https://jsonlint.com/


  그대로 결과 JSON 문서를 붙여넣기 해보면 다음과 같이 정상적인 데이터라는 것을 확인할 수 있습니다.



  이후에 클라이언트(Client)에서 해당 API에 접근하여 고객 목록을 비동기적으로 가져오도록 만들면 됩니다. 가장 먼저 5000번 포트를 API 서버로 이용하기 위해서 클라이언트(Client)의 package.json 파일에 다음의 문구를 추가하시면 됩니다.


  "proxy": "http://localhost:5000/"


  이제 app.js 파일을 작성하여 실제로 API 서버에 접근할 수 있도록 처리하시면 됩니다.


▶ app.js


  비동기적으로 API 요청 기능을 수행하기 위해서 async - await 구문을 사용했습니다. 서버로부터 JSON 데이터를 받아올 때까지는 테이블에 내용을 출력하지 않다가 데이터를 모두 받아왔을 때 비로소 테이블에 내용을 채우게 됩니다.


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 { withStyles } from '@material-ui/core/styles';

const styles = theme => ({
root: {
width: "100%",
marginTop: theme.spacing.unit * 3,
overflowX: "auto"
},
table: {
minWidth: 1080
}
});

class App extends Component {

state = {
customers: ''
}

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

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

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} />
}) : ''}
</TableBody>
</Table>
</Paper>
);
}
}

export default withStyles(styles)(App);


  이후에 yarn dev 명령어를 이용해 서버를 구동시킬 수 있습니다.



728x90
반응형