웹 개발 101

웹 개발101_Chapter03_프론트엔드 개발

강용민 2022. 12. 7. 16:33

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

Noje.js 와 NPM

Node.js로 인해 자바스크립트를 브라우저 밖에서 실행할 수 있게 되면서 자바스크립트로 된 node 서버를 이용할 수 있게 되었다.

NPM은 Node Package Manager로 Node.js의 패키지 관리 시스템이다. 메이븐 리포지터리에서 라이브러리를 다운받는 것과 비슷 한 개념으로 우리는 npm을 이용해 npmjs에서 node.js 라이브러리를 설치할 수 있다.

 

 

브라우저의 작동 원리

다음은 클라이언트와 서버 통신을 나타낸 그림이다.

브라우저의 작동 원리
DOM

HTML을 받은 브라우저는 파싱과 렌더링 단계를 거쳐 클라이언트에게 보여진다.

  • 파싱
    • 쉽게 말하자면 렌더링을 하기 위한 전처리 단계이다.
    • 파싱 단계에서 브라우저가 하는 일은 크게 세 가지가 있다.
      • 브라우저는 HTMl을 트리 자료구조의 형태인 DOM(Document Object Model)트리로 변환한다.
      • image, css, script 등 다운로드해야 하는 리소스를 다운로드한다. css는 CSSOM(CSS Object Model)트리로 변혼한다.
      • 다운로드한 자바스크립트를 인터프리트, 컴파일, 파싱, 실행한다.
  • 렌더링
    • DOM 트리와 CSSOM 트리를 합쳐 렌더 트리를 만든다.
    • 레이아웃을 정한다. 이는 트리의 각 노드가 브라우저의 어디에 배치될지, 어떤 크기로 배치될지를 정하는 것이다.
    • 브라우저 스크린에 렌더트리의 각 노드를 그려준다.

React.js

파일 구조

react를 설치하면 다음과 같이 볼 수 있다.

그림의 폴더구조에 대해 살펴보자.

  • node_modules 디렉터리 : package.json을 토대로 디펜던시 패키지들이 설치된 폴더이다.
  • public 디렉터리 : 서버에 필요한 리소스 파일이 있는 폴더이다.
  • src 디렉터리 : 리액트 컴포넌트가 설치되어있는 폴더이다.
  • package.json : 프로젝트의 메타데이터, 사용할 node.js 패키지 목록 등을 포함한다.
  • package-lock.json : 각 패키지에 대한 정보들을 포함하고 있다. 덕분에 어느 환경에서든 같은 버전의 패키지를 설치할 수 있다. 만약 패키지 버전을 변경하고 싶다면 package.json에서 버전 변경 후 node_moules와 package-lock.json을 삭제한 후 'npm install'로 재설치 한다.

 

SPA

React.js 나 Angular.js, Vew.js는 대중적인 SPA라이브러리/프레임워크다.

SPA란 Single Page Application의 약자로, 말 그대로 한번 웹 페이지를 로딩하면 유저가 임의로 새로고침하지 않는 이상 페이지를 새로 로딩하지 않는 애플리케이션을 의미한다. 다시 말해 우리가 페이지를 바꾸고 싶다면 root의 하위 엘리먼트를 다른 HTMl로 수정하면 된다. 브라우저의 자바스크립트는 fetch나 ajax 등의 함수로 서버와 주고받은 데이터를 이용해 자바스크립트 내에서 HTML을 재구성한다. 이렇게 서버에게 HTML 페이지를 요청하지 않고 자바스크립트가 동적으로 HTML을 재구성해 만드는 클라이언트 애플리케이션은 SPA라 한다.

그리고 이 렌더링 과정을 클라이언트-사이드 렌더링이라 한다.

 

React 컴포넌트

자바스크립트 파일 내에서 HTML 코드를 사용할 수 있게 확장한 자바스크립트를 JSX라 한다. 이 JSX 문법능 Babel이라는 라이브러리가 빌드 시간에 자바스크립트로 변역해준다.

