개인 개발 프로젝트/AI 숫자 판별 앱

[AI 숫자 판별 앱] 6. React 화면 코드 작성

종범2 2019. 12. 29. 18:09

여기서부터는 React를 이용한 화면 코드를 설명한다. 이전 글에서는 client.js까지 설명하였다. client.js에서는 Main.js의 코드를 index.html의 root라는 id를 가진 div 태그에 렌더링 한다. 여기서부터 Main.js부터 Main.js에서 사용하는 컴포넌트를 설명하겠다.

 

Main.js

import React from 'react';
import AppBar from '@material-ui/core/AppBar';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import PredictWithFile from './component/PredictWithFile';
import Guide from './component/Guide';
import Home from './component/Home';
const Main = () => {
	const [value, setValue] = React.useState(0);
	const handleChange = (event, newValue) => {
		setValue(newValue);
	};
	return (
		<div >
			<AppBar position="static">
				<Tabs value={value} onChange={handleChange}>
					<Tab label = "홈"/>
					<Tab label = "가이드라인"/>
					<Tab label = "파일 업로드"/>
				</Tabs>
			</AppBar>
			<Home value={value} index = {0}/>
			<Guide value={value} index = {1}/>
			<PredictWithFile value={value} index = {2}/>
		</div>
	)
}
export default Main;

React 코드는 Hooks를 이용하여 작성하였고 ui는 material-ui에서 제공하는 컴포넌트를 이용하여 구현하였다. Material-ui의 컴포넌트는 다음을 참고하였다.

https://material-ui.com/

 

Material-UI: A popular React UI framework

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

material-ui.com

 

Tabs에는 Tab 3개의 컴포넌트가 위치해있고 Tab을 클릭할 때 Home, Guide, PredictWithFile 셋 중 하나의 컴포넌트를 보여준다. 여기서는 모든 컴포넌트를 렌더링하고 각 컴포넌트에 value라는 state 값과 컴포넌트 고유의 index 값을 props로 전달한다. 각 컴포넌트는 props로 전달받은 value 값과 index값이 일치할 때만 보여주도록 코드를 작성하였다. 초기에 value 값을 0으로 설정했으므로 처음 페이지를 진입하면 Home 컴포넌트만 보여준다.

 

Home.js

import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles(theme => ({
  root: {
    margin: '20px 0 0 20px'
  },
  title: {
    width:'500px',
    fontSize:'20px',
    textAlign:'center',
  },
  content: {
    width:'500px',
    fontSize:'15px',
    margin:'20px 0'
  }
}))

const Home = (props) => {
  const classes = useStyles();
  const {value, index} = props;
	return (
    <div className={classes.root}>
      {value === index &&<div >
        <div className={classes.title}>
          숫자 판별 앱
        </div>
        <div className={classes.content}>
          <li>0 ~ 9 사이의 숫자 손글씨를 보고 숫자를 판별하는 앱입니다.</li>
          <li>Mnist이용한 숫자 판별 AI가 구현되어있습니다.</li>
          <li>python을 이용하여 구현하였습니다.</li>
          <ol>
          <li>가이드라인 탭을 클릭하여 가이드라인을 보고 숫자 손글씨 이미지를 생성하세요.</li>
          <li>파일 업로드 탭을 클릭하여 숫자 이미지를 업로드하세요.</li>
          <li>인공지능이 판별한 숫자와 비교해보세요.</li>
          </ol>
        </div>
      </div>}
    </div>
	)
}
export default Home;

Home에서는 위에서 설명했듯이 value, index를 props로 받는다. value와 index가 일치하는 경우는 모든 내용을 보여주고 아닌 경우에는 비어있는 div 태그만 렌더링 한다. 어플리케이션과 관련된 설명을 보여주는 간단한 컴포넌트이다.

 

Guide.js

import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Stepper from '@material-ui/core/Stepper';
import Step from '@material-ui/core/Step';
import StepLabel from '@material-ui/core/StepLabel';
import Button from '@material-ui/core/Button';
const useStyles = makeStyles(theme => ({
  root: {
    margin: '20px 0 0 20px'
  },
  content: {
    border : '1px solid black',
    margin: '20px 0px 0px 0px',
    width: '500px',
    padding: '10px',
    height:'500px'
  },
}))

