3 프론트엔드 개발

3.1 프론트엔드 개발환경 설정

3.1.1 Node.js 와 NPM 설치

  • 프론트엔드

    • 사용자 바로 앞에서 사용자와 상호작용

    • 애플리케이션 로직을 수행하고 백엔드 서버로 요청을 보냄

    • 웹 서비스에서 클라이언트 혹은 프론트엔드란 웹 브라우저를 뜻함

      • 사용자는 자신의 컴퓨터에서 브라우저 실행

      • 브라우저는 인터넷을 이용해 서버에 있는 자원(HTML, JavaScript, CSS)을 사용자의 컴퓨터로 다운로드 및 실행

  • Node.js

    • Node.js가 등장하기 전까지 자바스크립트는 브라우저 내에서만 실행 가능

      • 즉, JS는 브라우저상에서 HTML 렌더링의 일부로 실행하거나 개발자창의 자바스크립트 콘솔을 이용해 실행

    • Node.js는 자바스크립트를 컴퓨터에서 실행할 수 있게 해주는 JS 런타임 환경

      • 이는 곧 JS를 클라이언트 언어뿐 아니라 서버 언어로도 사용할 수 있다는 뜻

    • Node.js는 구글 크롬의 V8 자바스크립트 엔진을 실행

  • NPM (Node Package Manager)

    • Node.js의 패키지 관리 시스템

    • NPM을 이용해 npmjs에서 Node.js 라이브러리를 설치

    • Node.js를 설치하면 함께 설치

node 프로젝트를 초기화하려면, npm init을 사용 (여기서는 npx라는 툴로 리액트 애플리케이션을 초기화할 예정)

3.1.2 비주얼 스튜디오 코드 설치

3.1.3 프론트엔드 애플리케이션 생성

리액트 애플리케이션 생성

>npx create-react-app todo-react-app

프로젝트 폴더로 이동 및 애플리케이션 실행

>cd todo-react-app

> npm start
  • 비주얼 스튜디오 코드에서 개발 환경 설정

    • 비주얼 스튜디오 코드에 워크스페이스 생성

      • File → Add Foler to Workspace

  • create-react-app 기본 생성 파일

    • package.json

      • 프로젝트의 메타데이터로 사용할 node.js 패키지 목록 등을 포함

    • public 디렉터리 아래 파일

      • index.html

        • http://localhost:3000이 가장 처음으로 리턴하는 HTML 파일

        • 리액트에서 html 파일은 index.html 하나 밖에 없음

        • 다른 페이지들은 React.js를 통해 생성되고 index.html에 있는 root 엘리먼트 아래에 동적으로 랜더링

    • src 디렉터리 아래의 파일

      • index.js는 index.html과 함께 가장 처음으로 실행되는 JS 코드

      • 이 자바스크립트 코드가 리액트 컴포넌트를 root 아래에 추가

      • App.js는 기본으로 생성된 리액트 컴포넌트

3.1.4 material-ui 패키지 설치

material-ui/core 설치

> npm install @material-ui/core

material-ui/icons 설치

npm install @material-ui/icons

3.1.5 브라우저의 동작 원리

  • 브라우저 동작 원리

    • 브라우저 주소창에 https://google.com 같은 웹 주소를 입력하면

    • 브라우저는 HTTP GET 요청을https://google.com의 서버로 전송

    • 보통 프론트엔드가 있는 웹 서비스의 경우 HTML 파일을 결과로 반환

  • HTML을 받은 후 텍스트로된 HTML을 브라우저에 보여주기까지

    1. 파싱

      1. 브라우저는 HTML을 트리 자료 구조 형태인 DOM 트리로 변환

      2. IMAGE, CSS, SCRIPT 등 다운로드해야 하는 리소스를 다운로드 (CSS의 경우 다운로드 후 CSS를 CSSOM 트리로 변환)

      3. 다운받은 자바스크립트를 인터프리트, 컴파일, 파싱 및 실행

    2. 렌더링

      1. DOM 트리와 CSSOM 트리를 합쳐 렌더 트리를 만듬

        • 내용인 DOM과 디자인인 CSSOM을 합침

        • 트리의 각 노드가 브라우저의 어디에 배치될지, 어떤 크기로 배치될지를 정하는 것

      2. 브라우저 스크린에 렌더 트리의 각 노드를 그려줌