ReactDOM은 매개변수로 넘겨받은 <컴포넌트 명 /> 컴포넌트를 이용해 DOM트리를 만드는데, 이때 컴포넌트의 render 함수가 반환한 JSX를 렌더링한다.

 

3.2 프론트엔드 서비스 개발

React.js의 단점 중 하나는 초보자가 디버깅하기 어렵다는 점이다. 즉, 정확히 어디서 문제가 발생하는지 알기 힘들다. 따라서 이 프로젝트에서는 HTML Mock을 우선적으로 작성하고, UI에 들어가게 될 가짜 데이터를 작성한다. 단계적 개발 방식을 도입함으로서 디버깅에드는 시간을 단축할 수 있다.

 

Todo 리스트

첫 번째로 Todo 컴포넌트를 만들어본다. src 디렉터리 아래에 Todo.js 파일을 다음과 같이 생성한다.

 

Todo.js

import React from "react";

const Todo = () => {
    return (
        <div className="Todo">
            <input type="checkbox" id="todo0" name="todo0" value="todo0" />
            <label for="todo0">Todo 컴포넌트 만들기</label>
        </div>
    );
};

export default Todo;

현재 index.js는 App 컴포넌트가 렌더링되고 있다. 따라서 App컴포넌트의 render 함수에 Todo 컴포넌트를 추가한다.

 

App.js

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

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

export default App;

 

Props과 useState Hook

현재 우리는 타이틀이 정해진 똑같은 Todo만을 추가할 수 있다. 임의의 Todo 리스트는 각 Todo마다 다른 타이틀을 가지고 있어야 한다. 이 요구사항을 충족하기 위해 Todo 컴포넌트에 title을 매개변수로 넘겨야 한다.

 

Todo.js

import React, { useState } from "react";

const Todo = (props) => {
    const [item, setItem] = useState(props.item);

    return (
        <div className="Todo">
            <input type="checkbox" id={item.id} name={item.id} checked={item.done}/>
            <label id={item.id}>{item.title}</label>
        </div>
    );
};

export default Todo;

리액트의 훅(Hook)중 하나인 useState는 함수형 컴포넌트에서 상태 변수를 사용할 수 있도록 해준다. 훅을 이용하면 리액트가 제공하는 기능과 상태변수를 사용할 수 있다.

상태 변수란 시간이 지남에 따라 또 컴포넌트의 사용자가 컴포넌트와 상호작용하는 동안 변경되는 변수를 의미한다. Todo는 보이는 것처럼 함수다. 함수 안에서 선언된 변수는 함수가 끝나면 함께 사라진다. 하지만 애플리케이션의 화면은 사용자가 입력하거나 수정하는 내용에 따라 화면이 변해야 한다. 이는 사용자가 입력하거나 수정하는 내용, 즉 상태를 릭액트가 계속 추적하고, 상태가 변할 때마다 함수를 다시 불러 새 상태가 화면에 나타나도록 다시 렌더링해야 한다. 이때, 리액트에게 이 오브젝트가 상태라고 알려주는 함수가 useState 함수다.

useState 함수는 배열을 반환하는데, 첫 번째 값은 방금 상태로 지정한 오브젝트이며, 두 번째 값은 이 상태를 업데이트할 수 있는 함수다.

한번 Todo의 props에 item을 넘겨보자.

 

App.js

import logo from './logo.svg';
import './App.css';
import Todo from './Todo';
import React, {useState} from "react"

function App() {
  const [items, setItem] = useState([
    {id:"0",title:"Heloo World 1",done: true},
    {id:"1",title: "Hello World 2",done: false},
  ]);
  let todoItems = items.length > 0 && items.map((item) => <Todo item={item} key={item.id} />);
  
  return (
    <div className="App">
      {todoItems}
    </div>
  );
}

export default App;

 

material-ui를 이용한 디자인

이번엔 material-ui 패키지를 이용해 UI를 변경해보자. material-ui에서 ListItem, ListItemText, InputBase, Checkbox 등의 컴포넌트를 이용해 복잡한 CSS를 사용하지 않고도 UI를 개선할 수 있다.

 

