본문 바로가기
Programming

스프링 부트 SpringBoot 웹 애플리케이션 개발 #4 프론트엔드 구현하기

by 하하호호 2022. 4. 1.
반응형

 

 

스프링 부트 웹 애플리케이션 개발하기 시리즈


 

 

SpringBoot 웹 애플리케이션 개발 #1 프로젝트 시작

웹서버 기본 개념 서버란 사용자가 요청하는 응답을 반환하기 위해 무한정 대기하는 프로그램이다. 서버는 정적 웹 서버와 동적 웹서버로 구분되는데, 정적 웹서버는 사용자의 요청에 기계적인

incomeplus.tistory.com

 

 

스프링 부트 SpringBoot 웹 애플리케이션 개발 #2 백엔드 개발

백엔드 서비스 아키텍처란? 레이어드 아키텍처 패턴은 스프링 프로젝트를 분리해서 사용자와 서버 프로그램이 유기적인 소통이 가능하도록 한다. 프로젝트 내부에서 어떻게 코드를 관리할 것

incomeplus.tistory.com

 

 

스프링 부트 SpringBoot 웹 애플리케이션 개발 #3 CRUD 구현하기

CRUD는 웹 서비스의 가장 기본적인 서비스인 생성(Create), 검색(Retrieve), 수정(Update), 삭제(Delete)의 약자다. 사용자의 요청에 따라서 Controller 로직은 URI를 매핑하게 되고, Service 로직에서는 Reposit..

incomeplus.tistory.com

 

웹 애플리케이션 개발 #4 프론트엔드 구현


서버 구성이 완료되면 사용자가 직접적으로 이용하는 화면인 프론트엔드를 구현한다. 사용자의 애플리케이션 로직을 수행하여 백엔드 서버로 요청을 보내고 응답을 받게 된다. 사용자가 웹 서비스를 이용하는 프로그램은 웹 브라우저다. Chrome, Whale, Safari 등의 웹 브라우저는 HTML, CSS, javascript 등의 자원을 서버에 요청하고 사용자에게 렌더링 하는 프로그램이다.

 

Node.js(feat React.js)

프론트엔드는 React.js를 사용한다. React.js는 Node.js라는 런타임 환경위에서 작동하는 프레임워크다. Node.js는 구글 크롬의 V8 javascript 엔진을 실행하면서 브라우저 뿐만 아니라 로컬 환경에서 javascript를 작동시킨다. 브라우저 밖에서 작동하는 javascript는 이제 프론트엔드 뿐만 아니라 서버 프로그램도 개발이 가능하다.

 

NPM(Node Package Manager)

Node.js의 패키지 관리 시스템이다. NPM을 이용해서 Node.js 라이브러리들을 다운로드 할 수 있다. 

 

NPM 버전 확인

$ npm version
{
  npm: '8.5.3',
  node: '16.14.0',
  v8: '9.4.146.24-node.20',
  uv: '1.43.0',
  zlib: '1.2.11',
  brotli: '1.0.9',
  ares: '1.18.1',
  modules: '93',
  nghttp2: '1.45.1',
  napi: '8',
  llhttp: '6.0.4',
  openssl: '1.1.1m+quic',
  cldr: '40.0',
  icu: '70.1',
  tz: '2021a3',
  unicode: '14.0',
  ngtcp2: '0.1.0-DEV',
  nghttp3: '0.1.0-DEV'
}

 

npm 프로젝트 초기화

$ mkdir demo-project

$ cd demo-project

$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (demo-project) 
version: (1.0.0) 
description: demo-project
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: Developer Blog
license: (ISC) 
About to write to /home/project/SpringBoot_project/02_demo/demo-project/package.json:

{
  "name": "demo-project",
  "version": "1.0.0",
  "description": "demo-project",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Developer Blog",
  "license": "ISC"
}


Is this OK? (yes) yes
npm notice 
npm notice New minor version of npm available! 8.5.3 -> 8.6.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v8.6.0
npm notice Run npm install -g npm@8.6.0 to update!
npm notice

 

npm을 init하면 package.json 파일이 생성된다. 이 안에는 프로젝트에 관한 이름, 버전, 설명, 저자 등의 메타데이터가 담기게 된다. 이제 프론트엔드를 작성하기 위해 react 패키지를 설치한다.

 

$ npm install react

added 3 packages, and audited 4 packages in 1s

found 0 vulnerabilities

 

react 패키지를 설치하면 node_moduls 디렉터리가 생성되고 이 안에 react 패키지가 설치된다. 또한 package.json을 다시 열어보면 설치한 패키지가 명시된다. dependencies에 추가된 패키지는 이후 서비스 배포에 사용될 수 있도록 준비가 완료된 것이다.

drwxrwxr-x 3 ys ys 4096  4월  1 17:50 .
drwxrwxr-x 4 ys ys 4096  4월  1 17:45 ..
drwxrwxr-x 6 ys ys 4096  4월  1 17:50 node_modules
-rw-rw-r-- 1 ys ys  282  4월  1 17:50 package.json
-rw-rw-r-- 1 ys ys 2208  4월  1 17:50 package-lock.json

 

