Back-End는 Spring으로, Front-End는 React 환경으로 개발 환경을 만들어 보도록 하자.
한 프로젝트 내에서 Spring으로 백엔드를 구축하고 React로 프론트엔드를 개발하는 방식은 백엔드와 프론트엔드를 동시에 관리하고 통합하는 편리한 방법이다. 이와 같은 구조를 사용하면 하나의 코드베이스에서 모든 업무를 처리할 수 있으며, 백엔드와 프론트엔드를 동시에 빌드할 수 있어 개발과 배포를 간편하게 관리할 수 있다.
하지만 이러한 구조에도 단점은 존재한다.🤔 React는 정적인 앱으로, 백엔드가 종료되어도 프론트엔드는 여전히 작동할 수 있다. 그러나 이러한 구조에서는 백엔드가 종료되면 프론트엔드도 동시에 종료되므로 전체 시스템의 안정성에 대한 리스크가 있다. 또한, 두 애플리케이션의 빌드가 동시에 이루어지기 때문에 둘 중 하나라도 문제가 발생하면 전체 빌드가 실패할 수 있다.
https://github.com/kantega/react-and-spring
위 글을 참고하여 Spring Boot와 React 환경을 통합하였다.
💡프로젝트 생성
프로젝트는 Spring Web과 Spring Boot DevTools 의존성을 추가하여 Maven과 JAR 파일 방식으로 빌드하였다.
이외에 프로젝트 이름과 Root 등은 기호에 맞게 작성해 주면 된다.
프로젝트 구조
react-spring-app/
│
├── backend/
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/
│ │ │ │ └── (Spring Backend 소스 코드)
│ │ │ └── resources/
│ │ │ └── (Spring 설정 파일 및 리소스)
│ │ └── test/
│ │ └── (테스트 관련 파일)
│ └── pom.xml
│
└── frontend/
├── public/
│ └── index.html
├── src/
│ └── (React Frontend 소스 코드)
├── package.json
├── package-lock.json
└── (기타 React 프로젝트 파일)
backend
backend/: Spring Boot 애플리케이션의 루트 폴더이다. Java 코드와 설정 파일이 포함된다.
backend/src/main/java/: Spring 백엔드 애플리케이션의 Java 소스 코드가 위치하는 폴더이다.
backend/src/main/resources/: Spring 백엔드 애플리케이션의 설정 파일 및 기타 리소스가 위치하는 폴더이다.
frontend
frontend/: React 프론트엔드 애플리케이션의 루트 폴더이다. React 소스 코드와 프로젝트 파일이 포함된다.
frontend/public/: React 프론트엔드 애플리케이션의 공용 폴더이다. HTML 파일과 정적 자원이 위치한다.
frontend/src/: React 프론트엔드 애플리케이션의 소스 코드가 위치하는 폴더이다.
frontend/package.json: React 프로젝트의 의존성 및 스크립트가 정의된 파일이다.
2024.02.08 - [Programming/React] - [React] 프로젝트 구조, 프로젝트 파일 수정 및 배포
React 프로젝트의 구조에 대해서는 위 글을 참고한다.
개발하려고 하는 프로젝트에 따라서 구조가 상이할 수 있다.
https://emewjin.github.io/feature-sliced-design/?utm_source=substack&utm_medium=email
프런트엔드 아키텍처에 대해서는 위 글을 참고한다.
React 프로젝트 생성
$ npx create-react-app frontend
src/main 아래에 frontend라는 이름으로 React 프로젝트를 생성하였다.
CRA(Create React App)은 더 이상 React에서 공식적으로 추천하지 않는다. 이를 대신해서 Vite를 사용하는 것을 권장한다.
Vite란?
Vite(비트)는 프랑스어로 '빠르다'라는 뜻을 가진 단어로, 프런트엔드 개발 도구이다. 기존의 webpack, rollup 등의 빌드 도구는 자바스크립트 언어로 만들어졌지만, Vite가 내부적으로 사용하는 ESBuild는 Go라는 네이티브 언어로 만들어진 도구를 이용해 빌드하기 때문에 빌드 속도가 빠르다.
ES6 언어를 사용하는 프로젝트 생성
$ npm init vite frontend -- --template react
$ yarn create vite frontend -- --template react
이 명령어는 최신 버전의 Vite 프로젝트 템플릿을 사용하여 새로운 Vite 프로젝트를 생성하는 명령어이다.
create는 Vite 템플릿을 생성하는 Vite CLI 유틸리티이고, vite@latest는 최신 버전의 Vite를 설치하고 사용한다는 것을 의미한다.
명령어를 실행하면 프로젝트 이름과 프로젝트 디렉토리를 지정하라는 프롬프트가 표시되며, 사용자가 입력한 정보를 기반으로 새로운 Vite 프로젝트가 생성된다.
타입스크립트를 사용하는 프로젝트 생성
$ npm init vite frontend -- --template react-ts
$ yarn create vite frontend -- --template react-ts
빌드 명령어
$ npm run build
$ yarn build
개발 서버 시작 명령어
$ npm run dev
$ yarn dev
프로젝트 구조
- src: 자바스크립트와 타입스크립트 코드를 작성하는 디렉터리이다. 이 디렉터리의 진입 파일은 main.tsx 또는 main.jsx이다.
- public: 정적 파일과 리소스를 저장하는 디렉터리이다. 이 디렉터리는 자동으로 생성되지 않으므로 직접 만들어야 한다.
- dist: 빌드 후 생성된 결과물이 저장되는 디렉터리이다.
💡전체 코드
프로젝트 통합에 있어서 중요한 파일은 pom.xml과 package.json 파일이다.
만약 Maven이 아니라 Gradle로 진행하는 경우에는 build.gradle 파일이 된다.
2024.02.11 - [Programming/React] - [React] Spring Boot와 React 프로젝트 Rest API 연동하기
프로젝트를 구성하는 부분은 위 글과 거의 동일하다.
다만, pom.xml에서 프로젝트를 빌드하는 부분에서 차이가 있다.
RestController
package com.test.react.controller;
import java.util.Date;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ReactController {
@GetMapping("/api")
public String hello() {
return "안녕하세요. 현재 서버시간은 " + new Date() + "입니다. \n";
}
}
Back-End에 간단하게 Rest API 서버를 만들었다.
Front-End로 전송할 서버시간을 api 엔드포인트로 설정하여 보내도록 한다.
application.properties
# Server Port
server.port=8090
프로퍼티 설정 파일에서 서버 포트를 정해 주었다.
📌pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 부모 프로젝트 설정 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<relativePath /> <!-- 부모 프로젝트는 저장소에서 가져옴 -->
</parent>
<!-- 프로젝트 기본 정보 -->
<groupId>com.test</groupId>
<artifactId>react</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>react</name>
<description>Spring Web Application for Spring Boot</description>
<!-- 프로젝트 속성 -->
<properties>
<java.version>17</java.version> <!-- 자바 버전 -->
</properties>
<!-- 의존성 설정 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- 빌드 설정 -->
<build>
<plugins>
<!-- Spring Boot Maven 플러그인 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- Frontend Maven 플러그인 -->
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.9.1</version>
<configuration>
<workingDirectory>src/main/frontend</workingDirectory> <!-- 프론트엔드 작업 디렉토리 -->
<installDirectory>src/main/target</installDirectory> <!-- 설치 디렉토리 -->
</configuration>
<!-- 플러그인 실행 설정 -->
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<nodeVersion>v20.11.1</nodeVersion> <!-- Node.js 버전 -->
<npmVersion>10.2.4</npmVersion> <!-- npm 버전 -->
</configuration>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>install</arguments> <!-- npm install 실행 -->
</configuration>
</execution>
<execution>
<id>npm run build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build</arguments> <!-- npm run build 실행 -->
</configuration>
</execution>
</executions>
</plugin>
<!-- Maven AntRun 플러그인 -->
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<phase>generate-resources</phase> <!-- generate-resources 단계에서 실행 -->
<configuration>
<target>
<!-- 타겟 설정 -->
<mkdir dir="${project.build.directory}/classes/public" /> <!-- 디렉토리 생성 -->
<copy todir="${project.build.directory}/classes/public"> <!-- 디렉토리 복사 -->
<fileset dir="${project.basedir}/src/main/frontend/build" /> <!-- 프론트엔드 빌드 디렉토리 -->
</copy>
</target>
</configuration>
<goals>
<goal>run</goal> <!-- run 목표 수행 -->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
React와 Spring Boot를 함께 빌드하려면 Maven을 사용하여 백엔드와 프론트엔드 코드를 동시에 처리할 수 있어야 한다. 이를 위해 Maven에는 프론트엔드 빌드 도구인 npm을 실행하는 새로운 플러그인을 추가해야 한다.
pom.xml 파일에서 추가한 플러그인은 "Frontend Maven 플러그인", "Maven AntRun 플러그인"으로 2개이며, 이때 Node.js와 npm 버전을 본인의 버전으로 맞춰 주어야 한다.
Frontend Maven 플러그인
Frontend Maven 플러그인은 Maven에서 프론트엔드 작업을 관리하기 위해 사용된다. 이 플러그인을 사용하여 프로젝트의 프론트엔드 디렉터리에서 Node.js와 npm을 설치하고, npm 명령을 실행하여 프론트엔드 종속성을 설치하고 빌드할 수 있다.
install-node-and-npm 목표는 Maven 빌드 과정에서 Node.js와 npm을 설치하는 것을 담당한다. 이때, 설정된 Node.js와 npm의 버전은 <nodeVersion> 및 <npmVersion> 요소에서 정의된다.
npm 목표는 프론트엔드 디렉터리에서 npm 명령을 실행하여 종속성을 설치한다. 이는 <arguments> 요소에 정의된 명령으로, 주로 install 명령을 사용하여 종속성을 설치한다.
npm run build 목표는 npm을 사용하여 프론트엔드 앱을 빌드한다. 이는 <arguments> 요소에 정의된 명령으로, 주로 run build와 같은 빌드 스크립트를 실행하여 앱을 빌드한다.
이렇게 설정된 플러그인들을 통해 Maven은 백엔드와 프론트엔드 코드를 동시에 처리하고, 프론트엔드 앱을 빌드하여 정적 자원을 생성할 수 있다.
Maven AntRun 플러그인
Maven AntRun 플러그인은 Maven 빌드 중에 외부 프로그램이나 라이브러리를 실행하는 경우에 유용하다. 특히, 프론트엔드 빌드 도구인 npm을 실행하거나 프론트엔드 앱의 빌드 결과물을 Spring Boot 애플리케이션에 포함시키는 경우에 사용된다.
이 플러그인은 Maven 빌드 프로세스의 generate-resources 단계에서 실행된다. 이는 Maven이 리소스를 생성하고 준비하는 단계로, 프로젝트의 빌드 프로세스 중 일찍 실행되는 단계이다.
- ${project.build.directory}/classes/public 디렉터리를 생성하여 Spring Boot 애플리케이션의 클래스 경로에 추가될 정적 자원을 준비한다.
- ${project.basedir}/src/main/frontend/build에서 프론트엔드 빌드 디렉터리에 있는 파일을 복사하여 클래스 경로에 배포될 자원으로 설정한다.
이러한 설정을 통해 Maven은 프론트엔드 애플리케이션의 빌드 결과물을 Spring Boot 애플리케이션에 포함시키고, 정적 자원을 클래스 경로에 배포하여 사용할 수 있도록 준비한다.
package.json
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.6.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"proxy": "http://localhost:8090",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
"proxy": "http://localhost:8090",
package.json 파일의 proxy 속성은 React 애플리케이션이 개발 서버에서 실행될 때 API 요청을 백엔드 서버로 프록시하도록 설정하는 데 사용된다.
Spring Boot 서버를 8090으로 사용할 예정이므로 proxy 설정 또한 8090으로 설정하였다. Spring Boot와 React를 통합하면 해당 주소로 React 애플리케이션이 실행된다.
CORS 문제 해결
스프링 부트의 백엔드 서버는 localhost:8080에서 실행되고, 리액트 프론트엔드 서버는 localhost:3000번으로 실행된다. 이런 환경에서는 CORS(Cross-Origin Resource Sharing) 문제가 발생할 수 있다. 이 문제를 해결하기 위해서는 프론트엔드에서 proxy를 설정해 준 것이다.
App.js
fetch를 사용하는 경우
import React, { useState, useEffect } from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
const [message, setMessage] = useState("");
useEffect(() => {
fetch('/api')
.then(response => response.text())
.then(message => {
setMessage(message);
});
}, []);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">{message}</h1>
</header>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
export default App;
React의 App.js 파일에서 Spring Boot의 엔드포인트 api에서 값을 받아올 수 있다.
fetch와 axios는 둘 다 HTTP 요청을 처리하는 JavaScript 라이브러리로, 네트워크 요청을 보내고 응답을 받아오는 기능을 제공한다. 그러나 이 두 라이브러리 간에는 몇 가지 차이가 있다.
fetch란?
fetch는 브라우저에 기본 내장된 네이티브 API로, 브라우저 환경에서 주로 사용된다. 이를 통해 비동기적으로 네트워크 요청을 수행하고 응답을 처리할 수 있다. 하지만 fetch는 Promise를 반환하므로 비동기 코드를 작성할 때 조금 더 복잡할 수 있다. 또한 fetch는 기본적으로 CSRF(Cross-Site Request Forgery) 보호를 제공하지 않으며, JSON 데이터를 자동으로 파싱 해주지 않는다. 따라서 응답을 처리할 때 수동으로 response.json() 메서드를 사용하여 JSON 데이터를 파싱해야 한다.
2024.03.09 - [Programming/JavaScript] - [JavaScript] Promise: 비동기 작업 처리 객체
Promise에 대해서는 위 글을 참고한다.
axios를 사용하는 경우
import React, {useEffect, useState} from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState('')
useEffect(() => {
axios.get('/api')
.then(res => setData(res.data))
.catch(err => console.log(err))
}, []);
return (
<div>
받아온 값 : {data}
</div>
);
}
export default App;
axios를 사용하는 경우 위와 같이 코드를 수정할 수 있다.
여기까지 코드 작성을 마치면 Spring Boot를 실행했을 때와 React를 실행했을 때 각각의 화면에서 서버시간이 나오게 된다.
현재 React 서버를 켜고, Spring Boot 서버를 켜야 하므로, Spring Boot를 통해서만 실행이 될 수 있도록 패키징을 해 주는 작업이 필요하다.
axios란?
반면 axios는 브라우저와 Node.js 환경에서 모두 사용할 수 있는 Promise 기반의 HTTP 클라이언트 라이브러리이다. axios는 네트워크 요청과 응답을 다루는 데 유연성과 편의성을 제공한다. 또한 axios는 기본적으로 CSRF 보호를 제공하고 있으며, JSON 데이터를 자동으로 파싱 하여 JavaScript 객체로 반환해 준다. 더불어 axios는 HTTP 요청과 응답을 인터셉트하고 다양한 작업을 처리할 수 있는 기능을 제공한다.
일반적으로는 axios를 사용하는 것이 편리하고 직관적이며, 브라우저 및 Node.js 환경에서 일관된 코드를 작성할 수 있다. 하지만 fetch는 네이티브 API로 제공되므로 추가적인 라이브러리 없이 네트워크 요청을 처리해야 할 때 유용하게 사용할 수 있다.
💡프로젝트 패키징
프로젝트 빌드
JAR 파일 생성
mvnw clean install
위 명령어를 실행하며 BUILD SUCCESS가 나오면 빌드가 성공한 것이다.
JAR 파일 실행 명령어
"C:\Program Files\Java\jdk-17\bin\java.exe" -jar C:\Users\zhzkd\Documents\workspace-spring-tool-suite-4-4.21.0.RELEASE\react\target\react-0.0.1-SNAPSHOT.jar
그냥 JAR 파일을 실행하면 백그라운드에서 실행되므로 나중에 프로세스를 찾아서 kill을 해주어야 한다. 따라서 java -jar JAR파일.jar 명령어로 실행시켜 주어야 하는데, java 명령어가 실행되는 폴더에 JAR 파일을 옮겨서 실행하는 방법도 있지만, 위와 같이 경로를 지정하여 실행하는 방법이 있다. 이때 jdk 버전을 본인의 버전으로 맞춰 주어야 한다.
빌드하고 JAR 파일이 생성되기까지 시간이 꽤 걸리는 편이다.
위 명령어를 통해 Spring Boot 프로젝트의 target 폴더 내에 JAR 파일이 생성되었다.
cmd를 실행하고, 명령어를 통해 JAR 파일을 실행하면 위와 같이 Spring Boot와 React 모두 실행되고 있는 것을 확인할 수 있다. 그리고 React 애플리케이션의 주소가 앞서 설정한 "http://localhost:8090/"으로 통합되어 실행되고 있다.
buildReact
gradlew.bat buildReact
buildReact 명령어로 cmd에서 Spring 환경을 설정하고 동시에 React 애플리케이션을 빌드할 수 있다.
AWS 서버에 배포
$ cd /home/ubuntu
$ java -jar react-0.0.1-SNAPSHOT.jar
서버에 JAR 파일을 배포한 결과에도 화면이 잘 출력되는 것을 확인하였다.
💡React에서 Open API 조회
React 구조에서 고속도로 공공데이터를 조회해 보도록 하자.
고속도로 공공데이터 포털
인증키를 발급하면 작성한 이메일로 인증 코드가 발송된다.
실시간 전국 교통량
https://data.ex.co.kr/openapi/basicinfo/openApiInfoM?apiId=0102&pn=-1
실시간 전국 교통량 데이터를 조회해 보도록 하자.
React 프로젝트 생성
npx create-react-app traffic-app
cd traffic-app
npm install axios
npm audit fix --force
traffic-app이라는 이름으로 react 프로젝트를 생성하였다.
전체 코드
index.js
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')
);
reportWebVitals();
index.css
@font-face {
font-family: 'Pretendard-Regular';
src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
}
body {
font-family: 'Pretendard-Regular', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
App.jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import Traffic from './components/Traffic/Traffic';
import TrafficSearchForm from './components/Traffic/TrafficSearchForm';
import './App.css';
function App() {
const [isLoading, setIsLoading] = useState(true); // 데이터 로딩 상태를 관리하는 상태 변수
const [data, setData] = useState([]); // API에서 가져온 교통 데이터를 저장하는 상태 변수
// 검색 함수를 정의
const fetchData = async (date, exDivCode, tcsType) => {
try {
// API에서 교통 데이터 가져오기
const url = `http://data.ex.co.kr/openapi/trafficapi/nationalTrafficVolumn?key=APIKey&type=json&sumDate=${date}&exDivCode=${exDivCode}&tcsType=${tcsType}`;
const response = await axios.get(url);
// 가져온 데이터를 상태 변수에 저장하고 데이터가 없을 경우 빈 배열로 설정
setData(response.data.list || []);
} catch (error) {
// 데이터를 가져오는 중에 오류가 발생한 경우 콘솔에 오류를 출력
console.error('Error fetching traffic data:', error);
} finally {
// 데이터 가져오기가 완료되면 로딩 상태를 false로 변경
setIsLoading(false);
}
};
useEffect(() => {
// 컴포넌트가 마운트될 때 한 번만 데이터를 가져오도록 설정
fetchData('20231101', '00', '1');
}, []);
return (
<section className="container">
{/* 검색 폼을 렌더링하고, 검색 함수를 props로 전달 */}
<TrafficSearchForm onSearch={fetchData} />
{/* 데이터가 로딩 중인지 여부에 따라 다른 화면을 출력 */}
{isLoading ? (
<div className="loader">
<span className="loader_text">Loading...</span>
</div>
) : (
<div className="trafficInfo">
{/* API에서 받아온 데이터를 화면에 출력 */}
{data.map((item, index) => (
<Traffic
key={index}
sumDate={item.sumDate}
exDivCode={item.exDivCode}
tcsType={item.tcsType}
carType={item.carType}
trafficVolumn={item.trafficVolumn}
/>
))}
</div>
)}
</section>
);
}
export default App;
jsx 문법에서는 if문이 없기 때문에 삼항연산자를 사용한다.
isLoading 값에 따라서 다른 화면을 출력하도록 삼항 연산자를 사용하고 있다.
App.css
/* 메인 컨테이너에 대한 스타일 */
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
/* 로더에 대한 스타일 */
.loader {
display: flex;
justify-content: center;
align-items: center;
font-size: 24px;
color: #333;
margin-top: 20px;
}
/* 검색 폼에 대한 스타일 */
form {
margin-bottom: 20px;
}
/* 폼 라벨에 대한 스타일 */
label {
display: block;
margin-bottom: 10px;
font-weight: bold;
color: #555;
}
/* 폼 입력 요소에 대한 스타일 */
input[type="date"],
select {
width: calc(100% - 22px);
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 1em;
margin-bottom: 10px;
}
/* 제출 버튼에 대한 스타일 */
button[type="submit"] {
background-color: #007bff;
color: #fff;
border: none;
border-radius: 5px;
padding: 10px 20px;
cursor: pointer;
font-size: 1em;
}
/* 제출 버튼 텍스트에 대한 스타일 */
button[type="submit"]:hover {
background-color: #0056b3;
}
/* 교통 정보에 대한 스타일 */
.traffic {
background-color: #f9f9f9;
border-radius: 10px;
padding: 20px;
}
/* 교통 정보 제목에 대한 스타일 */
.traffic-item h3 {
margin-top: 0;
margin-bottom: 20px;
font-size: 1.5em;
color: #333;
}
/* 교통 정보 세부 사항에 대한 스타일 */
.traffic-item {
margin-bottom: 20px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
App.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
// Traffic 컴포넌트에 대한 테스트
test('renders traffic information', () => {
const trafficData = {
sumDate: '2023-01-01',
exDivCode: 'A',
tcsType: 'Type A',
carType: 'Sedan',
trafficVolumn: 100
};
render(<Traffic {...trafficData} />);
const sumDateElement = screen.getByText(/측정일시:/i);
expect(sumDateElement).toBeInTheDocument();
});
Traffic.jsx
import React from "react";
function Traffic({
sumDate,
exDivCode,
tcsType,
carType,
trafficVolumn
}){
return(
<div className="traffic-item">
<h3>일자별 교통량 현황</h3>
<div className="date">측정일시: {sumDate}</div>
<div className="exDivCode">집계주체구분: {exDivCode}</div>
<div className="tcsType">하이패스/일반구분: {tcsType}</div>
<div className="carType">차종구분: {carType}</div>
<div className="trafficVolum">수량: {trafficVolumn}</div>
</div>
);
}
export default Traffic;
Traffic.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
// Traffic 컴포넌트에 대한 테스트
test('renders traffic information', () => {
const trafficData = {
sumDate: '2023-01-01',
exDivCode: 'A',
tcsType: 'Type A',
carType: 'Sedan',
trafficVolumn: 100
};
render(<Traffic {...trafficData} />);
const sumDateElement = screen.getByText(/측정일시:/i);
expect(sumDateElement).toBeInTheDocument();
});
TrafficSerachForm.jsx
import React, { useState, useEffect } from 'react';
function TrafficSearchForm({ onSearch }) {
const [date, setDate] = useState(''); // 날짜 상태 변수
const [exDivCode, setExDivCode] = useState(''); // 도공/민자 구분코드 상태 변수
const [tcsType, setTcsType] = useState(''); // TCS/hi-pass 구분 상태 변수
useEffect(() => {
// 어제의 날짜를 계산
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
// 어제의 날짜를 YYYY-MM-DD 형식으로 변환
const yesterdayString = yesterday.toISOString().slice(0, 10);
// 어제의 날짜를 상태 변수에 설정
setDate(yesterdayString);
}, []);
const handleSubmit = (e) => {
e.preventDefault();
// YYYY-MM-DD 형식의 날짜를 YYYYMMDD 형식으로 변환
const formattedDate = date.replaceAll('-', '');
// 사용자가 입력한 값으로 조회 함수 호출
onSearch(formattedDate, exDivCode, tcsType);
};
return (
<form onSubmit={handleSubmit}>
{/* 날짜 입력 폼 */}
<label>
Date:
<input type="date" value={date} onChange={(e) => setDate(e.target.value)} required />
</label>
{/* 도공/민자 구분코드 입력 폼 */}
<label>
ExDivCode:
<select value={exDivCode} onChange={(e) => setExDivCode(e.target.value)} required>
<option value="00">도공</option>
<option value="01">민자</option>
<option value="02">기타</option>
</select>
</label>
{/* TCS/hi-pass 구분 입력 폼 */}
<label>
TcsType:
<select value={tcsType} onChange={(e) => setTcsType(e.target.value)} required>
<option value="1">TCS</option>
<option value="2">hi-pass</option>
</select>
</label>
<button type="submit">Search</button>
</form>
);
}
export default TrafficSearchForm;
구현 결과
디자인을 고려하지 않고 Open API 데이터를 조회하였을 때 위와 같이 출력되었다.
UI 개선
위와 같이 Open API를 조회하는 화면을 구성하였다.
각 차종에 적합한 이미지로 보기 좋게 개선하고, 날짜별 통계를 내는 방식으로 개선할 수 있을 듯하다.
알고리즘
- 데이터 조회 알고리즘
- 사용자는 TrafficSearchForm 컴포넌트를 통해 조회할 날짜, 도공/민자 구분코드, TCS/hi-pass 구분을 선택합니다.
- 사용자가 폼을 제출하면, handleSubmit 함수가 실행됩니다.
- handleSubmit 함수는 입력된 값들을 가지고 fetchData 함수를 호출합니다.
- fetchData 함수는 Axios를 사용하여 API에 GET 요청을 보냅니다.
- API 요청은 선택된 날짜, 도공/민자 구분코드, TCS/hi-pass 구분에 맞는 데이터를 반환합니다.
- 반환된 데이터는 JSON 형식으로 받아와서 처리됩니다.
- 데이터를 처리한 후에는 상태 변수인 data에 저장되고, UI가 업데이트됩니다.
- UI 표시 알고리즘
- 데이터가 로딩 중인지 확인하고, 로딩 중이면 로딩 상태를 화면에 표시합니다.
- 데이터 로딩이 완료되면, App 컴포넌트는 data 상태 변수를 매핑하여 Traffic 컴포넌트를 반복하여 렌더링합니다.
- 각 Traffic 컴포넌트는 받아온 교통량 정보를 표시하고, 이 정보는 sumDate, exDivCode, tcsType, carType, trafficVolumn 등의 속성을 가지고 있습니다.
- Traffic 컴포넌트에서는 각각의 정보를 화면에 표시하기 위해 HTML 요소를 사용합니다.
실행 순서
시작
|
사용자 입력 받기
|
API 요청 보내기
|
API 응답 받기
|
데이터 로딩 여부 확인
|
데이터 로딩 중이면 로딩 화면 표시
|
데이터 로딩 완료되면 각 교통량 정보를 UI에 표시
|
종료
이런식으로 각 단계가 순차적으로 실행되어 데이터를 조회하고 UI에 표시하는 프로세스가 진행된다.
Next.js 프로젝트 생성
프로젝트 폴더를 생성하고 React.js와 Next.js를 초기화한다.
$ npx create-next-app my-react-next-project
필요한 패키지를 설치한다.
$ cd my-react-next-project
$ npm install axios dotenv
API URL을 .env.local 파일에 설정한다. 그리고 components 폴더에 Traffic.js 파일을 생성하고, Traffic 컴포넌트를 작성한다. 이후 pages 폴더에 index.js 파일을 생성하고, 데이터를 받아오고 화면에 표시하는 Home 페이지를 작성하고 프로젝트를 실행한다.
$ npm run dev
이렇게 하면 React.js와 Next.js를 사용하여 OpenAPI와 데이터를 연결하고 조회하는 프로젝트를 구현할 수 있다.
참고 자료
Spring Boot와 React를 연동하여 개발환경을 만들어보자, 허접프로그래머, 2020.03.02.
Spring Boot와 React를 위한 개발환경 구축하고 같이 빌드하기, kcdevdes, 2023.06.20.
Webapp with Create React App and Spring Boot, kantega, 2020.01.17.
React와 Spring boot를 한번에 빌드해서 실행하기 (Maven 프로젝트), HeoJiye, 2023.04.05.
[React + Spring Boot 연동 (1/3)] - 프론트엔드 설정과 화면그리기, juniKang, 2021.12.09.
[Spring] SpringBoot + React 퀵 스타트 with Gradle, trace90, 2022.08.09.
[React] 리액트의 폴더 구조, coderH, 2022.08.13.
React 활용 - 7 : 폴더 구조 잡기, cooking coding, 2023.04.18.
<React> 공공데이터 API 불러오기(한국도로공사 OpenAPI), 창조적생각, 2021.11.17.