const Guide = (props) => {
  const classes = useStyles();
  const { value, index } = props;
  const [activeStep, setActiveStep] = React.useState(0);
  const getSteps = () => {
    return ['그림판 열기', '크기 조절', '숫자 그리기','이미지 저장'];
  }
  const steps = getSteps();
  const getStepContent = (stepIndex) =>{
    switch (stepIndex) {
      case 0:
        return '윈도우 기본 앱 그림판을 실행합니다.';
      case 1:
        return '[홈] - [크기 조정]을 클릭합니다.\n[기준]에서 [픽셀]을 클릭합니다.\n가로와 세로를 28*28로 설정합니다.';
      case 2:
        return '자유롭게 숫자를 그립니다.';
      case 3:
        return '[파일] - [저장]을 클릭합니다.\n그림을 저장합니다.';
      default:
        return '오류';
    }
  }
  const handleNext = () => {
    setActiveStep(prevActiveStep => prevActiveStep + 1);
  };
  const handleBack = () => {
    setActiveStep(prevActiveStep => prevActiveStep - 1);
  };
  const handleReset = () => {
    setActiveStep(0);
  };
  return (
    <div className={classes.root}>
      {value === index && <div >
        <Stepper style={{padding: '0px', width: '500px'}} activeStep={activeStep} alternativeLabel>
          {steps.map(label => (
            <Step key={label}>
              <StepLabel>{label}</StepLabel>
            </Step>
          ))}
        </Stepper>
        <div>
          {activeStep === steps.length ? (
            <div>
              <div className={classes.content}>파일 업로드 탭을 클릭하여 파일을 업로드하세요!</div>
              <Button  style={{width: '100px', marginTop: '20px'}} onClick={handleReset}>다시 보기</Button>
            </div>
          ) : (
              <div>
                <div className={classes.content}>
                  {getStepContent(activeStep).split('\n').map(line=>{
                    return <div>{line}<br/></div>
                  })}
                </div>
                <div style={{marginTop:'20px'}}>
                  <Button disabled={activeStep === 0} onClick={handleBack} style={{width: '100px', marginRight: '20px'}}>
                    이전
                  </Button>
                  <Button variant="contained" color="primary" onClick={handleNext} style={{width: '100px'}}>
                    {activeStep === steps.length - 1 ? '왼료' : '다음'}
                  </Button>
                </div>
              </div>
            )}
        </div>
      </div>}
    </div>
  )
}
export default Guide;

Guide.js에서는 숫자 판별 앱을 사용하는 방법을 설명한다. Home.js와 동일하게 value와 index값이 같은 경우만 내용을 보여준다. 단순히 내용을 보여주는 기능이지만 단계별로 설명하기 위해 material-ui의 Stepper 컴포넌트를 이용하였다.

 

Stepper 컴포넌트는 모든 단계를 보여주고 현재 단계에 색을 표시하여 보여준다. 현재 단계에 색을 표시하기 위해 activeStep라는 state값을 props로 전달한다. 또한 내부에는 모든 단계를 표시하기 위해 Step 컴포넌트를 작성한다. Steps라는 변수에는 각 단계의 이름으로 구성된 배열을 저장하고, 배열에 반복문을 이용하여 Step 컴포넌트를 작성한다.

 

내용은 마지막 단계인 경우와 아닌 경우로 나누어 작성하였다. 마지막 단계인 경우에는 이전, 다음이라는 버튼이 아닌 다시 보기라는 버튼이 존재해야 하므로 조건문으로 작성하였다.

 

PredictWithFile.js