package.json파일 내 dependencies 항목아래에 18.0.0 버전의 react가 설치되어 추가되었다.

{
  "name": "demo-project",
  "version": "1.0.0",
  "description": "demo-project",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Developer Blog",
  "license": "ISC",
  "dependencies": {
    "react": "^18.0.0"
  }
}

 

react 프로젝트 생성

node.js 및 react 설치가 완료되면 본격적으로 프로젝트를 생성한다. 먼저 react-workspace 디렉토리를 생성하고 react 프로젝트를 생성한다. react 프로젝트 디렉토리가 생성된다. 디렉토리를 살펴보면 npm을 init했을 때와 동일하게 node_modules, package.json, pckage-lock.json이 자동생성되어 있다. 이제 워킹 디렉토리를 옮겨서 react 프로젝트를 실행시켜준다. 

$ mkdir react-workspace

$ cd react-workspace

$ npx create-react-app demo-react-app

Creating a new React app in /home/project/SpringBoot_project/02_demo/demo-project/react-workspace/demo-react-app.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...


added 1370 packages in 52s

169 packages are looking for funding
  run `npm fund` for details

Initialized a git repository.

Installing template dependencies using npm...
npm WARN deprecated source-map-resolve@0.6.0: See https://github.com/lydell/source-map-resolve#deprecated

added 38 packages in 5s

169 packages are looking for funding
  run `npm fund` for details
Removing template package using npm...


removed 1 package, and audited 1408 packages in 4s

169 packages are looking for funding
  run `npm fund` for details

6 moderate severity vulnerabilities

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.

Created git commit.

Success! Created demo-react-app at /home/project/SpringBoot_project/02_demo/demo-project/react-workspace/demo-react-app
Inside that directory, you can run several commands:

  npm start
    Starts the development server.

  npm run build
    Bundles the app into static files for production.

  npm test
    Starts the test runner.

  npm run eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:

  cd demo-react-app
  npm start

Happy hacking!



$ cd demo-react-app

$ npm start

 

자동으로 웹 브라우저가 실행되고 http://localhost:3000/ 주소로 이동하게 된다. React.js의 첫 페이지는 아래의 모습이다.

 

VSCode에서 File > 'Add Folder To Workspace'을 클릭해서 미리 생성한 React 프로젝트 디렉토리 'demo-react-app'을 선택한다.

 

package.json

프로젝트의 메타데이터다. Node.js 패키지 목록이 담긴다.

 

public Directory

public 디렉토리 내 index.html는 가장 먼저 리턴되는 HTML 파일이다. 자세히 살펴보면 react 프로젝트 디렉토리 내 HTML파일은 index.html 파일 하나 밖에 없다. 실질적으로 클라이언트에게 렌더링 되는 페이지는 모두 index.html 내의 root Element 아래에 동적으로 처리되는 것이다.

 

src Directory

src 디렉토리 내의 index.js는 index.html과 함께 react 프로젝트에서 가장 먼저 참조하는 파일이다. index.js내의 코드를 통해 index.html의 root Element에 동적으로 리액트 컴포넌트들이 추가된다. App.js는 react 프로젝트를 생성할 때 기본적으로 생성되는 리액트 컴포넌트다. 

 


 

 

 

 

 

 

UI 패키지 설치


react.js에서 CSS파일을 수동으로 생성해서 관리할 필요가 없다. material-ui 패키지를 사용하면 UI위한 컴포넌트를 자동으로 생성 및 관리해준다.

 

@material-ui/core 설치

$ npm install @material-ui/core

 

@material-ui/icons 설치

$ npm install @material-ui/icons

 

 

 

프론트엔드 서버 작동원리?


SPA(Single Page Application)와 ReactDOM 프론트엔드 서버를 기반으로 작동한다. SPA는 웹 페이지가 한번 로딩 된 이후 사용자가 특별히 새로 로딩을 하지 않는 이상 한개의 페이지에서 작동하는 애플리케이션이다. 즉, HTML 파일을 요청하지 않으면서 Ajax, fetch등의 js 함수를 사용해서 데이터를 주고받는 클라이언트-사이드 렌더링(Client-Side Rendering)을 통해 페이지 기능을 구현하게 된다.

 

브라우저가 작동하는 방식은 클라이언트가 특정 URL을 입력(HTTP GET을 서버에 요청)하면 프론트엔드 서버는 HTML파일을 반환한다. HTML을 응답받은 브라우저에서는 먼저 파싱을 하고 이후 렌더링을 해서 실제 페이지를 구현하여 클라이언트에게 보여주게 된다.

 

파싱(parsing)이란 렌더링을 하기 위한 전처리 작업이다. 파싱단계에서 브라우저는 먼저 HTML을 DOM(Domain Object Model)로 변환한다. 이후 정적 리소스인 js, css, image 파일들을 다운로드 한다. 다운로드 된 CSS 파일은 CSSOM(CSS Object Model) 트리 형태로 변환한다. js는 인터프리트, 컴파일, 파싱, 실행 작업을 거치게 된다. 

 

