스프링 부트 웹 애플리케이션 개발 시리즈
Mounting, CORS
렌더링이란 ReactDOM에서 컴포넌트 변경이 감지되면 HTML로 변경해주고, 웹 브라우저에서는 변경된 컴포넌트를 실제로 확인할 수 있다. 이게 렌더링이다. 그럼 마운트는 뭐냐? 렌더링이 되는 시점에 ReactDOM은 처음 render() 함수를 호출해서 자신의 ReactDOM을 구성하는 것을 마운트라고 한다.
componentDidMount()
리액트는 JDX를 통해 컴포넌트를 파싱하고 ReactDOM을 만들어 렌더링 하는 과정 까지 일련이 '생성 주기'를 가진다. 컴포넌트가 최초 시작되는 지점이 Mount라고 말한다. context, defaultProps, state를 저장하게 되고 componentWillMount 메소드를 호출한다. 이후 render() 메소를 호출해서 컴포넌트들을 DOM에 올리게 되고 componentDidMount()메소드가 호출되면서 직접적인 컴포넌트 접근 작업이 가능하게 되는 것이다.
마운트는 생성자(contructor)와 render()함수를 호출하면서 시작되고, 종료시점에 componentDidMount() 함수를 호출하여 ReactDOM 구성을 마친다. 이 componentDidMount() 함수에 백엔드 API 콜을 구현하게 된다. 마운팅이 모두 완료된 시점에서야 컴포넌트들의 프로퍼티가 완성되기 때문에 프로그램의 안정성을 위해서라도 API 콜 구현은 가장 마지막 부분에서 호출하는게 맞다.
백엔드와 프론트엔드를 통합하는 과정에서 또 하나 해결해야 하는 문제가 있다. 바로 CORS 문제다. CORS(Cross-Origin Resource Sharing)란 최초 리소스를 제공한 도메인(Origin)이 현재 요청하는 도메인과 다르다 하더라도 그 요청을 허락해주는 웹 보안 지침이다.
CORS 문제 해결
웹 브라우저 입장에서 localhost:3000에서 컴포넌트를 생성하고 삭제하는 등의 기능을 프론트엔드 서버로 부터 응답을 받았다. 즉, 리액트 프론트엔드 서버는 현재 페이지를 받은 서버(현재 페이지의 Origin)라는 말이다. 하지만 여기서 스프링 백엔드 서버에 요청을 보내게 되면 CORS 문제가 발생하게 되는 것이다.
스프링 백엔드 서버 프로그램에서 CORS 방침 설정을 해주지 않으면 403 Error Code가 반환되고, cross-origin 이슈가 발생한다.
스프링부트에서 메인 디렉토리 내 'config' 디렉토리를 생성 후 CORS 방침을 설정하기 위한 클래스를 생성한다. WebMvcConfigurer 인터페이스의 'addCorsMappings' 메소드를 구현해준다.
WebMvcConfig
package com.example.damo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// 스프링 빈으로 등록/관리
@Configuration
//WebMvcConfigurer 인터페이스 상속
public class WebMvcConfig implements WebMvcConfigurer {
private final long MAX_AGE_SECS = 3600;
@Override
public void addCorsMappings(CorsRegistry registry){
// 모든 경로들에 대해서 CORS 방침을 설정한다.
registry.addMapping("/**")
// Origin이 'http:localhost:3000'인 경우
.allowedOrigins("http://localhost:3000")
// 허용할 METHODS 나열
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(MAX_AGE_SECS);
}
}
localhost:3000으로 다시 접속해서 웹 브라우저 개발자도구의 'Network' 탭에서 CORS 문제가 해결된 것을 확인할 수 있다.
JavaScript fetch()
JavaScript Promise
JavaScript의 fetch() 함수는 promise를 리턴한다. 그럼 promise는 뭘까? Promise는 JavaScript에서 비동기 오퍼레이션이 가능하도록 지원하는 도구다. JavaScript는 기본적으로 싱글 스레드에서 작동하는 프로그램이기 때문에, 백엔드에서 리턴하는데 까지 걸린 시간 만큼 웹 브라우저의 작동이 멈춘다는 뜻이다. 이 문제점을 극복하기 위해서 비동기 오퍼레이션을 사용하게 된다.
만약 Promise가 없다면 서버에 요청한 리소스들이 정상적으로 응답받은 사실을 알기 위해서 콜백 함수를 정의해줘야 하고, 이 콜백 함수 내에서 또 다른 HTTP 요청이 발생할 경우 또 다른 콜백 함수를 정의해줘야 하는 콜백 지옥(Calback Hell) 딜레마에 빠지게 된다. Promise를 사용하면 콜백 지옥을 해결하면서 Promise 오브젝트에 정의된 사항을 실행시켜 주게 되는 것이다.
Promise는 크게 Pending, Resolve, Reject 3가지 상태로 구분된다. pending은 오퍼레이션이 끝날 때 까지 대기하게 되고, 오퍼레이션이 성공적으로 완료되면 resolve() 함수는 then의 매개변수로 넘어오는 함수를 실행해 원하는 값을 전달하게 된다. 만약 에러가 나는 경우에는 reject() 함수를 실행하고 catch 매개변수로 넘어오는 함수가 실행된다.
fetch()
JavaScript의 fetch() 함수는 Promise 오브젝트를 리턴하고 있다. 덕분에 API 서버에 HTTP 요청을 보내고 응답을 받을 때 사용된다. then과 catch에 콜백 함수를 전달하면서 응답을 처리하게 되는 것이다.
fetch() 함수를 통해 TodoList를 생성하고 삭제하는 과정을 살펴보자. 먼저 리액트 프로그램에서 App.js의 add() 함수를 수정한다. 기존의 로직은 프론트엔드 서버에서 작동하는 방식이므로, 백엔드와 연결하기 위해 "http://localhost:8080/crud" URL로 요청을 보내고 응답 데이터를 수신하여 this.setState()함수를 사용해 this.state.items로 재설정한다.
App.js add() 함수
add = (item) => {
const requestOptions = {
method : "POST",
headers : {'Content-Type' : 'application/json'},
body : JSON.stringify(item),
};
fetch('http://localhost:8080/crud', requestOptions)
.then(res => res.json())
.then(response=>{
console.log(response.data);
this.setState({items : response.data});
});
}
App.js delete() 함수
TodoList를 삭제하는 로직도 add() 함수와 동일하다. 먼저 "http://localhost:8080/crud"로 특정 id를 담은 item을 직렬화한 데이터를 매개변수로 서버에 요청을 보낸다. 이내 응답받은 데이터를 this.setState() 함수를 사용해서 this.state.items를 재설정한다.
delete = (item) => {
const requestOptions = {
method : "DELETE",
headers : {'Content-Type' : 'application/json'},
body : JSON.stringify(item)
};
fetch('http://localhost:8080/crud', requestOptions)
.then(res => res.json())
.then(response => {
this.setState({items : response.data});
})
}
프론트엔드 서버를 실행한 상태에서 fetch() 함수를 구현해서 생성과 삭제 로직을 테스트 해본다. 정상적으로 백엔드와의 통신이 진행되고 UI가 수정되는 것을 확인할 수 있다.
유틸리티 함수 작성
위의 방식대로 Add, Delete, Update 로직을 구현해볼 수 있겠지만, 동일한 코드의 반복이 신경쓰인다. 이 문제를 해결하기 위해 유틸리티 함수를 작성해준다. 하드코딩된 URL 주소를 변수에 담고 현재 브라우저가 localhost인 경우 로컬 호스트에서 작동하는 백엔드 애플리케이션을 이용하는 함수다.
먼저 프론트엔드 프로그램의 src 디렉토리 하위에 'demo-app-config.js'를 생성한다.
demo-app-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}`;
다음으로 src 디렉토리 하위에 service 디렉토리를 생성하고 그 하위에 'DemoAPIService.js' 파일을 생성한다. 이 파일 안에서 백엔드 서버로 요청을 보낼 때 사용하는 유틸리티 함수를 정의한다.
DemoAPIService.js
import {API_BASE_URL} from "../demo-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){
options.body = JSON.stringify(reqeust);
}
return fetch(options.url, options)
.then(res => res.json())
.then(response => {
if(!res.ok){
return Promise.reject(response);
}
return response;
})
}
add() / delete() / componentDidMount() 함수 재정의
App.js에서 정의한 3개의 함수를 유틸리티 함수를 사용해서 재정의한다. 코드가 아주 간결해졌다. 프론트엔드 서버를 실행한 상태에서 fetch() 함수 작동을 테스트해보면 정상적으로 작동하는 것을 확인할 수 있다.
import { call } from './Demo/service/DemoAPIService';
add = (item) => {
call("/crud", "POST", item)
.then((response) => this.setState({items : response.data}));
}
delete = (item) => {
call("/crud", "DELETE", item)
.then((response) => this.setState({items : response.data}));
}
componentDidMount(){
call("/crud", "GET", null)
.then(res => this.setState({items : res.data}));
}
update() 함수 로직 변경
App.js에서 update() 함수를 구현해주고, 'Demo' 컴포넌트 속성에 update를 매개변수로 전달한다.
App.js
update = (item) => {
call("/crud", "PUT", item)
.then((response) => this.setState({items : response.data}));
}
<Paper style = {{margin:100 , marginRight:200 , marginLeft:200}}>
<List>
{this.state.items.map((item, idx) => (
<Demo item={item} key={item.id} delete={this.delete} update={this.update}/>
))}
</List>
</Paper>
Demo 컴포넌트에서 props로 update() 함수를 받아와서 enterKeyEventHandler()와 checkboxEventHandler()에 update() 부분을 추가해준다.
Demo.js
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;
this.update = props.update;
}
// delete() 함수를 작동시킬 EventHandler
deleteEventHandler = () => {
this.delete(this.state.item);
}
// 수정 모드 전환
offReadOnlyMode = () => {
this.setState({readOnly : false});
}
// Enter시 수정 모드 종료
enterKeyEventHandler = (e) => {
if(e.key == 'Enter'){
this.setState({readOnly : true})
this.update(this.state.item);
}
}
// 수정 모드에서 새로운 타이틀을 저장한다.
editEventHandler = (e) => {
const thisItem = this.state.item;
thisItem.name = e.target.value;
this.setState({item : thisItem});
}
// 클릭시 체크박스 값 반전
checkboxEventHandler = (e) => {
const thisItem = this.state.item;
thisItem.done = !thisItem.done;
this.setState({item : thisItem});
this.update(this.state.item);
}
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.name}
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;
프론트엔드 서버가 실행된 상태에서 기존에 추가된 TodoList의 name을 클릭 후 수정하면 정상적으로 반영이 된다. 새로고침을 하더라도 백엔드에서 데이터를 수신하기 때문에 내용에 변경이 없다. 또한 개발자 도구의 'Network'도구에서 모니터링 하면 'PUT' METHOD를 이용하는 update()기능 작동시 응답 코드 200을 확인할 수 있다.
더 많은 콘텐츠
'Programming' 카테고리의 다른 글
스프링부트 SpringBoot 웹 애플리케이션 개발 #7 프론트엔드 인증 구현 (feat React.js) (0) | 2022.04.06 |
---|---|
자바 스트림이란 What is Stream in JAVA ? (0) | 2022.04.04 |
스프링 부트 #6 SpringBoot 웹 애플리케이션 개발 스프링 인증 구현 (0) | 2022.04.04 |
스프링 부트 SpringBoot 웹 애플리케이션 개발 #4 프론트엔드 구현하기 (0) | 2022.04.01 |
스프링 부트 SpringBoot 웹 애플리케이션 개발 #3 CRUD 구현하기 (1) | 2022.03.31 |
자바 JAVA 제네릭 Generic이란? (0) | 2022.03.31 |
댓글