Todo.js

import React, { useState } from "react";
import {ListItem, ListItemText, InputBase, Checkbox} from "@mui/material";

const Todo = (props) => {
    const [item, setItem] = useState(props.item);

    return (
        <ListItem>
            <Checkbox checked={item.done}/>
            <ListItemText>
                <InputBase
                    inputProps={{"aria-label":"naked"}}
                    type="text"
                    id={item.id}
                    name={item.id}
                    value={item.title}
                    multiline={true}
                    fullWidth={true}
                />
            </ListItemText>
        </ListItem>
    );
};

export default Todo;

 

App.js

import logo from './logo.svg';
import './App.css';
import Todo from './Todo';
import React, {useState} from "react"
import {List,Paper} from "@mui/material"

function App() {
  const [items, setItem] = useState([{id:"0",title:"Heloo World 1",done: true},
    {id:"1",title: "Hello World 2",done: false},]);
  let todoItems = items.length > 0 && (<Paper style={{margin: 16}}>
    <List>
      {items.map((item) => (
        <Todo item={item} key={item.id}/>
      ))}
    </List>
  </Paper>);
  return (
    <div className="App">
      {todoItems}
    </div>
  );
}

export default App;

컴포넌트를 이용해 UI가 다음처럼 변한 것을 확인할 수 있다.

 

Todo 추가

리스트 형태의 Todo 아이템을 화면에 렌더링 하는 코드를 작성했다. 이번에는 Todo를 추가하기 위한 UI와 백엔드 콜을 대신할 Mock을 생성하고 Todo 추가를 통해 이벤트 핸들러 함수를 구현하는 방법과 핸들러 함수를 UI에 연결하는 방법을 배운다.

Todo 추가 모듈을 만들기 위해 src 아래에 AddTodo.js를 생성하고 다음과 같이 구현한다.

 

AddTodo.js

import React, {useState} from "react";
import {Button, Grid, TextField} from "@mui/material";

const AddTodo = (props) =>{
    const [item, setItem] = useState({title: ""});

    return(
        <Grid container style={{marginTop: 20}}>
            <Grid xs={11} md={11} item style={{paddingRight: 15}}>
                <TextField placeholder="Add Todo here" fullWidth />
            </Grid>
            <Grid xs={1} md={1} item>
                <Button fullWidth style={{height: '100%'}} color="secondary" variant="outlined"> + </Button>
            </Grid>
        </Grid>
    );
}
export default AddTodo

 

App.js

import logo from './logo.svg';
import './App.css';
import Todo from './Todo';
import React, {useState} from "react";
import {Container,List,Paper} from "@mui/material";
import AddTodo from "./AddTodo";

function App() {
  const [items, setItem] = useState([{id:"0",title:"Heloo World 1",done: true},{id:"1",title: "Hello World 2",done: false},]);
  let todoItems = items.length > 0 && (<Paper style={{margin: 16}}>
    <List>
      {items.map((item) => (
        <Todo item={item} key={item.id}/>
      ))}
    </List>
  </Paper>);
  return (
    <div className="App">
      <Container maxWidth="md">
        <AddTodo/>
        <div className="TodoList">{todoItems}</div>
      </Container>
    </div>
  );
}

export default App;

 

그럼 다음그림과 같이 된다.

 

AddItem 핸들러 추가

일단 구색을 갖춘 것을 확인할 수 있지만 + 버튼을 눌러도 아무 일도 일어나지 않는다. 이제 + 버튼의 기능을 추가해보자. AddItem 기능을 추가하려면 버튼에 핸들러 함수를 연결해야 한다.

사용자가 todo를 추가하기 위해 다음과 같은 순서를 거친다.

  • todo를 추가하기 위해 인풋필드에 title을 입력한다.
    • onInputChange : 사용자가 인풋필드에 키를 하나 입력할 때마다 실행되며 인풋필드에 담긴 문자열을 자바스크립트 오브젝트에 저장한다.
  • + 버튼을 눌러 todo 아이템을 추가한다.
    • onButtonClick : 사용자가 + 버튼을 마우스로 클릭할 때 실행되며 onInputChane에서 저장하고 있던 문자열을 리스트에 추가한다.
    • enterEventKeyHandler : 사용자가 인풋필드상에서 Enter 또는 Return 키를 눌렀을 때 실행되며 기능은 onButtonClick과 같다.

 