전처리 작업인 파싱이 완료되면 브라우저는 DOM과 CSSOM을 합쳐 렌더 트리를 생성한다. 이를 기반으로 레이아웃을 정하고, 브라우저에 렌더 트리를 렌더링하게 된다. 최종적으로 사용자는 화면에 버튼, 텍스트, 이미지, 동적인 js 기능이 포함된 웹 브라우저를 사용할 수 있게 되는 것이다.

출처 : 위키백과 DOM

 

 

React.js란?


프론트엔드 대표적인 프레임워크로는 Vue.js, Angular.js, React.js가 있다. 공식적인 React.js 튜토리얼은 https://ko.reactjs.org/docs/getting-started.html을 참조하자.

 

React Component

index.html의 root Element에 최초로 렌더링 되는 React Component는 index.js가 렌더링하는 App.js다. 이게 React Component다. Component는 함수형과 클래스형 모두 지원한다. 

 

함수형 컴포넌트

import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      .
      .
      .
    </div>
  );
}

export default App;

 

클래스형 컴포넌트

import React from 'react';
import logo from './logo.svg';
import './App.css';

class App extends React.Component{
	render(){
    	return(

            <div className="App">
              .
              .
              .
            </div>
		)
   }
}

export default App;

 

 

두가지 형태 모두 js 파일 내에서 js와 HTML을 모두 사용하는 JSX 문법을 사용한다. React Component는 JSX를 리턴하게 되고 Babel 라이브러리에서 빌드 후 자바스크립트로 번역하게 된다. 

 

이제 React Component가 실제로 렌더링 되는 장면을 살펴보자. 최초로 렌더링에 참여하는 index.js 코드를 살펴보면 ReactDOM.render() 메소드를 찾을 수 있다. 매개변수로는 React Component와 index.html의 root Element를 받고 있다. 즉 React Component(JSX 형식의 데이터)를 받아서 index.html의 root아래에 렌더링 작업을 하는 작업이 이곳이다. React Component를 사용하기 위해서 App Component를 import 하고 있다. 

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

 


 

 

 

 

본격적인 프론트엔드 개발


새로운 React Component를 만들어서 렌더링 해보는 작업을 해본다. TodoList를 관리할 수 있는 기본적인 프로그램을 만들면서 컴포넌트 생성, 컴포넌트 삭제, EventHandler, props, state등을 이용한 프론트엔드 서버가 작동하는 방식과 UI를 설계하는 방법을 살펴본다.

 

Demo.js 작성

h1태그와 체크박스와 라벨 Element를 가진 간단한 Component를 작성한다. React Component 맨 마지막에는 export 키워드를 사용해서 다른 Component에서 사용할 수 있도록 반환해준다.

import React from 'react';

class Demo extends React.Component{
    render(){
        return(
            <div className='Demo'>
                <h1>Developer Blog</h1>
                <input type="text" id="demo_02"/>
                <br/>
                <input type="checkbox" id="demo_01" name="demo_01" value="todo_01"/>
                <label for="demo_01">Todo List Basic</label>
            </div>
        );
    }
}

export default Demo;

 

App.js 수정

div태그 아래에 직접 작성한 Demo React Component를 추가해준다. npm start를 해서 프레임워크를 다시 실행해주거나, 브라우저를 리로딩하면 페이지가 변경되는 것을 확인할 수 있다.

import logo from './logo.svg';
import Demo from './Demo.js';
import './App.css';


function App() {
  return (
    <div className="App">
      <Demo />
    </div>
  );
}

export default App;

 

 

 

 

Props + State

사용자의 입력에 따라서 동적인 웹 애플리케이션을 만들기 위해서는 매개변수를 받아와야 한다. React Component에서 매개변수를 받기 위해서는 Constructor를 통해서 매개변수를 Component를 넘겨줘야 한다. state는 리액트에서 자체적으로 관리하는 오브젝트다. 자바스크립트에서 변경한 값을 HTML로 다시 렌더링 하기 위해서차후 변경될 수 있는 부분을 따로 관리하게 된다. super()를 통해 props 오브젝트를 초기화 한다. 

 

Demo.js Component

import React from 'react';

class Demo extends React.Component{

    // 사용자 변수를 받기 위한 생성자
    constructor(props){
        super(props);
        this.state = {item: props.item}

    }

    render(){
        return(
            <div className='Demo'>
                <h1>{this.state.item.title}</h1>
                <br/>
                <input type="checkbox" id={this.state.item.id} name={this.state.item.id} value={this.state.item.done}/>
                <label for={this.state.item.id}>Todo List Basic</label>
            </div>
        );
    }
}

export default Demo;

 

App.js Component

