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)
NPM을 이용해 npmjs에서 Node.js 라이브러리를 설치
node 프로젝트를 초기화하려면, npm init을 사용 (여기서는 npx라는 툴로 리액트 애플리케이션을 초기화할 예정)
3.1.2 비주얼 스튜디오 코드 설치
3.1.3 프론트엔드 애플리케이션 생성
리액트 애플리케이션 생성
Copy > npx create - react - app todo - react - app
프로젝트 폴더로 이동 및 애플리케이션 실행
Copy > 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 설치
Copy > npm install @ material- ui / core
material-ui/icons 설치
Copy npm install @ material- ui / icons
3.1.5 브라우저의 동작 원리
브라우저 동작 원리
보통 프론트엔드가 있는 웹 서비스의 경우 HTML 파일을 결과로 반환
HTML을 받은 후 텍스트로된 HTML을 브라우저에 보여주기까지
파싱
브라우저는 HTML을 트리 자료 구조 형태인 DOM 트리로 변환
IMAGE, CSS, SCRIPT 등 다운로드해야 하는 리소스를 다운로드 (CSS의 경우 다운로드 후 CSS를 CSSOM 트리로 변환)
다운받은 자바스크립트를 인터프리트, 컴파일, 파싱 및 실행
렌더링
DOM 트리와 CSSOM 트리를 합쳐 렌더 트리를 만듬
트리의 각 노드가 브라우저의 어디에 배치될지, 어떤 크기로 배치될지를 정하는 것
브라우저 스크린에 렌더 트리의 각 노드를 그려줌
위와 같은 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 컴포넌트
컴포넌트는 자바스크립트 함수 또는 자바스크립트 클래스 형태로 생성할 수 있음
함수 컴포넌트 클래스 컴포넌트
Copy import React from 'react' ;
import logo from './logo.svg' ;
import './App.css' ;
function App () {
return (
< div className = "App" >
// JSX code
</ div >
)
}
export default App;
Copy import React from 'react' ;
import logo from './logo.svg' ;
import './App.css' ;
class App extends React . Component {
render () {
return (
< div className = "App" >
//JSX code
</ div >
)
}
export default App;
JSX
React가 한 파일에서 HTML과 자바스크립트를 함께 사용하려고 확장한 자바스크립트 문법
JSX 문법은 Babel이라는 라이브러리가 빌드 시 자바스크립트로 번역해 줌
Copy //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 적용
Todo.js App.js
Copy 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;
Copy import React from "react" ;
import "./App.css" ;
import Todo from "./Todo.js" ;
import { Paper , List } from "@material-ui/core" ;
class App extends React . Component {
constructor (props) {
super (props);
this .state = {
items : [
{ id : 0 , title : "Hello World1 !" , done : true } ,
{ id : 1 , title : "Hello World2 !" , done : false } ,
] ,
};
}
render () {
var todoItems = this . state . items . length > 0 && (
< Paper style = {{ margin : 16 }}>
< List >
{ this . state . items .map ((item , idx) => (
< Todo item = {item} key = { item .id} />
))}
</ List >
</ Paper >
);
return < div className = "App" >{todoItems}</ div >;
}
}
export default App;
3.2.2 Todo 추가
Add 핸들러 추가
onInputChange : 사용자가 input 필드에 키를 하나 입력할 때마다 실행되며 input 필드에 담긴 문자열을 자바스크립트 오브젝트에 저장
onButtonClick : 사용자가 + 버튼을 클릭할 때 실행되며 onInputChange에서 저장하고 있던 문자열을 리스트에 추가
enterKeyEventHandler : 사용자가 input 필드상에서 엔터 또는 리턴키를 눌렀을 때 실행되며 기능은 onButtonClick과 같음
AddTodo.js App.js
Copy 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;
Copy import React from "react" ;
import "./App.css" ;
import Todo from "./Todo.js" ;
import AddTodo from "./AddTodo.js" ;
import {
Paper ,
List ,
Container
} from "@material-ui/core" ;
class App extends React . Component {
constructor (props) {
super (props);
this .state = {
items : [
{ id : 0 , title : "Hello World1 !" , done : true } ,
{ id : 1 , title : "Hello World2 !" , done : false } ,
] ,
};
}
add = (item) => {
const thisItems = this . state .items;
item .js = "ID-" + thisItems . length ;
item .done = false ;
thisItems .push (item);
this .setState ({ items : thisItems });
console .log ( "items: " , this . state .items);
};
render () {
var todoItems = this . state . items . length > 0 && (
< Paper style = {{ margin : 16 }}>
< List >
{ this . state . items .map ((item , idx) => (
< Todo
item = {item}
key = { item .id}
/>
))}
</ List >
</ Paper >
);
return (
< div className = "App" >
< Container maxWidth = "md" >
< AddTodo add = { this .add} />
< div className = "TodoList" >{todoItems}</ div >
</ Container >
</ div >
);
}
}
export default App;
3.2.3. Todo 삭제
Todo.js App.js
Copy 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;
Copy import React from "react" ;
import "./App.css" ;
import Todo from "./Todo.js" ;
import AddTodo from "./AddTodo.js" ;
import {
Paper ,
List ,
Container
} from "@material-ui/core" ;
class App extends React . Component {
constructor (props) {
super (props);
this .state = {
items : [ ] ,
};
}
add = (item) => {
const thisItems = this . state .items;
item .js = "ID-" + thisItems . length ;
item .done = false ;
thisItems .push (item);
this .setState ({ items : thisItems });
console .log ( "items: " , this . state .items);
};
delete = (item) => {
const thisItems = this . state .items;
console .log ( "Before Update Items : " , this . state .items);
const newItems = thisItems .filter (e => e .id !== item .id);
this .setState ({ items : newItems } , () => {
console .log ( "Update Items : " , this . state .items );
});
};
render () {
var todoItems = this . state . items . length > 0 && (
< Paper style = {{ margin : 16 }}>
< List >
{ this . state . items .map ((item , idx) => (
< Todo
item = {item}
key = { item .id}
delete = { this .delete}
/>
))}
</ List >
</ Paper >
);
return (
< div className = "App" >
< Container maxWidth = "md" >
< AddTodo add = { this .add} />
< div className = "TodoList" >{todoItems}</ div >
</ Container >
</ div >
);
}
}
export default App;
3.2.4 Todo 수정
Copy // 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 트리를 구성
Copy // 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 에서 방침 설정을 해줘야 함
Copy 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 응답을 받았다는 사실을 콜백 함수를 통해 알 수 있음
Copy // 콜백 함수를 이용한 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가 실행되는 시점
Copy // 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
url을 매개변수로 받거나 url과 options을 매개변수로 받음
fetch()함수는 Promise 오브젝트를 리턴
then과 catch에 콜백 함수를 전달해 응답을 처리
Copy fetch( "localhost:8080/todo" )
. then (response => {
//respone 수신 시 하고 싶은 작업
})
. catch (e => {
// 에러가 났을 때 하고싶은 작업
})
then에는 응답을 받은 후 실행할 함수 response ⇒ {}를 매개변수로 넘김
catch에는 예외 발생 시 실행할 함수 e ⇒ {}를 넘김
메서드를 명시하고 싶은 경우나 헤더와 바디를 함께 보내야 할 경우에는 아래와 같이 주 번째 매개변수에 요청에 대한 정보가 담긴 오브젝트를 넘겨줌
Copy 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
Copy let backendHost;
const hostname = window && window . location && window . location . hostname ;
if (hostname === "localhost" ) {
backendHost = "<http://localhost:8080>" ;
}
export const API_BASE_URL = `${backendHost}`;
백엔드로 요청을 보낼 때 사용할 유틸리티 함수를 작성
ApiService or
Copy 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;
})
)}
);
}
Copy // 이런 유틸리티 함수가 없다면 반복해야 될 코드
Copy // 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를 이용해 서버 데이터를 업데이트한 후
변경된 내용을 화면에 다시 출력하는 두 가지 작업이 필요
Copy // App 컴포넌트에 update() 함수 구현
update = (item) => {
call( "/todo" , "PUT" , item) . then ((response) =>
this . setState ({ items : response . data })
);
};
Copy // App 컴포넌트에서 Todo의 props에 연결
<Todo
item = {item}
key = { item . id }
delete = { this . delete }
update = { this . update }
/>
Copy // 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 );
};