컴포넌트 state에 추가할 Todo 기억하기

사용자가 인풋필드에 입력하는 정보를 컴포넌트 내부에서 임시로 저장하기 위해 useState로 상태 변수 item을 초기화했다. 사용자가 인풋필드에 정보를 입력하면 그 정보는 TextField 컴포넌트에 전달되며 이는 onChange를 props로 받는다.따라서 onChange에 핸들러 함수 onInputChange를 연결해 사용자가 입력하는 키보드 값을 item에 저장할 수 있다.

 

AppTodo.js

import React, {useState} from "react";
import {Button, Grid, TextField} from "@mui/material";

const AddTodo = (props) =>{
    const [item, setItem] = useState({title: ""});
    const onInputChange = (e) =>{
        setItem({title: e.target.value});
        console.log(item);
    };

    return(
        <Grid container style={{marginTop: 20}}>
            <Grid xs={11} md={11} item style={{paddingRight: 15}}>
                <TextField placeholder="Add Todo here" fullWidth onChange={onInputChange} value={item.title}/>
            </Grid>
            <Grid xs={1} md={1} item>
                <Button fullWidth style={{height: '100%'}} color="secondary" variant="outlined"> + </Button>
            </Grid>
        </Grid>
    );
}
export default AddTodo

onInputChange()는 Event e를 매개변수로 받기때문에 TextFiled 컴포넌트에 어떤 이벤트가 발생했을 때마다, onChange()를 실행하고 매개변수로 Event e를 넘긴다. 이런 함수를 이벤트 핸들러 함수라고 한다.  이 이벤트 핸들러 함수는 개발자가 구현해야한다. 그래서 우리는 이벤트 핸들러로 onInputChange() 함수를 구현하고 이를 TextField의 이벤트 핸들러로 onInputChange() 함수를 연결해주는 것이다.

따라서 e.target.value를 item의 title로 지정한 후 setItem을 통해 item의 title로 지정한 후 setItem을 통해 item을 새로 업데이트해 사용자의 todo 아이템을 임시로 저장할 수 있다.

 

Add 함수 작성

두 번째로 + 를 눌렀을 때 todo 아이템을 추가할 함수를 작성해야 한다. AddTodo 컴포넌트는 상위 컴포넌트의 items에 접근할 수 없다. 하지만 상위 컴포넌트인 App 컴포넌트는 items에 접근할 수 있다. App 컴포넌트가 items를 관리하기 때문이다. 따라서 App 컴포넌트에 addItem 함수를 추가하고, 이 함수를 AddTodo의 프로퍼티로 넘겨 AddTodo에서 사용하는 것이다.

 

App.js

import logo from './logo.svg';
import './App.css';
import Todo from './Todo';
import React, {useState} from "react";
import {Container,List,Paper} from "@mui/material";
import AddTodo from "./AddTodo";

function App() {
  const [items, setItems] = useState([{id:"0",title:"Heloo World 1",done: true},{id:"1",title: "Hello World 2",done: true},]);
  const addItem = (item) => {
    item.id = "ID-" + items.length;
    item.done = false;
    setItems([...items,item]);
    console.log("items : ",items);
  };

  let todoItems = items.length > 0 && (<Paper style={{margin: 16}}>
    <List>
      {items.map((item) => (
        <Todo item={item} key={item.id}/>
      ))}
    </List>
  </Paper>);

  return (
    <div className="App">
      <Container maxWidth="md">
        <AddTodo addItem={addItem}/>
        <div className="TodoList">{todoItems}</div>
      </Container>
    </div>
  );
}

export default App;