App Component에서도 동일하게 생성자를 만들어주고, Demo Component로 전달할 변수를 담은 state를 초기화 한다. Demo Component를 호출하는 코드에 매개변수를 추가해준다.

import logo from './logo.svg';
import Demo from './Demo.js';
import './App.css';
import React from 'react';


class App extends React.Component{

  // 매개변수를 설정하기 위한 생성자
  constructor(props){
  	// props 초기화
    super(props)
    // state 오브젝트 초기화
    this.state = {
      item : {id:0, title:"Developer Blog", done:true},
    }

  }
  render(){
    return (
      <div className="App">
      	// Demo Component에 변수값 전달
        <Demo item={this.state.item} />
        
      </div>
    );
  }
  
}

export default App;

 

Component를 추가하면서 다른 변수를 매개변수로 던져줄 수 있다. Dictionary 형태로 state의 변수를 추가하면 Component별 다른 변수를 전달할 수 있다.

class App extends React.Component{
  constructor(props){
    super(props)
    this.state = {
      item : {id:0, title:"Developer Blog", done:true},
      item2 : {id:1, title:"React Project", done:false},
    }

  }
  render(){
    return (
      <div className="App">
        <Demo item={this.state.item} />
        <Demo2 item={this.state.item2} />
      </div>
    );
  }
  
}

 

map() 함수 사용

Component를 반복해서 작성하는 방법을 대신하여 반복문을 가지고 코드를 리팩토링 할 수 있다. map()함수를 이용해서 this.state를 초기화한 items 배열을 불러와서 Demo Component의 'item'과 'key'값에 매개변수를 던져준다. 반환값에는 {demoItems}를 리턴한다.

constructor(props){
    super(props)
    this.state = {
      items : [
        {id:0, title:"Developer Blog", done:true},
        {id:1, title:"React Project", done:false}
      ]
    }
  }
render(){
    var demoItems = this.state.items.map((item, idx) => (
      <Demo item={item} key={item.id} />
    ))

    return (
      <div className="App">      
        {demoItems}
      </div>
    );
  }

 

material-ui 사용

사용자가 직접 대면하는 UI를 작성하는데 CSS 코드를 작성할 필요는 없다. material-ui 또한 Component기 때문에 불러와서 코드를 추가만 해주면 된다. 아래 예시 코드에서 사용하는 material-ui Component는 ListItem, ListItemText, InputBase, CheckBox를 사용한다.

 

Demo.js Component

import React from 'react';
import { ListItem, ListItemText, InputBase, Checkbox } from "@material-ui/core";

class Demo extends React.Component{

    // 사용자 변수를 받기 위한 생성자
    constructor(props){
        super(props);
        this.state = {
            item: props.item,
            item2: props.item2,
        }

    }

    render(){
        return(
            <div className='Demo'>
                <ListItem>
                    <Checkbox checked={this.state.item.done} />
                    <ListItemText>
                        <InputBase 
                            inputProps={{ "arial-label" : "naked" }}
                            type = "text"
                            id={this.state.item.id} 
                            name={this.state.item.id} 
                            value={this.state.item.title}
                            multiline={true}
                            fullWidth={true}
                            />
                    </ListItemText>
                </ListItem>
            </div>
        );
    }
}

export default Demo;

 

App.js Component

App Component에서 사용하는 @material-ui Component는 Paper, List를 사용한다. 매개변수로 전달받는 items가 존재하면 margin 100의 속성을 가진 Paper를 만들고 아래에 ul태그를 생성하는 List Component를 만들어 Demo Component를 리스팅한다.

import logo from './logo.svg';
import Demo from './Demo.js';
import './App.css';
import React from 'react';
import { Paper, List } from "@material-ui/core";


class App extends React.Component{
  constructor(props){
    super(props)
    this.state = {
      items : [
        {id:0, title:"Developer Blog", done:true},
        {id:1, title:"React Project", done:false}
      ]
    }
  }
  render(){
    var demoItems = this.state.items.length > 0 && (
      <Paper style = {{margin:100}}>
        <List>
          {this.state.items.map((item, idx) => (
            <Demo item={item} key={item.id} />
          ))}
        </List>
      </Paper>
    )
    return (
      <div className="App">      
        {demoItems}
      </div>
    );
  }
  
}

export default App;

 

 

 

Todo List 추가 기능 구현

위에서 구현한 기능은 미리 입력 해놓은 Todo List를 출력하는 기능만 가지고 있다. 이제 Input과 Button으로 구성된 새로운 Component를 추가하고, 실제로 입력 후 버튼을 클릭했을 때 아래의 TodoList에 아이템들이 나열되는 기능을 구현해보자. 먼저 Button과 Input으로 구성된 Component를 생성해야 한다.

 

DemoTodoList.js 생성

새로운 컴포넌트는 TextField, Paper, Button, Grid 컴포넌트를 import 한다. 새로 가져온 Component들을 가지고 입력과 버튼으로 구성된 UI를 새롭게 구성한다.

import React from 'react';
import { TextField, Paper, Button, Grid } from '@material-ui/core';