위와 같은 2 단계를 거치면 사용자는 브라우저상에서 시각화된 HTML 파일을 볼 수 있게 됨

개발자툴 → 디버깅창 : HTML Elements, CS, 자바스크립트 콘솔, 네트워크 탭, Application 탭

3.1.6 React.js

  • SPA (Single Page Application)

    • 웹 페이지를 로딩하면 사용자가 임의로 새로 고침하지 않는 이상 페이지를 새로 로딩하지 않는 애플리케이션

    • 서버에게 새 HTML 페이지를 요청하지 않고 자바스크립트가 동적으로 HTML을 재구성

    • 브라우저의 자바스크립트는 fetch, ajax 등의 함수로 서버에 데이터를 요청하고 받은 데이터를 이용해 자바스크립트 내에서 HTML 재구성

  • React.js 첫 화면

    • 하얀 화면 : index.html 로딩중

      • HTML이 <body></body>를 렌더링하다보면 마지막에 bundle.js라는 스크립트 로딩

      • bundle.js는 npm start를 실행했을 떄 만들어지는 스크립트인데, index.js를 포함

      • index.js의 일부로 ReactDom.render() 함수가 실행

      • render() 함수가 동적으로 HTML 엘리먼트를 리액트 첫 화면으로 바꿔줌

      • 이떄 렌더링된 하위 엘리먼트는 render() 함수의 매개변수로 들어가는 <App /> 부분

  • React 컴포넌트

    • 컴포넌트는 자바스크립트 함수 또는 자바스크립트 클래스 형태로 생성할 수 있음

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

function App() {
    return (
        <div className="App">
          // JSX code
         </div>
    )
}

export default App;

  • JSX

    • React가 한 파일에서 HTML과 자바스크립트를 함께 사용하려고 확장한 자바스크립트 문법

    • JSX 문법은 Babel이라는 라이브러리가 빌드 시 자바스크립트로 번역해 줌

//index.js App 렌더링
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import reportWebVitals from "./reportWebVitals";
import AppRouter from "./AppRouter";

ReactDOM.render(
  <React.StrictMode>
    <AppRouter />
  </React.StrictMode>,
  document.getElementById("root")
);

ReacrDOM은 매개변수로 받은 <App /> 컴포넌트를 이용해 DOM트리를 만들고, 이때 컴포넌트의 render() 함수가 반환한 JSX를 렌더링 함

  • import를 이용해 App 컴포넌트를 불러옴

  • <컴포넌트이름 />을 이용해 컴포넌트를 사용

  • ReactDOM.render

    • 첫 번째 매개변수로 리액트 컴포넌트를 받음

    • 두 번째 매개변수로는 root 엘리먼트를 받음

      • root 엘리먼트에 첫 번째 매개변수로 넘겨진 리액트 컴포넌트를 root 엘리먼트 아래에 추가하라는 뜻

      • root 엘리먼트는 index.html에 정의되어 있음

3.2 프론트엔드 서비스 개발

⚠️ HTML Mock을 우선적으로 작성하고 UI에 들어가게 될 가짜 데이터 작성

3.2.1 Todo List

  • Props와 State 적용, material-ui 적용

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

class Todo extends React.Component {
  constructor(props) {
    super(props);
    this.state = { item: props.item };
  }
  render() {
    const item = this.state.item;
    return (
      <ListItem>
        <Checkbox checked={item.done} />
        <ListItemText>
          <InputBase
            inputProps={{ "arial-label": "naked" }}
            type="text"
            id={item.id}
            name={item.id}
            value={item.title}
            multiline={true}
            fullWidth={true}
          />
        </ListItemText>
      </ListItem>
    );
  }
}

export default Todo;