setItems 함수를 부를 때 setItems(thisItems)가 아닌 setItems([...thisItems])처럼 새 배열을 만들어 주는 이유는 리액트가 레퍼런스를 기준으로 재렌더링하기 때문이다. 배열의 레퍼런스는 배열에 값을 추가하더라도 변하지 않는다. 따라서 리액트는 이 배열에 아무 변화도 없었다고 판단하고 다시 렌더링하지 않는다.

 

AddTodo.js

import React, {useState} from "react";
import {Button, Grid, TextField} from "@mui/material";

const AddTodo = (props) =>{
    const [item, setItem] = useState({title: ""});
    const addItem = props.addItem;

    const onButtonClick = () =>{
        addItem(item);
        setItem({title: ""});
    }

    const onInputChange = (e) =>{
        setItem({title: e.target.value});
        console.log(item);
    };

    return(
        <Grid container style={{marginTop: 20}}>
            <Grid xs={11} md={11} item style={{paddingRight: 16}}>
                <TextField placeholder="Add Todo here" fullWidth onChange={onInputChange} value={item.title}/>
            </Grid>
            <Grid xs={1} md={1} item>
                <Button fullWidth style={{height: '100%'}} color="secondary" variant="outlined" onClick={onButtonClick}> + </Button>
            </Grid>
        </Grid>
    );
}
export default AddTodo;

 

엔터키 입력 시 아이템 추가

마찬가지로 Enter 키 입력 시 아이템을 추가한다.

 

AddTodo.js

import React, {useState} from "react";
import {Button, Grid, TextField} from "@mui/material";

const AddTodo = (props) =>{
    const [item, setItem] = useState({title: ""});
    const addItem = props.addItem;

    const onButtonClick = () =>{
        addItem(item);
        setItem({title: ""});
    }

    const onInputChange = (e) =>{
        setItem({title: e.target.value});
        console.log(item);
    };

    const enterKeyEventHandler = (e) => {
        if (e.key === 'Enter'){
            onButtonClick();
        }
    }

    return(
        <Grid container style={{marginTop: 20}}>
            <Grid xs={11} md={11} item style={{paddingRight: 16}}>
                <TextField placeholder="Add Todo here" fullWidth onChange={onInputChange} onKeyPress={enterKeyEventHandler} value={item.title}/>
            </Grid>
            <Grid xs={1} md={1} item>
                <Button fullWidth style={{height: '100%'}} color="secondary" variant="outlined" onClick={onButtonClick}> + </Button>
            </Grid>
        </Grid>
    );
}
export default AddTodo;

 

Todo 삭제

삭제 기능을 구현하려면 각 리스트 아이템의 오른쪽에 삭제 아이콘을 추가해야 한다. 그런 다음 사용자가 삭제 아이콘을 누르면 아이템을 삭제하는 기능을 추가해야 한다.

 

App.js 일부

const deleteItem = (item) =>{
const newItems = items.filter(e => e.id !== item.id);
setItems([...newItems]);
}

let todoItems = items.length > 0 && (<Paper style={{margin: 16}}>
<List>
  {items.map((item) => (
    <Todo item={item} key={item.id} deleteItem={deleteItem}/>
  ))}
</List>
</Paper>);

 

Todo.js

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

const Todo = (props) => {
    const [item, setItem] = useState(props.item);
    const deleteItem = props.deleteItem;
    const deleteEventHandler = () => {
        deleteItem(item);
    };

    return (
        <ListItem>
            <Checkbox checked={item.done}/>
            <ListItemText>
                <InputBase
                    inputProps={{"aria-label":"naked"}}
                    type="text"
                    id={item.id}
                    name={item.id}
                    value={item.title}
                    multiline={true}
                    fullWidth={true}
                />
            </ListItemText>
            <ListItemSecondaryAction>
                <IconButton aria-label="Delete Todo" onClick={deleteEventHandler}>
                    <DeleteOutlined/>
                </IconButton>
            </ListItemSecondaryAction>
        </ListItem>
    );
};

export default Todo;

 

Todo 수정

수정의 경우 두 가지 경우가 있다. 첫 번째는 체크박스에 체크하는 경우와 타이틀을 변경하고 싶은 경우다.