class DemoTodoList extends React.Component{
    constructor(props){
        super(props);
        this.state = props.item;
    }

    render(){

        return(
            <Paper style={{margin:100 , marginRight:200 , marginLeft:200, padding : 50}}>
                <Grid container>
                    <Grid xs={11} md={11} item style={{paddingRight : 20}}>
                        <TextField placeholder='Add new Todo List' fullWidth />
                    </Grid>
                    <Grid xs={1} md={1} item>
                    <Button fullWidth color="primary" variant="outlined" >
                        +
                    </Button>
                </Grid>
                </Grid>
                
            </Paper>
        )
    }
}

export default DemoTodoList;

 

App.js에서 DemoTodoList Component 추가

먼저 DemoTodoList를 import 한 후 {demoItems} 윗 부분에 Component를 추가해주면 UI가 정상적으로 반영되는 것을 확인할 수 있다.

import logo from './logo.svg';
import Demo from './Demo.js';
import './App.css';
import React from 'react';
import { Paper, List } from "@material-ui/core";
import DemoTodoList from './DemoTodoList.js';



class App extends React.Component{
  constructor(props){
    super(props)
    this.state = {
      items : [
        {id:0, title:"Developer Blog", done:true},
        {id:1, title:"React Project", done:false}
      ]
    }
  }
  render(){
    var demoItems = this.state.items.length > 0 && (
      <Paper style = {{margin:100 , marginRight:200 , marginLeft:200}}>
        <List>
          {this.state.items.map((item, idx) => (
            <Demo item={item} key={item.id} />
          ))}
        </List>
      </Paper>
    )
    return (
      <div className="App">      
        <DemoTodoList />
        {demoItems}
        
      </div>
    );
  }
  
}

export default App;

 

새로운 Component가 적용된 UI


 

 

 

 

EventHandler 추가


사용자가 사용하는 측면에서 생각했을 때, 구현해야 할 EventHandler는 총 3가지다. 사용자가 사용하는 순서는 먼저 Input에 TodoList를 입력하고, Button을 클릭하게 된다. 혹은 Enter Key를 입력해서 TodoList를 반영하고자 한다. 구현해야 할 EventHandler 기능은 총 3가지가 되는 것이다. 여기에 삭제 기능까지 추가한다.

  • onInputChange : Input에 값이 입력될 때 마다 자바스크립트 오브젝트에 저장된다.
  • onButtonClick : '+' 버튼을 클릭할 때 마다 저장된 값이 리스트에 추가된다.
  • enterKeyEventHandler : Input값을 입력 후 Enter Key를 입력했을 때 저장된 값이 리스트에 추가된다.
  • delete : Delete Button 클릭시 item을 delete 한다.

 

onInputChange 기능 구현

생성자의 state 오브젝트는 사용자가 입력한 값으로 동적으로 변하기 때문에 초기값을 ""로 만들어 준다. onInputChange에 익명함수를 구현해서 할당해주고, TextField 'onChange' 속성값에 앞에서 정의한 onInputChange를 할당해준다. value 값에는 사용자가 입력한 값인 'this.state.item.title'을 할당한다.

 

onInputChange 함수를 살펴보면 Event 오브젝트 'e'를 익명함수에 매개변수로 보낸다. TextField Component에서 이벤트가 발생할 때 마다 onChange() 속성이 작동한다. 사용자가 입력한 값은 'e.target.value'에 저장되어 있다. 자바스크립트에서 value값을 가져오는 일반적이 방법이다.

 

 

[Web Dev] javascript value 가져오는 방법

javascript에서 HTML의 value값을 가져오는 방법은 간단하다. document.querySelector('#value_id').value document.getElementById('#value_id').value document.getElementByClass('.value_class').value 이런식..

incomeplus.tistory.com

 

DemoTodoList.js

class DemoTodoList extends React.Component{
    constructor(props){
        super(props);
        this.state = {
            item : {
                title : ""
            },
        };
    }

    onInputChange = (e) => {
        const thisItem = this.state.item;
        thisItem.title = e.target.value;
        this.setState({item : thisItem});
        console.log(this.state.item.title);
    }

    render(){

        return(
            <Paper style={{margin:100 , marginRight:200 , marginLeft:200, padding : 50}}>
                <Grid container>
                    <Grid xs={11} md={11} item style={{paddingRight : 20}}>
                        <TextField placeholder='Add new Todo List' fullWidth onChange={this.onInputChange} value={this.state.item.title} />
                    </Grid>
                    <Grid xs={1} md={1} item>
                    <Button fullWidth color="primary" variant="outlined" >
                        +
                    </Button>
                </Grid>
                </Grid>
                
            </Paper>
        )
    }
}

export default DemoTodoList;

 

개발자 화면에서 모니터링을 하면 Input에 값이 입력될 때 마다 해당 value가 반환되는 것을 확인할 수 있다.

onInputChange 작동 결과

 

onButtonClick 기능 구현