3.2.2 Todo 추가

  • Add 핸들러 추가

    • onInputChange : 사용자가 input 필드에 키를 하나 입력할 때마다 실행되며 input 필드에 담긴 문자열을 자바스크립트 오브젝트에 저장

    • onButtonClick : 사용자가 + 버튼을 클릭할 때 실행되며 onInputChange에서 저장하고 있던 문자열을 리스트에 추가

    • enterKeyEventHandler : 사용자가 input 필드상에서 엔터 또는 리턴키를 눌렀을 때 실행되며 기능은 onButtonClick과 같음

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

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

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

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

  enterKeyEventHandler = (e) => {
    if (e.key === "Enter") {
      this.onButtonClick();
    }
  };

  render() {
    return (
      <Paper style={{ margin: 16, padding: 16 }}>
        <Grid container>
          <Grid xs={11} md={11} item style={{ paddingRight: 16 }}>
            <TextField
              placeholder="Add Todo here"
              fullWidth
              onChange={this.onInputChange}
              value={this.state.item.title}
              onKeyPress={this.enterKeyEventHandler}
            />
          </Grid>
          <Grid xs={1} md={1} item>
            <Button
              fullWidth
              color="secondary"
              variant="outlined"
              onClick={this.onButtonClick}
            >
              +
            </Button>
          </Grid>
        </Grid>
      </Paper>
    );
  }
}

export default AddTodo;

3.2.3. Todo 삭제

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

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

  deleteEventHandler = () => {
    this.delete(this.state.item);
  };

  checkboxEventHandler = (e) => {
    const thisItem = this.state.item;
    thisItem.done = !thisItem.done;
    this.setState({ item: thisItem });
    this.update(this.state.item);
  };

  offReadOnlyMode = () => {
    console.log("Event !!", this.state.readOnly);
    this.setState({ readOnly: false }, () => {
      console.log("ReadOnly? ", this.state.readOnly);
    });
  };

  enterKeyEventHandler = (e) => {
    if (e.key === "Enter") {
      this.setState({ readOnly: true });
      this.update(this.state.item);
    }
  };

  editEventHandler = (e) => {
    const thisItem = this.state.item;
    thisItem.title = e.target.value;
    this.setState({ item: thisItem });
  };
  render() {
    const item = this.state.item;
    return (
      <ListItem>
        <Checkbox checked={item.done} onChange={this.checkboxEventHandler} />
        <ListItemText>
          <InputBase
            inputProps={{
              "arial-label": "naked",
              readOnly: this.state.readOnly,
            }}
            onClick={this.offReadOnlyMode}
            onKeyPress={this.enterKeyEventHandler}
            onChange={this.editEventHandler}
            type="text"
            id={item.id}
            name={item.id}
            value={item.title}
            multiline={true}
            fullWidth={true}
          />
        </ListItemText>
        <ListItemSecondaryAction>
          <IconButton
            arial-label="Delete Todo"
            onClick={this.deleteEventHandler}
          >
            <DeleteOutlined />
          </IconButton>
        </ListItemSecondaryAction>
      </ListItem>
    );
  }
}

export default Todo;

3.2.4 Todo 수정

  1. 체크박스에 체크하는 경우

  2. 타이틀을 변경하고 싶은 경우

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

class Todo extends React.Component {
  constructor(props) {
    super(props);
    this.state = { item: props.item, readOnly: true };
    this.delete = props.delete;
  }

  deleteEventHandler = () => {
    this.delete(this.state.item);
  };

  checkboxEventHandler = (e) => {
    const thisItem = this.state.item;
    thisItem.done = !thisItem.done;
    this.setState({ item: thisItem });
    this.update(this.state.item);
  };

  offReadOnlyMode = () => {
    console.log("Event !!", this.state.readOnly);
    this.setState({ readOnly: false }, () => {
      console.log("ReadOnly? ", this.state.readOnly);
    });
  };

  enterKeyEventHandler = (e) => {
    if (e.key === "Enter") {
      this.setState({ readOnly: true });
      this.update(this.state.item);
    }
  };

  editEventHandler = (e) => {
    const thisItem = this.state.item;
    thisItem.title = e.target.value;
    this.setState({ item: thisItem });
  };
  
  render() {
    const item = this.state.item;
    return (
      <ListItem>
        <Checkbox checked={item.done} onChange={this.checkboxEventHandler} />
        <ListItemText>
          <InputBase
            inputProps={{
              "arial-label": "naked",
              readOnly: this.state.readOnly,
            }}
            onClick={this.offReadOnlyMode}
            onKeyPress={this.enterKeyEventHandler}
            onChange={this.editEventHandler}
            type="text"
            id={item.id}
            name={item.id}
            value={item.title}
            multiline={true}
            fullWidth={true}
          />
        </ListItemText>
        <ListItemSecondaryAction>
          <IconButton
            arial-label="Delete Todo"
            onClick={this.deleteEventHandler}
          >
            <DeleteOutlined />
          </IconButton>
        </ListItemSecondaryAction>
      </ListItem>
    );
  }
}