체크박스는 클릭 시 item.done의 값을 변경하면 되지만 타이틀 변경의 경우 사용자가 아이템의 title 부분을 클릭하면 자동으로 수정 가능한 상태가 되게끔 만들고 사용자가 Enter 키를 누르면 수정 내용을 저장하게 하려한다.

 

App.js

import logo from './logo.svg';
import './App.css';
import Todo from './Todo';
import React, {useState} from "react";
import {Container,List,Paper} from "@mui/material";
import AddTodo from "./AddTodo";

function App() {
  const [items, setItems] = useState([{id:"0",title:"Heloo World 1",done: true},{id:"1",title: "Hello World 2",done: true},]);
  const addItem = (item) => {
    item.id = "ID-" + items.length;
    item.done = false;
    setItems([...items,item]);
    console.log("items : ",items);
  };

  const deleteItem = (item) =>{
    const newItems = items.filter(e => e.id !== item.id);
    setItems([...newItems]);
  }
  
  const editItem = () => {
    setItems([...items]);
  }

  let todoItems = items.length > 0 && (<Paper style={{margin: 16}}>
    <List>
      {items.map((item) => (
        <Todo item={item} key={item.id} deleteItem={deleteItem} editItem={editItem}/>
      ))}
    </List>
  </Paper>);

  return (
    <div className="App">
      <Container maxWidth="md">
        <AddTodo addItem={addItem}/>
        <div className="TodoList">{todoItems}</div>
      </Container>
    </div>
  );
}

export default App;

 

Todo.js

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

const Todo = (props) => {
    const [item, setItem] = useState(props.item);
    const [readOnly, setReadOnly] = useState(true);
    const deleteItem = props.deleteItem;
    const editItem = props.editItem;

    const deleteEventHandler = () => {
        deleteItem(item);
    };

    const turnOffReadOnly = () =>{
        setReadOnly(false);
    }

    const turnOnReadOnly = (e) => {
        if(e.key ==="Enter"){
            setReadOnly(true);
        }
    }
    const editItemHandler = (e) =>{
        item.title = e.target.value;
        editItem();
    }

    const checkboxEventHandler = (e) => {
        item.done = e.target.checked;
        editItem();
    }

    return (
        <ListItem>
            <Checkbox checked={item.done} onChange={checkboxEventHandler}/>
            <ListItemText>
                <InputBase
                    inputProps={{"aria-label":"naked",readOnly: readOnly}}
                    onClick={turnOffReadOnly}
                    onKeyDown={turnOnReadOnly}
                    onChange={editItemHandler}
                    type="text"
                    id={item.id}
                    name={item.id}
                    value={item.title}
                    multiline={true}
                    fullWidth={true}
                />
            </ListItemText>
            <ListItemSecondaryAction>
                <IconButton aria-label="Delete Todo" onClick={deleteEventHandler}>
                    <DeleteOutlined/>
                </IconButton>
            </ListItemSecondaryAction>
        </ListItem>
    );
};

export default Todo;

 

3.3 서비스 통합

현재 독립적으로 동작하는 백엔드 애플리케이션과 독립적으로 동작하는 프론트엔드 애플리케이션이 하나씩 있다. 이제는 두 애플리케이션을 통합해 하나의 기능을하는 웹 애플리케이션을 완성할 차례이다.

애플리케이션 통합 시 보안에 관련된 기능인 CORS처리를 알아보자.

 

CORS

fetch() 함수를 이용해 백엔드 서버와 연결하자. 연결하기 전에 2장에서 만들었던 Spring Boot 서버를 켜야한다.

 

App.js

function App() {
  const [items, setItems] = useState([{id:"0",title:"Heloo World 1",done: true},{id:"1",title: "Hello World 2",done: true},]);

  const requestOptions = {
    method: "GET",
    headers:{"Content-Type" : "application/json"},
  };

  fetch("http://localhost:8080/todo",requestOptions)
  .then((response) => response.json())
  .then(
    (response) => {
      setItems(response.data);
    },
    (error) => {
      
    }
  )
  //생략
}