하위 Component인 DemoTodoLIst에서 App Component로 add함수를 구현해서 보낼 수는 없다. 반대로 App.js에서 add함수를 구현한다. 구현된 add함수를 매개변수로 보내서 DemoTodoList의 props로 받아와 onButtonClick함수를 구현한다. 마지막으로 Button 태그에 onClick 속성값에 onButtonClick을 추가해주면 로직이 완성된다.

 

Add.js

Event 오브젝트 'item'을 받아서 id와 done을 초기화 한다. 현재 state 오브젝트에 생성된 item을 추가해준다. DemoTodoList Component를 렌더링 하는 로직에 add 함수를 매개변수로 보내준다.

import logo from './logo.svg';
import Demo from './Demo.js';
import './App.css';
import React from 'react';
import { Paper, List } from "@material-ui/core";
import DemoTodoList from './DemoTodoList.js';



class App extends React.Component{
  constructor(props){
    super(props)
    this.state = {
      items : [
        {id:0, title:"Developer Blog", done:true},
        {id:1, title:"React Project", done:false}
      ]
    }
  }

  // add 함수 구현
  add = (item) => {
    const thisItems = this.state.items;
    // Event Object 초기화
    item.id = "ID-"+thisItems.length;
    item.done = false;
    // state 오프젝트에 추가
    thisItems.push(item);
    // 추가된 Event 오브젝트를 state로 초기화
    this.setState({items : thisItems})
    console.log("items : ", this.state.items);

  }


  render(){
    var demoItems = this.state.items.length > 0 && (
      <Paper style = {{margin:100 , marginRight:200 , marginLeft:200}}>
        <List>
          {this.state.items.map((item, idx) => (
            <Demo item={item} key={item.id} />
          ))}
        </List>
      </Paper>
    )
    return (
      <div className="App">   
      // 매개변수로 add 넘겨줌
        <DemoTodoList add={this.add} />
        {demoItems}
        
      </div>
    );
  }
  
}

export default App;

 

DemoTodoList.js

item 오브젝트의 'title'은 onInputChange에서 이미 초기화를 했기 때문에 item을 add함수의 매개변수로 보내주고, 다시 item을 초기화 한다. 마지막으로 Button 태그의 onClick 함수에 구현된 onButtonClick을 할당한다.

import React from 'react';
import { TextField, Paper, Button, Grid } from '@material-ui/core';


class DemoTodoList extends React.Component{
    constructor(props){
        super(props);
        this.state = {
            item : {
                title : ""
            },
        };
        // Add.js에서 add() 함수를 props로 넘겨받는다.
        this.add = props.add;
    }

    onInputChange = (e) => {
        const thisItem = this.state.item;
        thisItem.title = e.target.value;
        this.setState({item : thisItem});
        console.log(this.state.item.title);
    }

	// onButtonClick 함수 구현
    onButtonClick = () => {
        this.add(this.state.item);
        this.setState({item : {title : ""}});
    }

    render(){

        return(
            <Paper style={{margin:100 , marginRight:200 , marginLeft:200, padding : 50}}>
                <Grid container>
                    <Grid xs={11} md={11} item style={{paddingRight : 20}}>
                        <TextField placeholder='Add new Todo List' fullWidth onChange={this.onInputChange} value={this.state.item.title} />
                    </Grid>
                    <Grid xs={1} md={1} item>
                    <Button fullWidth color="primary" variant="outlined" onClick={this.onButtonClick}>
                        +
                    </Button>
                </Grid>
                </Grid>
                
            </Paper>
        )
    }
}

export default DemoTodoList;

 

onButtonClick UI 확인

Input 값을 입력 후 Add Button을 입력하면 정상적으로 TodoList에 값이 추가 되는 것을 확인할 수 있다.

 

enterKeyEventHandler 구현

사용자가 Input 값을 입력한 후 Button을 클릭하는 것이 아니라 'Enter'를 눌렀을 때 EventHandler가 작동한다. 기본적인 로직은 onButtonClick 함수와 동일하다. 만약 입력된 KEY가 'Enter'이면 onButtonClick 함수를 실행한다. 또한 TextField 태그에 onKeyDown 속성에 'this.enterKeyEventHandler'를 추가해준다. onKeyPress는 더 이상 사용되지 않기 때문에 'onkeyDown'을 사용한다.

import React from 'react';
import { TextField, Paper, Button, Grid } from '@material-ui/core';


class DemoTodoList extends React.Component{
    constructor(props){
        super(props);
        this.state = {
            item : {
                title : ""
            },
        };
        // Add.js에서 add() 함수를 props로 넘겨받는다.
        this.add = props.add;
    }

    onInputChange = (e) => {
        const thisItem = this.state.item;
        thisItem.title = e.target.value;
        this.setState({item : thisItem});
        console.log(this.state.item.title);
    }

    onButtonClick = () => {
        this.add(this.state.item);
        this.setState({item : {title : ""}});
    }