export default Todo;

3.3 서비스 통합

3.4.1 componentDidMount

  • 렌더링 과정

    • 리액트는 브라우저에 보이는 HTML DOM 트리의 다른 버전인 ReactDOM(Virtual DOM)을 갖고 있음

    • 어떤 이유에서든 컴포넌트의 상태(state)가 변하면 ReactDOM은 이를 감지하고 변경된 부분의 HTML을 바꿔줌

    • HTML이 업데이트되면 우리는 변경된 결과를 눈으로 확인

  • 마운팅 과정

    • 랜더링이 맨 처음 일어나는 순간,

    • 즉 ReactDOM 트리가 존재하지 않는 상태에서

    • 리액트가 처음으로 각 컴포넌트의 render() 함수를 콜해 자긴의 DOM 트리를 구성

// App.js
componentDidMount() {
    const requestOptions = {
        method: "GET",
        headers: { "Content-Type": "application/json" },
    };
    
    fetch("htpp://localhost:8080/todo", requestOptions)
        .then((response) => response.json())
        .then(
            (response) => {
                this.setState({
                    items: response.data,
                });
            },
            (error) => {
                this.setState({
                    error,
                });
            }
        );
}

CORS 에러 메세지 뜸

3.4.2 CORS

  • CORS (Cross-Origin Resource Sharing)

    • 처음 리소스를 제공한 도메인이 현재 요청하려는 도메인과 다르더라도 요청을 허락해 주는 웹 보안 방침

    • CORS가 가능하려면 백엔드에서 CORS 에서 방침 설정을 해줘야 함

package com.todoweb.todoSpringApp.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    private final long MAX_AGE_SECS = 3600;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("<http://localhost:3000>")
                .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(MAX_AGE_SECS);
    }
}

3.4.3 fetch

  • 비동기 오퍼레이션

    • 만약 내가 HTTP 요청을 백엔드에 보냈는데 백엔드가 이를 처리하는 데 1분이 걸리면 내 브라우저는 1분간 아무것도 못하는 상태가 됨

  • 콜백 함수

    • 오퍼레이션이 현재 실행 중인 자바스크립트 스레드가 아니라 드른 곳에서 실행된다면, HTTP 응답을 받았다는 사실을 콜백 함수를 통해 알 수 있음

// 콜백 함수를 이용한 XMLHttpRequest
var oReq = new XMLHttpRuest();
oReq.open("GET", "<http://localhost:8080/todo>");
oReq.onload = function () {
	console.log(oReq.response);
};
oReq.send();
  • 콜백 지옥

    • XMLHttpRequest를 사용하는 경우 콜백 함수 내에서 또 다른 HTTP 요청을 하고 그 두 번째 요청을 위한 콜백을 또 정의하는 과정에서 코드가 굉장히 복잡해짐.

  • Promise

    • 콜백 지옥을 피할 수 있음

    • 비동기 오퍼레이션에서 사용

    • 이 함수를 실행한 후 Promise 오브젝트에 명시된 사항들을 실행시켜 주겠다는 약속

    • 세 가지 상태, Pending, Resolve, Reject가 있음

      • pending : 오퍼레이션이 끝나길 기다리는 상태

      • resolve : 오퍼레이션이 성공적으로 끝남. 이때 resolve는 then의 매개변수로 넘어오는 함수를 실행

      • reject : 오퍼레이션 중 에러가 나는 경우. 그 결과 catch 매개변수로 넘어오는 함수가 실행

    • them이나 catch로 넘기는 함수들은 지금 당장 실행되는 것은 아님

      • 실제 함수들이 실행되는 시점은 resolve와 reject가 실행되는 시점

// Promise를 사용한 XMLHttpRequest
function exampleFunction() {
	return new Promise((resolve, reject) => {
		var oReq = new XMLHttpRequest();
		oRequ.open("GET", "<http://localhost:8080/todo>");
		oReq.onload = function () {
			resolve(oReq.response);
		};
		oReq.onerror = fuction () {
			reject(oReq.response);
		};
		oReq.send();
	});
}