localhost:3000으로 들어가 개발자 툴의 콘솔 창을 켜보면 다음과 같은 에러를 확인할 수 있다.

보안을 위한 CORS 헤더 Policy를 위반했기 때문이다. CORS는 크로스-오리진 리소스 쉐어링(Cross-Origin Resource SHaring)의 약자로, 처음 리소스를 제공한 도메인(Origin)이 현재 요청하려는 도메인과 다르더라도 요청을 허락해주는 웹 보안 방침이다.

이 프로젝트의 Todo 페이지의 origin은 http://localhost:3000이다. 하지만 백엔드 서버의 도메인은 localhost:8080이다. 도메인이 다르므로 요청을 거절한다.

CORS를 가능하게 하기 위해선 백엔드에서 CORS 방침 설정을 해줘야 한다. config패키지 내 webMvcConfig파일을 만들어 CORS 관련 설정 코드를 작성해보자.

 

webMvcConfig.java

package com.example.demo.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","OPRIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(MAX_AGE_SECS);
    }
}

 

Effect Hook을 이용한 Todo 리스트 초기화

Effect Hook은 리액트가 무한 루프를 방지해준다.

 

App.js

function App() {
  const [items, setItems] = useState([{id:"0",title:"Heloo World 1",done: true},{id:"1",title: "Hello World 2",done: true},]);

  useEffect(() => {
    
    const requestOptions = {
      method: "GET",
      headers:{"Content-Type" : "application/json"},
    };

    fetch("http://localhost:8080/todo",requestOptions)
    .then((response) => response.json())
    .then(
      (response) => {
        setItems(response.data);
      },
      (error) => {}
    );
  },[]);
  //생략}
}

기존의 fetch 함수 호출 부분을 useEffect 함수 안으로 넣은 것이다. useEffect는 함수와 배열을 인자로 받는다.

 

fetch

자바스크립트 Promise

Fetch API에 들어가기 전에 Promise라는 것을 먼저 알아보자. Promise는 어떤 함수를 실행 후 Promise 오브젝트에 명시된 사항들을 실행시키겠다는 약속이다.

Promise는 세 가지 상태가 있다. Pending, Fullfilled, Rejected 상태다.

  • pending 
    • 오퍼레이션이 끝나길 기다리는 상태다.
    • 오퍼레이션이 성공적으로 끝나면 resolve 함수를 통해 이 오퍼레이션이 성공적으로 끝났음을 알리고 원하는 값을 전달할 수 있다.
  • ??? 이상함

 

Fecth API

Fetch는 API 서버로 http 요청을 송신 및 수신 할 수 있도록 도와주는 메서드다. fetch는 첫 번째 매개변수로 uri를 받는다.

 

이제 백엔드와 프론트엔드를 연결해보자.

설정파일에서 애플리케이션이 사용할 백엔드 URI를 동적으로 가져오도록 구현해 이후 도메인이 바뀌는 경우를 대비한다.

프론트엔드 프로젝트의 src 디렉터리 아래에 api-config.js를 생성한다. api-config.js에서 백엔드 서비스의 주소인 http://localhost:8080을 변수에 담고 현재 브라우저의 도메인이 localhost인 경우 로컬 호스트에서 동작하는 백엔드 애플리케이션을 사용한다.

 

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}`;

 

ApiService.js

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

export function call(api, method, request) {
  let 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.js의 기존 함수들을 ApiService를 이용해 수정해보자.

 

App.js

function App() {
  const [items, setItems] = useState([]);

  useEffect(() => {
    call("/todo", "GET", null)
    .then((response) => setItems(response.data));
  }, []);

  const addItem = (item) => {
    call("/todo", "POST", item)
    .then((response) => setItems(response.data));
  };

  const editItem = (item) => {
    call("/todo", "PUT", item)
    .then((response) => setItems(response.data));
  };

  const deleteItem = (item) => {
    call("/todo", "DELETE", item)
    .then((response) => setItems(response.data));
  };
  //생략
}