Spring Boot 3 & Spring Framework 6 - Section 16 : 풀스택 Application 실습 #2 - Backend API랑 연결

이정수·2025년 2월 27일
0


Front-End : React , Back-End : Spring , DBMS : PostgreSQL

  • React를 통해 Front-end로서 생성한 Project를 Spring을 통해 Back-end로서 REST API와 연결
    React에서 Axios를 통해 Back-end로 요청 시 해당 API를 관리하는 Spring에서 Http Request를 처리 후 Http Response를 전달.

Axios
JavaScript에서 HttpRequest를 쉽게 전송할 수 있는 HTTP Client Library
React에서 API 요청을 전송하여 REST API 호출 시 가장 많이 사용.

REST API와 통신을 수행 시 유용하며 Fetch API보다 유용한 기능을 제공하여 React Project에서 더 많이 사용됨.
( 자동 JSON Conversion , Request / Response Interceptor )
async / await을 지원하여 비동기 Promise 기반으로 작동. Javascript-Promise
▶ 비동기로서 여러개의 Request를 동시에 처리 가능.

Fetch API와 다르게 404, 500Http ErrorPromise객체.catch(콜백함수)를 통해 감지 가능. Fetch API
Interceptor를 통해 HTTP Request / HTTP Response를 가로채서 수정이 가능.

  • axios.create({ Property }) :
    axios의 instance를 생성하는 함수.
    axios의 기본설정 ( 기본 URL , Header , timeout 등 )을 사전에 지정이 가능.
    ▶ 매번 Axios를 활용한 API 호출 시 동일한 설정을 반복구현할 필요 없이 재사용 가능한 axios의 instance 생성 가능.

    axios.create() Property 종류

    import axios from 'axios'
            // 기본설정이 구현된 axios instance 생성
    const apiClient = axios.create({
      baseURL: "http://localhost:3000",  // 기본 URL
      timeout: 5000,                     // 요청 제한 시간 (5초)
      headers: {                         // 기본 Header 설정
        "Content-Type": "application/json",
        Authorization: "Bearer YOUR_ACCESS_TOKEN",
      },
    });
    • baseURL :
      API 호출 시의 기본 URL ( ex. Origin : http://localhost:3000 )을 지정
      baseURL을 정의하는 경우 해당 axios instance를 사용하여 Axios HTTP Method 사용 시 URL Parameter만 정의하여 API를 호출할 수 있다.

    • timeout :
      。제한요청시간을 설정하여 설정된 시간 이후 HTTP Request 실패 처리.

    • headers
      。기본 HTTP Request Header 지정.
      Header를 미리 지정할 경우 모든 HTTP Request에서 자동으로 포함되어 로그인 API를 사용 시 JWT Token을 자동 추가하여 유용.

      headers에 정의된 { } 배열의 Authorization은 인코딩된 형태의 Spring Security에서 등록된 ID와 PW를 기입.


  • Axios Http Method
    JSXimport axios from 'axios' 를 선언하여 사용하거나 axios.create()axios instance를 생성하여 활용.
    Server에 Http Request 전달 시 REST API 호출 성공여부에 따른 HTTP Response 또는 Error를 포함한 Promise객체를 return
    Axios HTTP Method에 추가적으로 Callback Method( Promise객체.then(콜백함수) , Promise객체.catch(콜백함수) , Promise객체.finally(콜백함수) )를 선언하여 콜백함수의 매개변수로 전달되는 HTTP Response 또는 Error에 따라서 비동기로 처리하는 logic 구현. Promise-Callback Method

    Axios HTTP Method를 사용하는경우 Promise를 return하며, 이때 Callback Method를 구현 시 Promise는 비동기 특성을 지니므로, Callback Method를 다 완료하기 전에 다음 구문을 실행하게된다.
    async / await을 사용하여 Callback Method가 완료된 후 다음구문을 실행하도록 설정.
    Promise에 async/await 활용사례
    • axios객체.get("URL" , { property }) :
      GET HTTP Request를 Server( = Application )로 전달하여 데이터를 가져온다.
      REST API 호출 성공 시 ( 200 ) Callback method에서 Promise.then(콜백함수)의 콜백함수의 매개변수로 HTTP Response이 전달된다.
      Exception 발생 시 Promise.catch(콜백함수)의 콜백함수의 매개변수로 Error객체가 전달.

      { property }기본 URL , Header , timeout 등을 지정가능하며, 이는 사전에 axios.create({ property })를 통해 axios instance를 생성 활용 가능.

    • axios객체.post("URL", Object객체 , { property })
      。데이터를 포함한 POST HTTP Request를 Server로 전달하여 생성 등의 작용 수행.
      Object객체API호출 시 HTTP Request Body로 전달되며 SpringController Method 매개변수의 @RequestBody를 통해 Mapping되어 데이터를 전달받는다.

    • axios객체.put("URL", Object객체 , { property })
      PUT HTTP Request를 Server로 전달하여 전체 데이터 UPDATE.

    • axios객체.patch("URL" , Object객체 , { property })
      PATCH HTTP Request를 Server로 전달하여 일부 데이터 UPDATE.

    • axios객체.delete("URL" , { property })
      DELETE HTTP Request를 Server로 전달하여 데이터 DELETE.
      • Axios Http Method의 Property에 headers를 포함하여 API 호출
        Axios Http Method의 매개변수에 headers가 정의된 Object 객체를 설정.
        Front-End Application에서 Spring ApplicationSpring Security로 보호된 REST API를 호출하기위해 API 호출 시 전송하는 HTTP RequestSpring에 접근할 수 있는 Authorization Header를 포함하도록 설정.
      import axios from 'axios'
      export const retrieveBean = (pathvariable) => axios.get(`/pg/hello-world-bean/${pathvariable}`,{ headers : {
        // 인코딩된 형태의 사용자 ID와 PW 
        Authorization : "Basic dXNlcjEyMzpwdzQ1Ng=="
       }});

      headers에 정의된 { } 배열의 Authorization은 인코딩된 형태의 Spring Security에서 등록된 ID와 PW를 기입.
      ▶ 이때, Axios API마다 일일이 설정하기보다 사전에 axios.create( { Property } )를 통해 header 등의 property를 구현한 axios instance를 생성하여 Axios HTTP Method 활용 시 매 HTTP Request 전달시마다 해당 header을 포함하여 전달하도록 설정

      import axios from 'axios'
      const apiClient = axios.create({
        baseURL : "http://localhost:8080",
        // axios instance에 기본 Header 설정
        // axios instance를 이용해 API 호출시 해당 Header가 항상 포함.
        headers: {                         
          Authorization: "Basic dXNlcjEyMzpwdzQ1Ng==",
        }
      })
      export const retrieveBean = (pathvariable) => apiClient.get(`/pg/hello-world-bean/${pathvariable}`);


  • Axios Interceptors
    AxiosAPI호출로 인한 HTTP Request or HTTP Response가 처리되기전에 Intercepting하여 Customize할 수 있는 기능.
    axios instance를 이용하여 Interceptor를 구현할 수 있다.

    。해당 기능을 사용하는 경우 headerToken 추가, Error Handling , HTTP Response 변환 등의 작업을 수행할 수 있다.
    • Request Interceptor : axios객체.interceptors.request
      。특정 Axios instance를 통해 API 호출HTTP RequestServer로 전송되기전에 Intercepting하여 Customize.

      axios객체.interceptors.request.use( callback ) 선언 시 callback의 매개변수로서 특정 Axios HTTP Request를 Intercepting하여 전달되는 Configuration 객체( = config )가 전달됨.
      ▶ 해당 Configuration 객체를 수정 시 Intercepting한 HTTP Request를 전송하기 전에 Configuration을 변경할 수 있다.

      Axios HTTP Request Configuration 객체 구조
      Javascript에서 config.headers.Authorization = token처럼 HTTP Request를 Intercepting하여 config을 통해 수정이 가능.

      {
        "url": "/users",
        "method": "get",
        "headers": {
          "Accept": "application/json, text/plain, */*"
        },
        "baseURL": "https://api.example.com",
        "timeout": 5000
      }
      • url : HTTP Request를 수행할 API의 URL Parameter

      • method : HTTP RequestHTTP Method

      • headers : HTTP Requestheader 객체.
        Accept , Authorization , Content-Type , Origin 등의 HTTP Header를 포함
        Authorization : JWT Token , HTTP Basic Auth를 추가 시 사용.

      • baseURL : 기본 URL

      • timeout : HTTP Request의 요청제한시간

      • params : GET HTTP RequestQuery Parameter

      • data : POST , PUT에서 전달할 데이터.


    • Response Interceptor : axios객체.interceptors.response
      。특정 Axios instance를 통해 API 호출HTTP Response를 전달받을 경우 Callback Method가 실행되기전에 Intercepting하여 Customize.


  • Axios Dependency 설치
    npm install axios
    PowerShell에서 npm을 통해 React Project에 dependency로 설치
    Node.js 서버 구동 시 ctrl + c로 취소 후 dependency 설치 후 npm start로 서버 재구동.


    React Projectpackage.json에서 설치된 dependency를 확인 가능.
    ▶ 이후 jsx 파일에서 import axios from 'axios'로 import하여 사용.

React Project에서 Spring에서 구축한 REST API를 호출
Spring에서 구축한 REST API
React Project에서 Axios dependency를 추가 후 사용할 jsx파일에 import axios from 'axios'Axios를 import.
Axios를 활용해 HTTP Method에 해당하는 HTTP Request를 Spring이 Mapping한 REST API URL로 전달.

import {useParams,Link} from 'react-router-dom'
import axios  from 'axios'
export default function WelcomeComponent(){
  const {username} = useParams()
  function callRestAPI(){
    // Spring에서 Mapping한 URL로 GET HTTP Request를 수행.
    axios.get("http://localhost:8080/pg/jpa/users") 
    // REST API 호출 성공 시 작동하며 콜백함수의 매개변수로서 HTTP Response를 가져온다.
    .then((response)=>{console.log(response)}) 
    .catch((error)=>{console.log(error)})
    // REST API 호출 중 Exception 발생 시 작동하며 매개변수로서 Error객체를 가져온다. 
    .finally(()=>{console.log("호출완료")})
  }
  return(
    <div>
      Welcome {username}
      <div>
        Manage Your todos - <Link to="/todos">Go Here</Link>
      </div>
      <div>
        {/* Click 시 REST API를 통해 Spring에서 Data를 GET하는 함수를 작동하는 버튼 */}
      <button className="btn btn-success m-5" onClick={callRestAPI}>Call Data1</button>
      </div>
    </div>
  )
}
  • GET : 간단한 추가실습 :

    • Spring Application에서 Mapping한 URL에 Http Request를 전달하면 Resource를 Response하는 Controller Method 생성
      @RestController를 선언한 Class에서 @GetMapping(path="Mapping할 url")을 선언한 Controller Method 생성.
    @GetMapping(path="/pg/hello-world-bean")
    public String helloWorldBean(){
        return "Hello World";
     }
    • React Component에서 Axios를 통해 HTTP Request를 전송하는 함수와 버튼 구현
      axios를 통해 Spirng이 Mapping한 URL에 HTTP Request를 전송하여 API호출하는 함수 구현.
      HTTP ResponseState에 설정.

      <button>onClick={함수명}으로 해당 함수를 설정하여 버튼을 누르면 trigger되도록 설정.
    import axios  from 'axios'
    import {useState} from 'react'
    export default function WelcomeComponent(){
      const [httpresponse, setHttpResponse] = useState("")
      function CallRestAPIMessage(){
          // `Spirng`이 Mapping한 URL에 `HTTP Request`를 전송하여 API호출
        axios.get("http://localhost:8080/pg/hello-world-bean")
            // HTTP Response를 State값으로 설정
        .then((response)=>{
          setHttpResponse(response.data)}) // HttpResponse의 JSON에서 data JSON 배열로 설정.
        .catch((error)=>{console.log(error)})
        .finally(()=>{console.log("호출완료")})
      }
      return(
        <div>
          <div>
            {/* Click 시 REST API를 통해 Spring에서 Data를 GET하는 함수를 작동하는 버튼 */}
          <button className="btn btn-secondary m-5" onClick={CallRestAPIMessage}>Call Message</button>
          </div>
          <div className="text-info">{httpresponse}</div>
        </div>
      )
    }



  • 이때, http://localhost:3000/ ( React )에서 http://localhost:8080/ ( Spring )으로 다른 Origin을 통해 HTTP Request를 통해 REST API를 호출하는것이므로, HTTP RequestApplication Server에서 CORS로 인한 Spring Security에 의해 차단.
    Spring BootSpring Security에서는 기본적으로 모든 다른 Origin로부터의 HTTP Request로 인한 API 호출( Cross-Origin Request )은 차단하도록 설정. Spring Security-CORS

    401 : Resource에 유효한 자격증명이 없기 때문에 HTTP Request가 적용되지 않았음

  • Spring Boot REST API에서 CORS HTTP Request 활성화
    。특정 Origin( http://localhost:3000/ )에서 전송하는 모든 HTTP RequestCORS를 통해 허용하도록 설정.
    Spring ApplicationWebMvcConfigurer Interface를 활용하여 CORS Configuration 정의
    • CORS ConfigurationWebMvcConfigurer의 instance를 Spring Bean으로 반환하는 @Bean Method 구현.
      。Spring Application의 @SpringBootApplication이 정의된 Spring Boot Application의 진입점 Class에서 해당 @Bean Method 생성.
      WebMvcConfigurer의 instance의 구현Method addCorsMappings()를 Override하여 구현.
      ▶ Override를 수행할 addCorsMappings(CorsRegistry registry)에서 CorsRegistry객체를 활용하여 CORS Configuration을 수행 후 WebMvcConfigurer instanceSpring Bean으로 반환.
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.Bean;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
          // @SpringBootApplication을 통해 Spring Boot의 
          // Auto-Configuration, Spring Bean의 생성 등이 자동으로 설정.
    @SpringBootApplication
    public class Practice5Application {
        public static void main(String[] args) {
            SpringApplication.run(Practice5Application.class, args);
        }
        // CORS Configuration을 수행한 WebMvcConfigurer instance를 생성하여
          // Spring Bean으로 반환하는 @Bean Method.
        @Bean
        public WebMvcConfigurer corsConfigurer() {
            return new WebMvcConfigurer() {
                // addCorsMappings()를 Override하여 작성하며
          // 매개변수로 전달된 CorsRegistry instance를 통해 Cross-Origin Request의 CORS Configuration 수행.
                public void addCorsMappings(CorsRegistry registry) {
                    registry.addMapping("/**") // 모든 URL Pattern에 대하여 CORS 허용.
                            .allowedMethods("*") // 모든 HTTP Method에 대하여 CORS 허용
                            .allowedOrigins("http://localhost:3000"); // ReactOrigin에 대해서만 CORS 허용.
                }
            };
        }
    }


    CORS Configuration에 의해 ReactOrigin ( http://localhost:3000 )에 한해서만 모든 HTTP Method와 URL Pattern에 대해서 Spring ApplicationOrigin으로 Cross-Origin RequestAPI호출을 수행해도 CORS에 의해 차단되지 않는다.

    axios.get("ResourceURL")에서 가져온 HTTP Response는 다음처럼 datastatus 등을 포함한 JSON Object.

    • Cross-Origin Request :
      。특정 Origin에서 다른 Origin으로 HTTP Request를 전송하는것을 의미.

      Origin :
      。웹브라우저에서 Origin프로토콜 + 도메인 + 포트번호 를 조합한 값.
      https:// + example.com + :8080



    • CORS ( Cross-Origin Resource Sharing ) :
      。 브라우저가 Cross-Origin Request의 허용여부를 결정하는 웹 보안정책.
      CORS가 존재하지 않으면, 브라우저가 Cross-Origin Request를 무조건 차단.

      Spring BootSpring Security에서는 기본적으로 모든 다른 Origin로부터의 HTTP Request로 인한 API 호출( Cross-Origin Request )은 차단하도록 설정되어있음.
      ▶ 백엔드 API 서버 ( = Spring Application )에서 CORS Configuration을 통해 Access-Control-Allow-Origin Header를 정의하여 모든 도메인 또는 특정 도메인에만 HTTP Request를 허용하도록 설정.

      ex ) http://localhost:3000/ ( React )에서 http://localhost:8080/ ( Spring )으로 다른 Origin으로부터 HTTP Request를 통해 REST API를 호출하는것이므로, CORS로 인한 Spring Security에 의해 차단.

    • WebMvcConfigurer
      Spring MVC의 세부적인 JAVA 기반 Configuration을 사용자 정의하는 구현 Method를 제공하는 Interface.
      Spring Boot은 기본적으로 Auto Configuration을 제공하지만, 세부설정이 필요할 경우 WebMvcConfigurer을 통해 정의.

      。 해당 Interface의 CORS Configuration , Interceptor 추가 , Static Resource Mapping , View Controller Configuration 수행하는 구현 Method를 Override하여 구현.

      WebMvcConfigurer 구현 메소드

      • addCorsMappings(CorsRegistry registry)
        Spring에서 CORS 정책을 설정하는 WebMvcConfigurer interface 구현 Method.
        ▶ 해당 Method를 구현 및 WebMvcConfigurerOverride하여 설정 시, Back-end Application에서 특정 OriginCross-Origin Request를 전역적으로 허용가능.

        CorsRegistry의 instance를 매개변수로 가져오므로, 해당 CorsRegistry객체를 이용해 CorsRegistry의 Method를 활용하여 CORS정책을 설정.
          @Bean
          public WebMvcConfigurer corsConfigurer() {
              return new WebMvcConfigurer() {
            // addCorsMappings()를 Override하여 작성하며
            // 매개변수로 전달된 CorsRegistry instance를 통해 Cross-Origin Request의 CORS Configuration 수행.
                  public void addCorsMappings(CorsRegistry registry) {
                      registry.addMapping("/**") // 모든 URL Pattern에 대하여 CORS 허용.
                              .allowedMethods("*") // 모든 HTTP Method에 대하여 CORS 허용
                              .allowedOrigins("http://localhost:3000"); // ReactOrigin에 대해서만 CORS 허용.
                  }
              };
          }


    • CorsRegistry :
      Spring MVC에서 CORS정책을 설정 시 사용하는 Class.
      WebMvcConfigurer InterfaceaddCorsMappings(CorsRegistry registry) 구현 Method를 통해 활용됨.

      CorsRegistry Method

      • CorsRegistry객체.addMapping("/URLpattern")
        Spring Boot에서 Cross-Origin RequestCORS를 허용할 URL 패턴을 지정.

        CorsRegistry객체.addMapping("/api/**") :
        /api/ 로 시작하는 모든 Cross-Origin Request에 대하여 CORS 허용.
        CorsRegistry객체.addMapping("/**") :
        。모든 Cross-Origin Request에 대하여 CORS 허용.

      • CorsRegistry객체.allowedOrigins("Origin1", "Origin2" , ...)
        Spring Boot에서 CORS를 허용할 Origin을 지정하는 Method.

        CorsRegistry객체.allowedOrigins("http://localhost:3000")
        。특정 Origin에 대해서만 CORS 허용.
        CorsRegistry객체.allowedOrigins("*")
        。모든 Origin에 대해서 CORS 허용.

      • CorsRegistry객체.allowedMethods("GET", "POST" , ...)
        Cross-Origin Request에서 CORS를 허용할 HTTP Method 지정.
        "GET" , "POST" , "PUT" , "DELETE" , "PATCH"

      • CorsRegistry객체.allowedHeaders("Content-Type" , "Authorization" , ...)
        Spring Boot에서 CORS를 허용할 Header를 지정하는 Method.
        CorsRegistry객체.allowedHeaders("*") : 모든 header에 대해서 CORS 허용.

      • CorsRegistry객체.allowCredentials(Boolean)
        Cookie, Authorization Header등의 인증정보를 포함한 Cross-Origin Request에 대하여 CORS 허용여부 설정.
        CorsRegistry객체.allowCredentials(true) 설정 시 CorsRegistry객체.allowedOrigins("*")를 사용할 수 없으며, 반드시 특정 Origin만 지정하여 설정해야한다.


  • API 호출을 전담하는 별도의 JS파일 생성하여 Refactoring 수행
    。기존 WelcomeComponentAxios를 통한 직접적으로 Application Server에 API 호출하는 기능을 해당 JS파일로 이전하고 API호출의 성공여부에 따른 상호작용만을 구현.
    WelcomeComponent에서 axios를 사용하지 않고, ApiService.js에서만 axios를 사용.

    axios.create()를 통해 axios instance를 생성하여 기본설정 ( 기본 URL , Header , timeout 등 )을 사전에 지정.

    Axios를 활용할 경우, Path Variable을 용이하게 활용이 가능.
                      // Spring
@GetMapping(path="/pg/hello-world-bean/{id}")
    public String helloWorldBean(@PathVariable String id){
        return String.format("Hello World %s",id);
    }

Spring에서 URL Parameter를 포함한 MappingURLHTTP Request하여 API호출 시 @PathVariable를 통해 Java 변수로 가져와서 HTTP Response로 return.

// ApiService.js
import axios from 'axios'
// baseURL이 구현된 Axios instance 생성
const apiClient = axios.create({
  baseURL : "http://localhost:8080"
})
// Axios instance를 통한 API호출을 수행하여 HttpResponse를 Return하는 함수 활용
export const retrieveBeanApi = (pathvariable) => apiClient.get(`/pg/hello-world-bean/${pathvariable}`);

baseURL이 설정된 axios instance를 생성하여 API 호출.
axios instance에 설정을 추가하여 해당 instance를 통해 매번 HTTP Request를 전송할때마다 JWT Token등 을 포함한 Header를 기본적으로 포함하여 전송이 가능.

default가 아닌 경우, 화살표함수로 export 가능.

// WelcomeComponent
import {retrieveBeanApi} from './api/ApiService.js'
const [httpresponse, setHttpResponse] = useState("")
function CallRestAPIMessage(){
    // ApiService.js의 Axios를 통한 API호출을 담당하는 함수를 import하여 활용
    retrieveBeanApi("wjdtn")
    .then((response)=>{
      setHttpResponse(response)})
    .catch((error)=>{console.log(error)})
    .finally(()=>{console.log("호출완료")})
  }



  • ListTodosComponent에서 하드코딩으로 구현된 Static 배열 데이터를 AxiosAPI호출을 통해 DB에서 가져온 데이터로 대체해서 표현
    useEffect() Hook 사용.
    • Spring Controller Method
     // GET Method 구현
        // GET /users : 모든 사용자 조회
            @GetMapping(path="/pg/jpa/users")
            public List<PostgresUser> ListAllUsers(){
                // JpaRepository를 구현한 Service Class의 JpaRepository객체.findAll() Method 활용.
                List<PostgresUser> users = pgservice.GetAllPGUsers();
                return users;
            }

    。API 호출 시 Spring JPA에서 Mapping이 된 DB TableDB Entity를 통해 SpringBeanJpaRepository객체를 통해 가져와서 HTTP Response하여 전송.

    • 특정 ( Todo ) DB Table의 API호출을 전담하는 Axios가 구현된 JS파일을 생성
    // TodoApiService.js
    import axios from 'axios'
    // baseURL이 구현된 Axios instance 생성
    const apiClient = axios.create({
      baseURL : "http://localhost:8080"
    })
    //  Application Server의 Resource URL에 GET HTTP Request를 전송하여 API호출하는 함수
    export const retrieveDataApi = () => apiClient.get(`/pg/jpa/users`);
    • ListTodosComponent에서 API 호출 후 받은 데이터를 JSX코드로 표현
      useEffect(콜백함수) Hook 활용.
      useEffect(콜백함수) Hook를 활용하여 Component가 Rendering된 후 내부에 구현된 콜백함수를 실행하도록 설정.
         // ListTodosComponent.jsx
    import { retrieveDataApi } from './api/TodoApiService.js'
    import { useState , useEffect } from 'react'
    export default function ListTodosComponent(){
      const [todos,setTodos] = useState([]);
      // useEffect(콜백함수)를 구현하여 해당 콜백함수가 
      // Component가 Rendering된 후 실행되도록 설정.
      // 의존성배열이 빈배열이므로, 처음 렌더링 시 한번만 실행
      useEffect( () => refreshState() , []) 
      // Axios -> Spring ->  DB에서 데이터를 가져와서 HTTP Response로 전달.
      function refreshState(){
        retrieveDataApi()
        .then((response)=>{
          console.log(response.data)
          setTodos(response.data)
        })
        .catch((error)=>{console.log(error)})
        .finally(()=>{console.log("실행완료")})
      }
      return(
        <div className="container-fluid">
          <table className="table">
            <thead>
              <tr>
                <td>id</td>
                <td>birthDate</td>
                <td>name</td>
              </tr>
            </thead>
            <tbody>
              { todos && todos.map(
                  (todo)=>
                    (<tr key={todo.id}>
                      <td>{todo.id}</td>
                      <td>{todo.birth_date}</td>
                      <td>{todo.user_name}</td>
                    </tr>
                  ) 
                )
              }
            </tbody>
          </table>
        </div>
      )
    }           

    • useEffect(콜백함수, [ Dependency List ]) :
      import { useEffect } from 'react'
      Functional Component에서 Side effect를 처리하는데 사용하는 React Hook
      ▶ Component의 Rendering 후 실행되는 코드를 정의 시 사용.

      API 호출 , Event Listener 추가, Timer 설정 , State 변화 감지 등 작업에 활용.

      의존성배열 : Dependency List :
      useEffect의 2번째 인자에 선언.

      정의안하는경우 : 모든 Rendering 시 마다 콜백함수 실행.
      ▶ 해당 API 호출 등의 작업을 Rendering할때마다 반복하므로 무한으로 사용.
      빈 배열 [] : React Component가 처음 렌더링 시 한번만 콜백함수 실행.
      [ 특정 변수 ] : 특정 변수가 변경시마다 콜백함수 실행.



    • React에서 axios를 통해 HTTP RequestResponse로 배열객체를 가져와서 State변경함수를 통해 State값으로 설정 후 State배열.map() JSX코드로 표현 시 TypeError: State배열.map is not a function으로 에러가 발생하는 경우
      。원인 : 초기 State를 설정한 React Component를 렌더링 시 State값의 초기값이 null로 설정되어있으므로,JSX코드 내부에 구현된 State배열.map()이 해당값을 인식하지 못한다.
      JSX코드에서 { State배열 && State배열.map() }을 정의하여 State배열에 값이 들어왔을때 map함수가 작동되도록 설정.


  • React Application에서 Application ServerDELETE HTTP Request를 전송하는 API호출을 통해 DB의 데이터를 삭제하는 기능 구현하기
    React Application에서 DELETE /pg/jpa/users/{id} 전송 시 Spring에서 수신하여 Spring JPA를 통해 PostgreSQL DB의 특정 데이터를 삭제
    • Spring Controller Method
    @DeleteMapping(path="/pg/jpa/users/{id}")
        public ResponseEntity<Void> DeleteUserById(@PathVariable int id){
            // JpaRepository를 구현한 Service Class의 JpaRepository객체.deleteById() Method 활용.
            pgservice.DeletePGUser(id);
            return ResponseEntity.noContent().build();
        }

    React에서 DELETE를 수행하기위한 URL Parameter를 포함한 API호출 시 Spring JPA에서 Mapping이 된 DB TableDB Entity를 통하여 JpaRepository instance를 이용해 DB Table의 data 삭제를 수행.

    。이때 Spirng에서 PostgreSQL DB에 대해 Spring JPA를 통해 DELETE 작업을 수행한 이후 ResponseEntity instance를 생성 및 HTTP 204 Status Code를 설정 후 Controller Method의 반환값으로 설정하여 HTTP ResponseReact로 반환.

    Talend API Tester로 Test할 경우 @DeleteMapping으로 Mapping된 URL로 API호출 시 HTTP 204이 설정된 HTTP Response를 반환.

    • 특정 DB Table의 API호출을 수행하는 Axios를 포함하는 JS파일DELETE HTTP Request를 전송하는 API 호출을 수행하는 Axios 함수 구현하기.
    // TodoApiService.js
    import axios from 'axios'
    // baseURL이 구현된 Axios instance 생성
    const apiClient = axios.create({
      baseURL : "http://localhost:8080"
    })
    //  Application에 참조할 데이터를 HTTP Request를 전송하여 API호출하는 함수
    export const retrieveDataApi = () => apiClient.get(`/pg/jpa/users`);
    export const deleteDataByIdApi = (id) => apiClient.delete(`/pg/jpa/users/${id}`); 

    TodoApiService.jsbase URL이 설정되어 생성된 Axios instance를 이용하여 Spring Application@DeleteMapping으로 Mapping된 URL로 id를 포함하여 API호출을 수행하는 화살표함수 생성.

    • ListTodosComponent에서 JSX코드로 표현된 DELETE버튼을 누르면 API호출을 통해 삭제를 수행 후 다시 렌더링하여 표현
      JSX코드에서 배열.map(콜백함수)<table> 구현 시 해당 배열요소의 변수값을 활용하여 onClick등의 이벤트에 사용하는 방법 : 화살표함수
    { todos && todos.map(
                  (todo)=>
                    (<tr key={todo.id}>
                      <td>{todo.id}</td>
                      <td>{todo.birth_date}</td>
                      <td>{todo.user_name}</td>
                      <td><button className="btn btn-danger" onClick={ ()=>{deleteState(todo.id)} }>DELETE</button></td>
                    </tr>
                  ) 
                )
    }

    배열.map()함수가 각 배열요소마다 return하는 JSX요소onClick에 정의된 함수의 매개변수로 해당 배열요소의 변수값을 참조하여 전달.
    ▶ 버튼을 누를 경우 전달된 변수값( id )를 DELETE API 호출 기능이 정의된 함수로 전달하여 삭제를 수행.

    <table><tr>요소의 자식으로 <button>등의 요소로 직접 설정할 수 없으며, <tr><td><button/></td></tr> 으로, 계보의 중간에 <td>를 경유하여 설정.

    import { deleteDataByIdApi } from './api/TodoApiService.js'
    const [todos,setTodos] = useState([]);
      // useEffect(콜백함수)를 구현하여 해당 콜백함수가 
      // Component가 Rendering된 후 실행되도록 설정.
    useEffect( () => refreshState(),[todos] )
    // id를 전달하여 Axios -> Spring -> DB로 데이터를 삭제한 후 HTTP Response로 전달.
    function deleteState(id){
        deleteDataByIdApi(id)
        .then((response)=>{
          console.log(response)
          refreshState()
        })
        .catch((error)=>{console.log(error)})
        .finally(()=>{console.log("실행완료")})
      }

    TodoApiService.js에서 정의된 deleteDataByIdApi(id)를 import하여 JSX코드에서 전달할 매개변수를 정의 후 API호출을 통해 반환될 HTTP Response에 대해 CallBack Method 정의.
    Promise객체.then( ()=>{ refreshState() } )를 설정하여 삭제가 완료될 경우 다시 데이터를 조회하는 기능 추가.
    ▶ 또는 useEffect(콜백함수, 의존성배열)에서 해당 state변수를 의존성배열에 추가하여 state변수 변화 발생 시 데이터를 조회하는 useEffect Hook를 설정.

    import { retrieveDataApi , deleteDataByIdApi } from './api/TodoApiService.js'
    import { useState , useEffect } from 'react'
    export default function ListTodosComponent(){
      const [todos,setTodos] = useState([]);
      const [message,setMessage]= useState(null);
      // useEffect(콜백함수)를 구현하여 해당 콜백함수가 
      // Component가 Rendering된 후 실행되도록 설정.
      useEffect( () => refreshState(),[] )
      // Axios -> Spring ->  DB에서 데이터를 가져와서 HTTP Response로 전달.
      function refreshState(){
        retrieveDataApi()
        .then((response)=>{
          console.log(response.data)
          setTodos(response.data)
        })
        .catch((error)=>{console.log(error)})
        .finally(()=>{console.log("실행완료")})
      }
      // id를 전달하여 Axios -> Spring -> DB로 데이터를 삭제한 후 HTTP Response로 전달.
      function deleteState(id){
        deleteDataByIdApi(id)
        .then((response)=>{
          console.log(response)
          setMessage(id)
          refreshState()
        })
        .catch((error)=>{console.log(error)})
        .finally(()=>{console.log("실행완료")})
      }
      return(
        <div className="container-fluid">
          { message && <div className="alert alert-warning">{`Successfuly Deleted ${message}`}</div> }
          <table className="table"> 
            <thead>
              <tr>
                <th>id</th>
                <th>birthDate</th>
                <th>name</th>
                <th>Delete</th>
              </tr>
            </thead>
            <tbody>
              { todos && todos.map(
                  (todo)=>
                    (<tr key={todo.id}>
                      <td>{todo.id}</td>
                      <td>{todo.birth_date}</td>
                      <td>{todo.user_name}</td>
                      {/* onClick={ 함수 }에 화살표함수 정의 시 해당 map함수의 배열요소의 변수를 참조 가능. */}
                      <td><button className="btn btn-danger" onClick={ ()=>{deleteState(todo.id)} }>DELETE</button></td>
                    </tr>
                  ) 
                )
              }
            </tbody>
          </table>
        </div>
      )
    }


    <td>에 비해 <th>로 강조된 제목.
    DELETE 버튼을 누를 경우, 화살표함수를 통해 해당 데이터의 id 요소를 포함하여 API호출을 수행하여 Axios - Spring JPA - PostgreSQL을 거쳐서 해당 행 데이터의 삭제가 수행 및 ResponseEntity를 통한 HTTP 204가 설정된 HTTP Response가 React로 전달됨.

    • ResponseEntity<Type> : Type : HTTP Response Body에 포함될 Data type
      Spring Framework에서 HTTP Response를 다룰 때 사용하는 객체.
      HTTP Status Code , Header , HTTP Response Body를 포함하여 임의 설정이 가능.

      。Controller Method의 반환값으로 ResponseEntity Instance를 설정 시 Spring이 자동으로 JSON등의 HTTP Response를 생성후 반환.
      @ResponseBody를 따로 선언할 필요가 없다.

      REST API에서 에러처리, Status Code , Custom Response를 쉽게 수행.
      • ResponseEntity.ok(Response Body내용) :
        ResponseEntityHTTP 200 OK Status Code와 함께 Response Body내용"문자열"을 반환.

      • ResponseEntity.noContent() :
        HTTP 204 No Content 설정.
        204 : Client의 HTTP Request를 처리했으나, HTTP Response할 데이터가 존재하지 않음.

      • ResponseEntity.badRequest() :
        HTTP 400 Bad Request 설정.

      • ResponseEnity.notFound() :
        HTTP 404 Not Found 설정.

      • ResponseEntity.internalServerError() :
        HTTP 500 Internal Server Error 설정.

      • ResponseEntity.status(HttpStatus객체.CREATED) :
        ResponseEntityHTTP Status Code를 설정.
        HttpStatus.CREATED : 201

      • ResponseEntity.headers(HttpHeaders객체) :
        HttpHeaders 객체를 Custom한 후 추가하여 Custom Header 추가 가능.

      • ResponseEntity.body(Response Body내용) :
        ResponseEntityResponse Body에 들어갈 내용을 정의.

      • ResponseEntity.build() :
        ResponseEntity의 instance를 생성.
        ResponseEntity.notFound().build() : HTTP 404 Not Found 설정된 ResponseEntity 생성.


  • React Context를 활용하여 로그인한 계정명을 다른 React Component에도 표시하기.
    AuthContext.js에 계정명을 저장할 State변수를 생성.
    <Context객체.Provider>을 통해 해당 State 변수값을 다른 React ComponentContext객체로서 전달.
import { createContext, useState , useContext} from 'react'
export const AuthContext = createContext();
export const ExportContext = () => useContext(AuthContext)
export default function AuthProvider({ children }){
  const [ AuthState , setAuthState ] = useState(false);
  // 계정명을 저장할 State
  const [username,setUsername] = useState(null)
  // Login Component에서 하드코딩 Login Logic만 구현.
  function logIn(username, password){
    if (username==="wjdtn" && password==="12345"){  
      setAuthState(true)
      // State에 계정명을 저장.
      setUsername(username)
      return true
    } else {
      setAuthState(false)
      return false
    }
  }
  // Logout Logic 구현
  function Logoutfunction(){
    setAuthState(false);
  }
  return( 
    // username State를 추가로 Context객체로서 다른 Component로 전달.
    <AuthContext.Provider value={ {AuthState,logIn,Logoutfunction,username}}>
      {children}
    </AuthContext.Provider>
  )
}

。이후 HeadComponent에서 기존 Context객체를 전달하여 `useContext() 를 통해 State를 포함하여 생성된 Context객체를 import하여 해당 Context객체로 전달된 username statenavigation bar`에 표현.

import {ExportContext} from './security/AuthContext'
export function HeaderComponent(){
const AuthContext = ExportContext()
<ul className="navbar-nav">
{ AuthContext.AuthState && 
  <li className="nav-item fs-5 mt-2 me-5">Hello, {AuthContext.username}</li>}

。로그인 상태에서는 지시되지않고, 로그인 후에 지시됨.

  • PostgreSQL DB에서 postgres_user와 외래키로 Mapping되어있으며 Post를 저장할 postgres_post를 생성 및 조회, 삭제하는 기능을 구현하여 Refactioring.
    Spring과 PostgreSQL DB 연결
    postgres_post Table 생성은 Spring JPA를 통해 DB Entity와 Mapping하여 생성.
    @ManyToOne , @OneToManypostgres_user와의 외래키 설정.

  • DB Entity 정의
    @Entity를 선언하여 PostgreSQL DB와 Mapping하는 DB Entity 생성 Spring JPA - Entity 관련 정의
    @ManyToOne , @OneToMany을 활용하여 외래키 역할을 수행하는 field 정의. DB Entity간 양방향 설정
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import java.time.LocalDate;
@Entity
public class PostgresPost {
    @Id
    @GeneratedValue
    private Integer id;
    private String description;
    private LocalDate targetDate;
    @ManyToOne(fetch= FetchType.LAZY)
    @JsonIgnore
    private PostgresUser user;
    public PostgresPost() {}
    public PostgresPost(Integer id, String description, PostgresUser user) {
        this.id = id;
        this.description = description;
        this.user = user;
    }
    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }
    public PostgresUser getUser() { return user; }
    public void setUser(PostgresUser user) { this.user = user; }
    public LocalDate getTargetDate() { return targetDate; }
    public void setTargetDate(LocalDate targetDate) { this.targetDate = targetDate; }
}
  // PostgresUser.java
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
import jakarta.validation.constraints.Past;
import jakarta.validation.constraints.Size;
import java.time.LocalDate;
import java.util.List;
@Entity
public class PostgresUser {
    @Id
    @GeneratedValue
    Integer id;
    @JsonProperty("username")
    @Size(min=2, message = "Name should have at least 2 characters.")
    String name;
    @JsonProperty("birth_date")
    @Past(message="Birth Date should be in the past.")
    LocalDate birthDate;
    public List<PostgresPost> getPosts() { return posts; }
    public void setPosts(List<PostgresPost> posts) { this.posts = posts; }
    @OneToMany(mappedBy="user")
    @JsonIgnore
    private List<PostgresPost> posts;
    public PostgresUser(){}
    public PostgresUser(Integer id, String name, LocalDate birthDate) {
        this.id = id;
        this.name = name;
        this.birthDate = birthDate;
    }
    public Integer getId() { return id; }
    public String getName() { return name; }
    public LocalDate getBirthDate() { return birthDate; }
    public void setId(Integer id) { this.id = id; }
    public void setName(String name) { this.name = name; }
    public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; }
}

id field의 경우 Wrapper ClassInteger type으로 설정.
int와 달리 Wrapper Class객체이므로 Null값을 저장할 수 있다.
DB에서 pk가 없거나 아직 할당되지 않은 상태를 지시하기 위해 NULL을 사용할 수 있게 설정.
Integer vs int

intInteger 차이
Java에서 둘다 정수를 다루지만, 기본 자료형(primitive type)과 wrapper class라는 차이가 존재.

  • int
    。기본자료형 ( primitive type )
    null값을 가질 수 없다.

  • Integer
    java.lang.Integer Class 객체
    int를 객체로 감싼 Wrapper Class 객체
    new 키워드로 선언하여 객체 생성.

    null값을 저장 가능.
    DB Entityprimary key로 활용.


  • Post 조회, 삭제기능 구현
    Post의 조회는 react에서 로그인한 계정이름을 Context를 통해 Axios에 포함하여 Spring으로 API호출 시 JpaRepository<Entity Class,pk type> inteface에 해당 계정이름으로 Spring JPA를 통해 데이터를 찾는 Custom Method를 작성하여 해당하는 PostgresUser instance를 가져온 후 해당 instance에서 자식으로서 외래키로 연결된 PostgresPost instance를 List로서 React에 반환.
  • 로그인한 계정명으로 DB의 데이터를 조회하는 Custom Method 생성
    JpaRepository<Entity Class,pk type>을 상속한 interface에 Custom Method 작성 시 명명규칙을 잘 지켜야한다. Spring Data JPA - Custom Method
    ▶ 제대로 작성안할 경우, 오류발생할 수 있음.
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface PgRepository extends JpaRepository<PostgresUser,Integer> {
    Optional<PostgresUser> findByname(String name);
}

。Custom Method를 통해 return 되는 임의의 데이터의 type을 정의 가능.
▶ 임의로 Optional<PostgresUser>로 설정.

  • PostgresPost의 DELETE 용도의 JpaRepository<Entity Class,pk type> 상속 interface 구현
import org.springframework.data.jpa.repository.JpaRepository;
public interface pgPostRepository extends JpaRepository<PostgresPost,Integer> { }
  • Spring JPABusiness Logic을 구현을 위한 @Service Spring Bean에 DB와 상호작용하는 Class 생성
    JpaRepository<Entity Class,pk type> 상속 interface의 instance를 생성 후 Constructor based Injection을 수행하여 의존성 주입.
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
@Service
public class pgService {
    // JpaRepository instance
    PgRepository pgRepository;
    pgPostRepository pgPostRepository;
    // @Autowired 생략 가능한 Constructor Based Dependency Injection
    public pgService(PgRepository pgRepository, pgPostRepository pgPostRepository) {
        this.pgRepository = pgRepository;
        this.pgPostRepository = pgPostRepository;
    }
        // Read Post by username
    public Optional<PostgresUser> GetPgUserByUsername(String username) {
        return pgRepository.findByname(username);
    }
    // Delete Post
    public void DeletePGPost(Integer id) {
        pgPostRepository.deleteById(id);
    }
}

GetPgUserByUsername(String username) : PgRepository에 구현된 Custom Method로 username를 매개변수로 받아 PostgresUser의 조회를 수행후 Optional<PostgresUser>로 반환.
DeletePGPost(Integer id) : id를 매개변수로 받아 해당 id의 PostgresPost의 데이터를 삭제를 수행.

  • Spring의 Controller Class에 Post를 조회 및 삭제하는 API 생성
    JPA Business Logic을 정의한 Spring Bean의 instance를 생성 후 생성자기반 의존성주입 수행하여 JPA Method 활용.
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.net.URI;
import java.util.List;
import java.util.Optional;
@RestController
public class PgController {
    // JPA Business Logic을 사전에 정의한 Spring Bean
    pgService pgservice;
    // @Autowired 생략 가능한 Constructor Based Dependency Injection
    public PgController(pgService pgservice) {
        this.pgservice = pgservice;
    }
    @GetMapping(path="/pg/jpa/users/{username}/posts")
    public List<PostgresPost> GetPostByUser(@PathVariable String username){
        // JpaRepository를 구현한 Service Class에서 JpaRepository객체.커스텀메소드 활용.
        // 가져온 데이터는 Optional<PostgresUser>로 return.
        Optional<PostgresUser> user = pgservice.GetPgUserByUsername(username);
        if(user.isEmpty()){ // 해당 id가 존재하지 않는 경우 발생.
            throw new UserNotFoundException("username :" + username);
        } else{
  		// 자식 DB Table로서 외래키로 연결된 PostgresPost의 instance를 getter를 통해 List로서 반환.
            return user.get().getPosts();
        }
    }
    @DeleteMapping(path="/pg/jpa/posts/{id}")
    public ResponseEntity<Void> DeletePostByUserId(@PathVariable int id){
        // JpaRepository를 구현한 Service Class의 JpaRepository객체.deleteById() Method 활용.
        pgservice.DeletePGPost(id);
        return ResponseEntity.noContent().build();
    }
}
  • React에서 해당 API를 호출하는 Axios 구현
// TodoApiService.js
import axios from 'axios'
// baseURL이 구현된 Axios instance 생성
const apiClient = axios.create({
  baseURL : "http://localhost:8080"
})
//  Application에 참조할 데이터를 HTTP Request를 전송하여 API호출하는 함수
export const retrieveDataApi = (username) => apiClient.get(`/pg/jpa/users/${username}/posts`);
export const deleteDataByIdApi = (id) => apiClient.delete(`/pg/jpa/posts/${id}`); 
  • 해당 Axios API를 활용하는 React Component 구현
    PostgresPost의 데이터는 username으로 조회하므로, LoginComponent에서 로그인하여 State변수로 계정명을 저장하는 React Context 활용하여 매개변수로 전달.
import { retrieveDataApi , deleteDataByIdApi } from './api/TodoApiService.js'
import { useNavigate } from 'react-router-dom';
import { ExportContext } from './security/AuthContext.js'
import { useState , useEffect } from 'react'
export default function ListTodosComponent(){
  const [todos,setTodos] = useState([]);
  const [message,setMessage]= useState(null);
  // Context를 통해 로그인한 계정명을 가져온다.
  const context = ExportContext();
  const username = context.username;
  const navigate = useNavigate()
  useEffect( () => refreshState(),[] )
  // Axios -> Spring ->  DB에서 데이터를 가져와서 HTTP Response로 전달.
  function refreshState(){
    retrieveDataApi(username)
    .then((response)=>{
      console.log(response.data)
      setTodos(response.data)
    })
    .catch((error)=>{console.log(error)})
    .finally(()=>{console.log("실행완료")})
  }
  // id를 전달하여 Axios -> Spring -> DB로 데이터를 삭제한 후 HTTP Response로 전달.
  function deleteState(id){
    deleteDataByIdApi(id)
    .then((response)=>{
      console.log(response)
      setMessage(id)
      refreshState()
    })
    .catch((error)=>{console.log(error)})
    .finally(()=>{console.log("실행완료")})
  }
  return(
    <div className="container-fluid">
      { message && <div className="alert alert-warning">{`Successfuly Deleted ${message}`}</div> }
      <table className="table"> 
        <thead>
          <tr>
            <th>id</th>
            <th>description</th>
            <th>Delete</th>
          </tr>
        </thead>
        <tbody>
          { todos && todos.map(
              (todo)=>
                (<tr key={todo.id}>
                  <td>{todo.id}</td>
                  <td>{todo.description}</td>
                  <td><button className="btn btn-danger" onClick={ ()=>{deleteState(todo.id)} }>DELETE</button></td>
                </tr>
              ) 
            )
          }
        </tbody>
      </table>
    </div>
  )
}


。정상적으로 특정 PostgresUser의 instance와 연결된 PostgresPost instance가 조회됨.

  • React Application에서 Application ServerPATCH HTTP Request를 전송하는 API호출을 통해 DB의 데이터를 UPDATE하는 기능 구현
    • UPDATE를 수행하는 Component로 redirect하는 기능 구현
      ListTodosComponent에서 UPDATE 버튼을 누를 경우 해당 버튼의 idURL Parameter로서 포함하여 useNavigate(URL) Hook를 통해 redirect 하는 기능 구현.
      // ListTodosComponent.jsx
    import { useNavigate } from 'react-router-dom';
    const navigate = useNavigate()
    function updateState(id){
        navigate(`/todoupdate/${id}`)
      }
    // jsx 코드
    <td><button className="btn btn-secondary" onClick={ ()=>{updateState(todo.id)} }>UPDATE</button></td>

    JSX코드에서 화살표함수를 통해 배열.Map(콜백함수)를 통해 렌더링되는 배열요소의 변수를 가져올 수 있다.

    • Container Component에서 URL Parameter를 포함하여 redirecting 시 Update를 수행하는 Component로 Routing하기 위한 <Route> 구현
      。해당 <Route>path="/URL/:변수명"를 선언하여 useNavigate()에 의해 URL Parameter를 포함하여 redirecting 시 elementComponentuseParams() Hook로 전달.
      // TodoApp.jsx
    <Route path="/todoupdate/:id" element={<AuthenticatedRoute>
                  <TodoUpdateComponent/>
                  </AuthenticatedRoute>}/>

    <AuthenticatedRoute>의 자식 Component로 설정하여 로그인 상태로서 AuthContext.AuthStatetrue일때만 접근할 수 있도록 설정.
    ▶ 브라우저에서 URL을 입력해서 접근을 못하도록 설정.

    • DB에서 특정 User의 특정 Post 데이터를 GET하는 Axios API 및 Spring에서 해당 조회 기능의 API 구현하기
      usernamepost id를 통해 특정 Post의 데이터를 조회.
      // TodoApiService.js
    export const retrievePostByPostIdApi = (username,id) => apiClient.get(`/pg/jpa/users/${username}/posts/${id}`)
      // PgController.java
    @GetMapping(path="/pg/jpa/users/{username}/posts/{postid}")
        public PostgresPost GetPostByUser(@PathVariable String username, @PathVariable int postid){
            Optional<PostgresUser> user = pgservice.GetPgUserByUsername(username);
            if(user.isEmpty()){
                throw new UserNotFoundException("username :" + username);
            }else{
                List<PostgresPost> posts = user.get().getPosts();
                posts.removeIf(post->post.getId() != postid);
      // List<PostgresPost>에서 0번째 배열요소를 PostgresPost로 반환
                return posts.get(0); 
            }
        }

    List<PostgresPost>HTTP Response가 반환하는 경우, reactaxios APIPromise객체promise객체.then((response)=>{ response.data[0] }로서 인덱스를 기입하여 HTTP Response Object를 받아야하며 그렇지 않을 경우 undefined로 지정됨.

    • TodoUPDATE를 수행하는 Component를 생성
      formik , moment.js 활용.
      powershell에서 React Project가 존재하는 directory에서 npm install formik, npm install moment을 입력하여 dependency 정의

            // TodoUpdateComponent.jsx
    import { useState , useEffect } from "react"
    import { useParams } from "react-router-dom"
    import { retrievePostByPostIdApi } from "./api/TodoApiService"
    import { ExportContext } from "./security/AuthContext"
    // Formik에서 <Formik> , <Field> , <Form> , <ErrorMessage> import.
    import { Formik, Field ,Form, ErrorMessage } from "formik"
    export default function TodoUpdateComponent(){
      const { id } = useParams()
      const context = ExportContext();
      const username = context.username;
      const [ description , setDescription ] = useState("");
      const [ targetdate , setTargetdate ] = useState("");
      useEffect( () => retrievePostById() , [id] );
      // Axios API를 통해 HTTP Response를 Promise로 받아서 Callback Method를 통해 State에 초기값용도의 Update할 데이터를를 지정
      function retrievePostById(){
        retrievePostByPostIdApi(username,id)
        .then(response =>{
          setDescription(response.data.description)
          setTargetdate(response.data.targetDate)
        })
        .catch((error)=>{console.log(error)})
        .finally(()=>{
          console.log("실행완료")})
      }
      // 입력필드 Error 발생 시 <ErrorMessage>에 표현될 Error Message 작성
      function validateForForm(value){
        let errors = {
          // 입력필드name : "Error Message" 를 담을 빈 배열
        }
        // Validation Logic 정의
        if (value.description.length < 5 ){
          // Error Message를 errors 배열에 정의
          errors.description = 'Enter at least 5 characters.'
        }
        if (value.targetdate == null){
          errors.targetdate = 'Enter a targetdate'
        }
        return errors
      }
      return (
        <div className="container">
        <h1 className="mb-3">Enter Todo Details</h1>
        <div>
          {/* <Formik> 정의 */}
          <Formik initialValues={ { description, targetdate } }  enableReinitialize={true}
          onSubmit = {(value)=>{console.log(value)}}
          validate = {validateForForm}
          validateOnChange = {false}>
            {
              (props)=>(
                <Form>
                  {/* <Field name="description">에서 Error 발생 시 validate함수에서 정의된 ErrorMessage 전달. */}
                  <ErrorMessage 
                  name = "description"
                  component = "div"
                  className = "alert alert-warning"/>
                  {/* <Field name="targetdate">에서 Error 발생 시 validate함수에서 정의된 ErrorMessage 전달. */}
                  <ErrorMessage
                  name = "targetdate"
                  component = "div"
                  className = "alert alert-warning"/>
                  {/* className="form-group"의 경우 <label>과 <Field>의 한쌍을 묶는게 일반적.*/}
                  <fieldset className="form-group m-2">
                    <label>Description</label>
                    <Field type= "text" className="form-control" name="description"/>
                  </fieldset>
                  <fieldset className="form-group">
                    <label>TargetDate</label>
                    <Field type="date" className="form-control" name="targetdate"/>
                  </fieldset>
                  <div>
                    <button className="btn btn-success" type="submit">Save</button>
                  </div>
                </Form>
              )
            }
          </Formik>
        </div>
        </div>
      )
    };
    • Formik 활용
      import { Formik, Form , Field , ErrorMessage } from "formik"
      Formik을 사용할 경우 useState() Hook를 통한 입력 Field의 onChange 이벤트를 통한 mapping을 일일이 수행 할 필요없이 해당 Formik을 사용하여 Form을 동기화하여 쉽게 관리할 수 있는 장점이 존재.

      <Formik></Formik> 태그를 정의 및 내부에 중괄호 { } 를 구현하여 jsx코드로서의 <Form> 태그요소를 반환하는 함수를 작성.
      ▶ 함수로 <Form> 태그 요소 반환 시 소괄호 ( JSX 태그요소 )로 반환.

      <Formik initialValues={ { 변수 } }>을 설정하여 Form element의 초기값으로 사용.
      <Field>name 속성의 변수명과 <Formik>initialValues 속성의 변수명이 동일한 경우 binding되어 초기값이 표현됨.
      <Field>에서 name 속성을 기반으로 <input>values , onChange , onBlur등의 속성을 자동으로 설정된다.

      <Formik enableReinitialize = { true } >을 설정하여 <Formik>의 re-initialization을 허용.

      <Formik onSubmit={ 함수명 }>을 지정하여 Form 내부의 <button type="submit">으로 Form 제출 시 작동할 함수 설정.
      ▶ 이때 함수의 매개변수 ( values )에 입력된 데이터들이 key-valueObject type으로 전달됨.

      Bootstrap:form-control , form-group
      className="form-group"의 경우 <label><Field>의 한쌍을 묶는게 일반적.

      // Formik에서 <Formik> , <Field> , <Form> import.
      import { Formik, Field ,Form } from "formik"
      <div>
            {/* <Formik> 정의 */}
            <Formik initialValues={ { description, targetdate } } {/* Form의 초기값 설정 */}
                    enableReinitialize={true}						{/* Formik의 재초기화 설정 */}
            		  onSubmit={ (values)=>{console.log(values)} } > {/* Form 제출 시 실행할 함수 설정 */}
              {
                (props)=>(
                  <Form>
                    {/* className="form-group"의 경우 <label>과 <Field>의 한쌍을 묶는게 일반적.*/}
                    <fieldset className="form-group m-2">
                      <label>Description</label>
                      <Field type= "text" className="form-control" name="description"/>
                    </fieldset>
                    <fieldset className="form-group">
                      <label>TargetDate</label>
                      <Field type="date" className="form-control" name="targetdate"/>
                    </fieldset>
                    <div>
                      <button className="btn btn-success" type="submit">Save</button>
                    </div>
                  </Form>
                )
              }
            </Formik>
          </div>


      Description, TargetDate <Field>name 속성을 통해 <Formik>initialValues={ { description, targetdate } } 속성에 의하여 초기값이 자동으로 정의됨.

      。Save 버튼을 통해 Form 제출 시 <Formik>onSubmit={ 함수 } 속성의 함수가 실행되어 object객체로서 값이 전달됨.

      • FormikValidation 활용
        <Formik>validate = { Validation 수행 함수 } 구현 시 에러가 발생하는경우, onSubmit 속성의 함수가 작동되지않고, validate 속성의 함수가 작동됨.
        ▶ 이때, validate 함수에 { 입력필드name : Error Message , ... }를 포함하여 return 시, <ErrorMessage name="입력필드name" />를 통해 표현됨.

        validate속성의 함수는 onSubmit 속성의 함수와 동일하게 매개변수에는 입력된 데이터들이 { key : value }Object객체로서 전달되므로, 입력Field에서 전달된 값을 이용하여 Validation 수행.

        <Formik>에서 validateOnChange = { false } 설정.
       // Formik에서 <Formik> , <Field> , <Form> , <ErrorMessage> import.
      import { Formik, Field ,Form, ErrorMessage } from "formik"
       // 입력필드 Error 발생 시 <ErrorMessage>에 표현될 Error Message 작성
        function validateForForm(value){
          let errors = {
            // 입력필드name : "Error Message" 를 담을 빈 배열
          }
          // Validation Logic 정의
          if (value.description.length < 5 ){
            // Error Message를 errors 배열에 정의
            errors.description = 'Enter at least 5 characters.'
          }
          if (value.targetdate == null){
            errors.targetdate = 'Enter a targetdate'
          }
          return errors
        }
       <div>
            {/* <Formik> 정의 */}
            <Formik initialValues={ { description, targetdate } }  enableReinitialize={true}
            onSubmit = {(value)=>{console.log(value)}}
            validate = {validateForForm}   				{/* Validation검증함수 설정 */}
            validateOnChange = {false}>					{/* 입력Field의 onChange 이벤트 off */}
              {
                (props)=>(
                  <Form>
                    {/* <Field name="description">에서 Error 발생 시 validate함수에서 정의된 ErrorMessage 전달. */}
                    <ErrorMessage 
                    name = "description"
                    component = "div"
                    className = "alert alert-warning"/>
                    {/* <Field name="targetdate">에서 Error 발생 시 validate함수에서 정의된 ErrorMessage 전달. */}
                    <ErrorMessage
                    name = "targetdate"
                    component = "div"
                    className = "alert alert-warning"/>
                    </Form>
                )
              }
      </div>


      <Form> 제출 시 validate속성의 함수를 우선 실행 후 validation에 이상이 없을 경우, onSubmit 속성의 함수를 실행
      validate속성의 함수에 Validation logic으로 정의된 입력Field에 Error가 발생할 경우, 임의로 정의한 ErrorMessage를 return하면서 <ErrorMessage name="에러발생입력Fieldname">에 ErrorMessage를 표시하고, onSubmit 속성의 함수를 실행하지 않는다.

      • Formik :
        React에서 Form을 쉽게 관리할 수 있도록 도와주는 library.
        form의 입력값관리, Validation 검사 등을 간편하게 수행가능.

        powershell에서 React Project가 존재하는 directory에 npm install formik을 입력하여 dependency 정의.

        useState() Hook를 통한 입력 Field의 onChange 이벤트를 통한 mapping을 일일이 수행 할 필요없이 해당 Formik을 사용하여 Form을 동기화하여 쉽게 관리할 수 있는 장점이 존재.
        ▶ 그러나, typing할때마다 validation을 수행하면 좋지 않으므로, validateOnChange={false}로 이벤트 종료.
        • Formik 특징
          • State 관리
            ReactuseState() Hook 없이도 Form의 입력값은 쉽게 관리 가능.

          • Validation 검증 :
            Form의 유효성을 검증하는 역할을 수행.

          • Form Submit
            onSubmit을 통해 쉽게 처리가능.


        • Formik의 Component 종류
          • <Formik> :
            <Form>의 상태를 관리하는 Container 역할을 수행.
            <Form>의 초기값, Validation 검증, onSubmit함수 등을 설정 가능.

            <Formik> 주요 속성

            • initialValues : <Form>의 초기값 설정
              <Field type="range">를 사용하기 위해서는 해당 <Field>name="변수명"을 지정 후 initialValues={{변수명:초기값}}으로 초기값을 지정해야한다.

            • onSubmit={ 함수 } :
              <Form> 제출 시 실행할 함수.
              。함수의 매개변수에는 입력된 데이터들이 { key : value }Object객체로서 전달됨.

            • validate={ Validate함수 } :
              。사용자 임의의 Validation 검증을 정의.
              validate={ validate함수 }로 함수 구현 시 에러가 발생하는경우 onSubmit 속성의 함수가 작동되지않고, validate 속성의 함수가 작동됨.

              onSubmit 속성과 동일하게 validate 속성에서 정의되는 함수의 매개변수에는 입력된 데이터들이 { key : value }Object객체로서 전달됨.

              。해당 validate함수가 특정 입력필드의 ErrorMessage를 return하는 경우, <ErrorMessage name="입력필드name">를 통해 표현이 가능.

            • enableReinitialize = { boolean } : <Formik>의 re-initialization을 설정.

            • validateOnChange = { boolean } : Formik<Field>에 typing을 수행할때마다 onChange 이벤트를 true , false 설정.
              ▶ typing 할때마다에 일일이 검증을 수행하면 안좋으므로, 보통 false로 설정한다.


          • <Form> :
            。HTML의 <form>과 동일하지만 <Formik>과 자동으로 연결.
            <Formik>에서 onSubmit을 정의한 경우, <Form>에서 onSubmit을 정의하지 않더라도 <Formik>이 자동으로 <Form>을 관리.

            <Field type="range">를 사용하기 위해서는 해당 <Field>name="변수명"을 지정 후 initialValues={{변수명:초기값}}으로 초기값을 지정해야한다.

            formik<Form>을 사용할 경우 handleSubmit을 구현할 필요 없이 자동으로 동작.

          • <Field> :
            。HTML의 <input>과 동일한 입력Field 역할을 수행하지만, <Formik>과 자동으로 연결.

            formik<Field>를 사용할 경우 name 속성을 기반으로 <input>values , onChange , onBlur등의 속성이 자동으로 Formik에 의해 관리 및 설정된다.

          • <ErrorMessage> :
            Formik에서 Validation 검증 Error Message를 표시하는 Component.
            ▶ 특정 입력필드에서 Validation 검증에 실패 시 해당 Error Message를 자동으로 표시.

            <Formik>validate 함수의 Validation 검증을 정의해야 Error Message를 표현 가능.
            formik<ErrorMessage><Field>name 속성을 기반으로 Error Message를 자동으로 가져온다.
            <Formik validate={(value) => { { 입력필드name : "ErrorMessage" } } }> 정의 및 <ErrorMessage name="입력필드name">을 정의할 경우, 입력필드에서 오류 발생 시 해당 Error Message를 표현.
            • name="입력필드name" :
              。해당 입력필드에서 오류 발생 시 validate 함수에서 정의된 Error Message를 표현.

            • component="div" :
              <ErrorMessage>를 표현할 HTML 태그요소 정의


      • Moment.js
        Javascript에서 날짜와 시간을 쉽게 다룰 수 있도록하는 라이브러리.
        ▶ 그러나, 공식적으로는 비효율적이고 무거우므로 현재는 Day.js library로 대체됨.

        powershell에서 React Project가 존재하는 directory에 npm install moment을 입력하여 dependency 정의


    • DB에 UPDATE를 수행하는 Business Logic 구현하기
      @Service가 선언되어 JpaRepository<EntityType,pk type> interface의 instance를 생성하여 DB와 상호작용을 수행하는 Business Logic이 구현하는 Class에 정의.
    // Update Post
        @Transactional
        public void updatePGPost(PostgresPost post){
            // 기존 DB Entity를 넣을 경우, UPDATE를 수행한다.
            pgPostRepository.save(post);
        }

    JpaRepository객체.save(DBEntity)INSERTUPDATE를 수행.
    기존 DB EntityUPDATE , 새로운 DB EntityINSERT
    DELETEINSERT를 수행하는 경우 동시성 문제가 발생하므로 주의.

    • Spring Controller Method에서 @PutMapping을 활용하여 UPDATE를 수행하는 Controller Method 생성
      PostgresPost의 Update를 수행하기위헤 Controller Method로 Mapping된 URL로 수정된 PostgresPostContentHTTP Request Body로 전송 시 Controller Method 매개변수의 @RequestBody로 Mapping. @RequestBody
    // Post의 Update를 수행하고 Update한 PostgresPost instance를 return.
        @PutMapping(path="/pg/jpa/users/{username}/posts/{postid}")
        public PostgresPost UpdatePostByUserId(@PathVariable String username, @PathVariable int postid, @RequestBody PostgresPost post){
            // HTTP Request Body가 @RequestBody를 통해 변수에 Mapping되어 Update를 수행.
            pgservice.updatePGPost(post);
            return post;
        }
    • DB에 CREATE를 수행하는 Business Logic 구현하기
     // Create Post
        @Transactional
        public void createPGPost(PostgresPost post){
            // 새로운 DB Entity를 넣을 경우, INSERT를 수행한다.
            pgPostRepository.save(post);
        }
    • Spring Controller Method에서 @PostMapping을 활용하여 CREATE를 수행하는 Controller Method 생성
      URL Parameter로 전송한 username를 이용하여 해당하는 PostgresUser 객체를 JpaRespository<> interface의 Custom Method로 가져온 후 PostgresPost의 외래키 field 역할의 usersetter로 지정.
      PostgresPost instance의 외래키가 해당 PostgresUser로 지정됨.
    // Post의 Save한 PostgresPost instance를 return.
        @PostMapping(path="/pg/jpa/users/{username}/posts")
        public PostgresPost UpdatePostByUserId(@PathVariable String username, @RequestBody PostgresPost post){
            Optional<PostgresUser> user = pgservice.GetPgUserByUsername(username);
            post.setUser(user.get()); 
            pgservice.createPGPost(post);
            return post;
        }


    API로 Test할 경우 다음처럼 추가됨을 확인 가능.
    Id Field의 경우 공란으로 작성하더라도 @GeneratedValue를 통해 자동 할당.

    • React의 Axios API 구현.
    // todo가 HTTP Request Body로서 API 호출 수행.
    export const updatePostApi = (username, id, todo)=>apiClient.put(`/pg/jpa/users/${username}/posts/${id}` , todo)
    export const createPostApi = (username, todo)=>apiClient.post(`/pg/jpa/users/${username}/posts` , todo);

    axios객체.put("url", Object객체) : API 호출 시 Object객체HTTP Request Body로서 전송.

    • Post Update를 수행하는 Component에서 FormikonSubmit 속성 함수 작성하기
    // onSubmit 함수
      function submitForm(value){
        const post = {
          id : id,
          description : value.description,
          targetDate : value.targetdate
        }
        updatePostApi(username,id,post)
        .then(response =>{
          console.log(response)
          navigate("/todos")
        })
        .catch((error)=>{console.log(error)})
        .finally(()=>{
          console.log("실행완료")})
      }

    <Formik>onSubmit을 통해 전달된 입력Field의 값을 이용해 PostgresPost Object 생성.
    ▶ 성공적으로 생성 된 경우 useNavigate() Hook를 통해 List로 안내.

    • Post Create를 수행하는 Component 구축 및 FormikonSubmit 속성 함수 작성하기
            // ListTodosComponent.jsx
            <button className="btn btn-success" onClick={ ()=>{ navigate(`/todocreate/`) } }>CREATE</button>
            // TodoApp.jsx
            <Route path="/todocreate" element={<AuthenticatedRoute>
                  <TodoCreateComponent/>
                  </AuthenticatedRoute>}/>
            // TodoCreateComponent.jsx
    import { Formik, Field ,Form, ErrorMessage } from "formik"
    import { createPostApi } from "./api/TodoApiService"
    import { ExportContext } from "./security/AuthContext"
    import { useNavigate } from "react-router-dom";
    export default function TodoCreateComponent(){
      const description = "description"
      const targetdate = "1998-04-04"
      const context = ExportContext();
      const username = context.username;
      const navigate = useNavigate()
      function validateForForm(value){
          let errors = {}
          if (value.description.length < 5 ){
            errors.description = 'Enter at least 5 characters.'
          }
          if (value.targetdate == null){
            errors.targetdate = 'Enter a targetdate'
          }
          return errors
        }
        // onSubmit 함수
        function submitForm(value){
          const post = {
            description : value.description,
            targetDate : value.targetdate
          }
          createPostApi(username,post)
          .then(response =>{
            console.log(response)
            navigate("/todos")
          })
          .catch((error)=>{console.log(error)})
          .finally(()=>{
            console.log("실행완료")})
        }
      return (
          <div className="container">
          <h1 className="mb-3">Enter Todo Details</h1>
          <div>
            {/* <Formik> 정의 */}
            <Formik  initialValues={{ description , targetdate }}
            enableReinitialize={true}
            onSubmit = {submitForm}
            validate = {validateForForm}
            validateOnChange = {false}>
              {
                (props)=>(
                  <Form>
                    {/* <Field name="description">에서 Error 발생 시 validate함수에서 정의된 ErrorMessage 전달. */}
                    <ErrorMessage 
                    name = "description"
                    component = "div"
                    className = "alert alert-warning"/>
                    {/* <Field name="targetdate">에서 Error 발생 시 validate함수에서 정의된 ErrorMessage 전달. */}
                    <ErrorMessage
                    name = "targetdate"
                    component = "div"
                    className = "alert alert-warning"/>
                    {/* className="form-group"의 경우 <label>과 <Field>의 한쌍을 묶는게 일반적.*/}
                    <fieldset className="form-group m-2">
                      <label>Description</label>
                      <Field type= "text" className="form-control" name="description"/>
                    </fieldset>
                    <fieldset className="form-group">
                      <label>TargetDate</label>
                      <Field type="date" className="form-control" name="targetdate"/>
                    </fieldset>
                    <div>
                      <button className="btn btn-success" type="submit">Save</button>
                    </div>
                  </Form>
                )
              }
            </Formik>
          </div>
          </div>
        )
    }
     const post = {
            description : value.description,
            targetDate : value.targetdate
          }

    ▶ 다음처럼 PostgresPost instance에 생성할 Object객체 생성 시 field의 이름을 올바르게 작성하지않으면 null로 값이 전달됨.

profile
공부기록 블로그

0개의 댓글