exampleFunction()
	.then((r) => console.log("Resolved " + r))
	.catch((e) => console.log("Rejectd " + e));
  • Fetch API

    • API 서버로 http 요청을 송신 및 수신

    • 자바스크립트가 제공하는 메서드

    • url을 매개변수로 받거나 url과 options을 매개변수로 받음

    • fetch()함수는 Promise 오브젝트를 리턴

    • then과 catch에 콜백 함수를 전달해 응답을 처리

fetch("localhost:8080/todo")
	.then(response => {
		//respone 수신 시 하고 싶은 작업
	})
	.catch(e => {
		// 에러가 났을 때 하고싶은 작업
  })
  • fetch는 첫번째 매개변수로 uri를 받음

  • 디폴트로 GET 메서드를 사용

  • then에는 응답을 받은 후 실행할 함수 response ⇒ {}를 매개변수로 넘김

  • catch에는 예외 발생 시 실행할 함수 e ⇒ {}를 넘김

  • 메서드를 명시하고 싶은 경우나 헤더와 바디를 함께 보내야 할 경우에는 아래와 같이 주 번째 매개변수에 요청에 대한 정보가 담긴 오브젝트를 넘겨줌

options = {
	method: "POST",
	headers: [
		["Content-Type", "application/json"]
	],
	body: JSON.stringify(data)
};

fetch("localhost:8080/todo", options)
	.then(response => {
		// response 수신 시 하고싶은 작업
	})
	.catch(e => {
	
	})
  • 하드코딩된 “localhost:8080/todo” 리팩터링

src/api-config.js

let backendHost;

const hostname = window && window.location && window.location.hostname;

if (hostname === "localhost") {
	backendHost = "<http://localhost:8080>";
}

export const API_BASE_URL = `${backendHost}`;
  • 백엔드로 요청을 보낼 때 사용할 유틸리티 함수를 작성

import { API_BASE_URL } from "../app-config";

export function call(api, method, request) {

  options = {
    headers: new Headers({
    "Content-Type": "application/json",
  }),
    url: API_BASE_URL + api,
    method: method,
  };
  if (request) {
    // GET method
    options.body = JSON.stringify(request);
  }

  return fetch(options.url, options)
    .then((response) =>
      response.json().then((json) => {
        if (!response.ok) {
          // response.ok가 true이면 정상적인 리스폰스를 받은것, 아니면 에러 리스폰스를 받은것.
          return Promise.reject(json);
        }
        return json;
      })
    )}
  );
}

// App컴포너트에서 ApiService 사용
import { call } from "./service/ApiService.js";

/* 기존 코드 */ 

componentDidMount() {
    call("/todo", "GET", null).then((response) =>
      this.setState({ items: response.data, loading: false })
    );
  }

  add = (item) => {
    call("/todo", "POST", item).then((response) =>
      this.setState({ items: response.data })
    );
  };

  delete = (item) => {
    call("/todo", "DELETE", item).then((response) =>
      this.setState({ items: response.data })
    );
  };

Todo Update 수정

  • 프론트엔드 UI 부분을 위한 mock() 함수에서는 사용자가 키보드에서 입력하면 editEventHandler 함수가 title을 수정해 줌

  • 따로 update() 함수를 App.js에 만들지 않아도 잘 동작

  • 하지만 API를 이용해 update하려면 Serve API를 이용해 서버 데이터를 업데이트한 후

  • 변경된 내용을 화면에 다시 출력하는 두 가지 작업이 필요

// App 컴포넌트에 update() 함수 구현
update = (item) => {
    call("/todo", "PUT", item).then((response) =>
      this.setState({ items: response.data })
    );
  };
// App 컴포넌트에서 Todo의 props에 연결
<Todo 
	item={item}
  key={item.id}
  delete={this.delete}
  update={this.update}
/>
// Todo 컴포넌트에서 update 연결 및 사용
constructor(props) {
    super(props);
    this.state = { item: props.item, readOnly: true };
    this.delete = props.delete;
    this.update = props.update;
  }

enterKeyEventHandler = (e) => {
    if (e.key === "Enter") {
      this.setState({ readOnly: true });
      this.update(this.state.item);
    }
  };

checkboxEventHandler = (e) => {
    const thisItem = this.state.item;
    thisItem.done = !thisItem.done;
    this.setState({ item: thisItem });
    this.update(this.state.item);
  };

Last updated