import React from 'react';
import axios from 'axios';
import Button from '@material-ui/core/Button';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles(theme => ({
  root: {
		margin: '20px 0 0 20px',
		display: 'grid'
  },
  title: {
    width:'500px',
    fontSize:'20px',
    textAlign:'center',
  },
  content: {
    width:'500px',
    fontSize:'15px',
  }
}))
const predictWithFile = (props) => {
	const { value, index } = props;
	const [data, setData] = React.useState('');
	const [result,setResult] = React.useState('');
	const classes = useStyles();
	const handleFileInput = (e) => {
		// Init
		setData(e.target.files[0]);
		let file = e.target.files[0];
		let reader = new FileReader();
		// Read File
		if (file) {
			reader.readAsDataURL(file);
		}
		// Draw
		reader.onload = (e) => {
			let image = document.querySelector('#img');
			image.src = e.target.result;
		}
	}
	const handleSubmit = () => {
		const formData = new FormData();
		formData.append('file', data);
		return axios.post("https://jb-mnist-server.herokuapp.com/number", formData, {
			headers: {
				mode: 'no-cors',
				'Access-Control-Allow-Origin': '*'
			}
		}).then(res => {
			alert('결과는 ' + res.data +' 입니다.');
			setResult(res.data);
			console.log(res);
		}).catch(err => {
			alert('실패했습니다.')
			console.log(err);
		})
	}
	return (
		<div >
			{value === index && <div className={classes.root}>
				<div className={classes.content}>파일은 반드시 28*28 크기의 이미지 파일이어야합니다.</div>
				<div style={{height:'500px', width: '500px', border: '1px black solid', marginTop: '20px',  }} >
					<img id="img" style={{width:'500px'}}/>
				</div>
				<div style={{margin:'20px 0 0 0 '}}>
					<div className={classes.content}>결과 : {result}</div>
				</div>
				<div style={{margin:'20px 0 0 0 '}}>
					<input style={{ display: "none" }} type="file" id="file" name="file" onChange={handleFileInput} accept="image/*" />
					<Button style={{ height: '35px', width: '380px', marginRight: '20px' }} variant="contained">
						<label for="file" style={{ width: '100%' }}>파일 선택</label>
					</Button>
					<Button style={{ height: '35px', width: '100px' }} variant="contained" color="primary" onClick={handleSubmit}>
						제출
					</Button>
				</div>
			</div>}
		</div>
	)
}
export default predictWithFile;

마지막으로 이전에 만든 API를 호출하는 화면이다. 파일 선택이라는 버튼을 클릭하면 파일 선택 화면 팝업이 뜨고 파일을 선택하면 해당 이미지를 보여준다. type이 file인 input 태그를 이용하였다. 디자인을 위해 input 태그는 숨김 처리하고 파일 선택이라는 버튼을 대신 보여주도록 label을 이용하였다. label 태그의 for 속성으로 input 태그의 id를 설정하였으므로 input 태그의 클릭 기능을 파일 선택이라는 버튼도 같이 수행하게 된다. input 태그는 숨겼으므로 사실상 input 태그의 기능을 Button으로 대체하였다고 생각해도 된다. input 태그의 파일이 변하게 되면 handleFileInput이라는 함수를 실행한다. handleFileInput 함수는 파일을 읽어 img 태그에 이미지를 표시한다. 또한 data라는 state에 해당 파일을 저장한다.

 

제출이라는 버튼을 클릭하면 handleSubmit이라는 함수를 실행한다. handleSubmit 함수는 data라는 state에 저장된 이미지를 file이라는 파라매터에 저장하고 jb-mnist-server.herokuapp.com/number 라는 url로 post로 요청한다. 그리고 결과를 result라는 state에 저장하고 alert 창에 결과를 보여준다.

 

여기까지 코드를 작성하고 다음과 같이 명령어를 입력하여 어플리케이션을 실행하여 http://localhost:8080/ 에서 확인한다.

 npm run dev

각 화면과 결과는 다음과 같다.

 

홈 화면

 

가이드라인 화면

 

파일 업로드 화면 및 결과

 

API를 heroku에 배포했듯이 React 웹 어플리케이션도 배포를 해야 한다. 다음 글에서는 어플리케이션을 heroku에 배포하는 과정을 설명한다.

 

다음 글 바로가기

[AI 숫자 판별 앱] 7. React 앱 Heroku에 배포