	// onButtonClick과 동일한 로직의 함수
    enterKeyEventHandler = (e) =>{
        if(e.key == 'Enter'){
            this.onButtonClick();
        }
    }

    render(){

        return(
            <Paper style={{margin:100 , marginRight:200 , marginLeft:200, padding : 50}}>
                <Grid container>
                    <Grid xs={11} md={11} item style={{paddingRight : 20}}>
                        <TextField placeholder='Add new Todo List' fullWidth onChange={this.onInputChange} value={this.state.item.title} onKeyDown={this.enterKeyEventHandler} />
                    </Grid>
                    <Grid xs={1} md={1} item>
                    <Button 
                    	fullWidth 
                        color="primary" 
                        variant="outlined" 
                        // Enter가 입력됐을 때 작동한다.
                        onClick={this.onButtonClick}>
                        +
                    </Button>
                </Grid>
                </Grid>
                
            </Paper>
        )
    }
}

export default DemoTodoList;

 

delete 기능 구현

삭제 기능을 구현하기 위해서 먼저 Button UI구성한다. 삭제는 @material-ui에서 제공하는 ListItemSecondaryAction, IconButton 컴포넌트를 사용한다. 

 

컴포넌트를 삭제하는 기능은 item.id 값으로 해당 컴포넌트를 식별한 후 '휴지통 Icon'을 클릭했을 때 deleteEventHandler 함수를 작동시킨다. App.js에서 구현한 delete() 함수를 Demo.js에 매개변수로 넘겨준다.

 

매개변수로 Deomo.js에서 App.js로 넘겨진 item을 제외한 나머지 items들만 filter()함수를 통해 추출해내고, 남은 items들로 현재의 this.state.items를 초기화 한다.

 

App.js

import logo from './logo.svg';
import Demo from './Demo.js';
import './App.css';
import React from 'react';
import { Paper, List } from "@material-ui/core";
import DemoTodoList from './DemoTodoList.js';



class App extends React.Component{
  constructor(props){
    super(props)
    this.state = {
      items : [
        {id:0, title:"Developer Blog", done:true},
        {id:1, title:"React Project", done:false}
      ]
    }
  }

  add = (item) => {
    const thisItems = this.state.items;
    // Event Object 초기화
    item.id = "ID-"+thisItems.length;
    item.done = false;
    // state 오프젝트에 추가
    thisItems.push(item);
    // 추가된 Event 오브젝트를 state로 초기화
    this.setState({items : thisItems})
    console.log("items : ", this.state.items);

  }

  delete = (item) => {
    // item에서 item.id로 해당 컴포넌트를 검색한다.
    const thisItems = this.state.items;
    const newItems = this.state.items.filter( e => e.id !== item.id);
    // 새로 만들어진 items로 state를 초기화한다.
    this.setState({items : newItems});
  }


  render(){
    var demoItems = this.state.items.length > 0 && (
      <Paper style = {{margin:100 , marginRight:200 , marginLeft:200}}>
        <List>
          {this.state.items.map((item, idx) => (
            <Demo item={item} key={item.id} delete={this.delete} />
          ))}
        </List>
      </Paper>
    )
    return (
      <div className="App">      
        <DemoTodoList add={this.add} />
        {demoItems}
        
      </div>
    );
  }
  
}

export default App;

 

Demo.js

props를 통해서 App.delete()를 전달받아, deleteEventHandler를 완성한다. 휴지통이 위치한 <IconButton>태그에 'onClick' 속성값을 'this.deleteEventHandler'로 초기화해준다. 

import React from 'react';
import { ListItem, ListItemText, InputBase, Checkbox, ListItemSecondaryAction, IconButton } from "@material-ui/core";
import DeleteOutlined from '@material-ui/icons/DeleteOutlined';

class Demo extends React.Component{

    // 사용자 변수를 받기 위한 생성자
    constructor(props){
        super(props);
        this.state = {
            item: props.item,
            item2: props.item2,
        }
        // App.js로 delete 함수를 전달받는다.
        this.delete = props.delete;
    }

    // delete() 함수를 작동시킬 EventHandler
    deleteEventHandler = () => {
        this.delete(this.state.item);
    }

    render(){
        return(
            <div className='Demo'>
                <ListItem>
                    <Checkbox checked={this.state.item.done}  disableRipple/>
                    <ListItemText>
                        <InputBase 
                            inputProps={{ "arial-label" : "naked" }}
                            type = "text"
                            id={this.state.item.id} 
                            name={this.state.item.id} 
                            value={this.state.item.title}
                            multiline={true}
                            fullWidth={true}
                            />
                    </ListItemText>
                    <ListItemSecondaryAction>
                        {/* 휴지통 모양을 클릭했을 때, deleteEventHandler가 작동한다. */}
                        <IconButton aria-label='Delete Todo List' onClick={this.deleteEventHandler}>
                            <DeleteOutlined/>
                        </IconButton>
                    </ListItemSecondaryAction>
                </ListItem>
            </div>
        );
    }
}

export default Demo;

 

Update 기능 구현

아이템을 수정되는 부분은 체크박스와 타이틀 변경 두가지 경우가 있다. 먼저 타이틀을 변경하는 경우 타이틀을 클릭 했을 때, readOnly 플래그를 false로 변경한다. <InputBase> 컴포넌트의 속성으로 inputProps에 readOnly 속성을 추가해준다. 수정 후 Enter키를 누르면 <InputBase>의InputProps.readOnly가 true로 변경되면서 수정불가 상태가 된다. 체크박스를 클릭 시 현재값을 반전하면서 값을 변경한다.

 

Demo.js

props에 새롭게 추가된 readOnly 플래그는 수정모드를 의미한다. 타이틀을 클릭하면 수정모드로 변하면서 입력된 값을 타이틀로 초기화 하고 엔터를 쳐서 수정모드를 종료한다. 체크박스는 클릭 할 때 마다 현재값과 반대값으로 변경된다. Demo.js에서 업데이트를 위해 사용된 함수는 총 4가지다.

 

  • offReadOnlyMode() : Input 컴포넌트의 onClick 속성과 연결되며 클릭 시 수정모드가 된다.
  • enterKeyEventHandler() :  Input 컴포넌트의 onKeyDown과 연결되며, 수정모드에서 Enter를 누르면 수정모드 종료
  • editEventHandler() : Input 컴포넌트의 onChange 속성과 연결, 타이틀 값을 변경한다.
  • checkBoxEventHandler() : checkBox 컴포넌트의 onClick 속성과 연결, 현재값과 반대값으로 초기화한다.
import React from 'react';
import { ListItem, ListItemText, InputBase, Checkbox, ListItemSecondaryAction, IconButton } from "@material-ui/core";
import DeleteOutlined from '@material-ui/icons/DeleteOutlined';

class Demo extends React.Component{

    // 사용자 변수를 받기 위한 생성자
    constructor(props){
        super(props);
        this.state = {
            item: props.item,
            // Update를 위해 readOnly 변수 추가
            readOnly : true,
        }
        // App.js로 delete 함수를 전달받는다.
        this.delete = props.delete;
    }

    // delete() 함수를 작동시킬 EventHandler
    deleteEventHandler = () => {
        this.delete(this.state.item);
    }
    // 수정 모드 전환
    offReadOnlyMode = () => {
        this.setState({readOnly : false});
    }

    // Enter시 수정 모드 종료
    enterKeyEventHandler = (e) => {
        if(e.key == 'Enter'){
            this.setState({readOnly : true})
        }
    }

    // 수정 모드에서 새로운 타이틀을 저장한다.
    editEventHandler = (e) => {
        const thisItem = this.state.item;
        thisItem.title = e.target.value;
        this.setState({item : thisItem});
    }

    // 클릭시 체크박스 값 반전
    checkboxEventHandler = (e) => {
        const thisItem = this.state.item;
        thisItem.done = !thisItem.done;
        this.setState({item : thisItem});
    }

    render(){
        return(
            <div className='Demo'>
                <ListItem>
                    {/* 클릭시 체크박스의 값은 반전된다. */}
                    <Checkbox checked={this.state.item.done}  disableRipple onClick={this.checkboxEventHandler}/>
                    <ListItemText>
                        <InputBase 
                            inputProps={{
                                 "arial-label" : "naked",
                                 // readOnly 플래그 추가
                                 readOnly : this.state.readOnly,
                                }}
                            type = "text"
                            id={this.state.item.id} 
                            name={this.state.item.id} 
                            value={this.state.item.title}
                            multiline={true}
                            fullWidth={true}
                            // 클릭 시 수정 모드 전환
                            onClick = {this.offReadOnlyMode}
                            // 입력시 타이틀 변경 시작
                            onChange = {this.editEventHandler}
                            // Enter 키 누를 경우 수정 모드 종료
                            onKeyDown = {this.enterKeyEventHandler}
                            />
                    </ListItemText>
                    <ListItemSecondaryAction>
                        {/* 휴지통 모양을 클릭했을 때, deleteEventHandler가 작동한다. */}
                        <IconButton aria-label='Delete Todo List' onClick={this.deleteEventHandler}>
                            <DeleteOutlined/>
                        </IconButton>
                    </ListItemSecondaryAction>
                </ListItem>
            </div>
        );
    }
}

export default Demo;

 

웹 브라우저를 오픈해서 UI를 확인하면 업데이트 기능이 정상적으로 작동하는 것을 확인할 수 있다.


 

 

 

 

 

 

 

더 많은 콘텐츠

 

 

SpringBoot란? 시작시 주의사항 알아야 하는 개념들

스프링 부트 시작할 때 주의사항 Java를 기반으로 한 웹 프레임워크다. 오픈소스 기반 프레임워크다. SpringBoot는 IoC 컨테이너를 가진다. IoC(Inversion of Control, 제어의 역)이란 개발자가 인스턴스를

incomeplus.tistory.com

 

반응형

댓글