241121 리액트 (리덕스 DB연동, RESTful)

241121 에이콘 아카데미 수업에 기반하여 작성되었음을 알립니다.

 

리액트 (Redux)

리덕스의 풀은 굉장히 넓다. 어디가서 자랑하면 개망신 당할 가능성이 농후하다.
항상 겸손하게 "리덕스 입문 했어요, 살짝 발만 담궜습니다" 말하기!

리덕스 실습 (DB 연동)

직원 이름 검색, 고객 검색
jsp 사용해서 해보자!

findjikwon.jsp

<%@page import="org.json.simple.JSONObject"%>
<%@page import="org.json.simple.JSONArray"%>
<%@page import="java.sql.*"%>
<%@ page language="java" contentType="text/json; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
String name = request.getParameter("name");
JSONArray jikwons = new JSONArray();

	Connection conn = null;
	PreparedStatement pstmt = null;
	ResultSet rs = null;
	String result = "";
	
	try {
		Class.forName("org.mariadb.jdbc.Driver");
		String url = "jdbc:mariadb://localhost:3306/test";
		conn = DriverManager.getConnection(url, "root", "1111");
		
		
	} catch(Exception e) {
		System.out.println("=== DB 연동 오류 ===");
		e.printStackTrace();
		return;
	}
	
	try {
		pstmt = conn.prepareStatement("SELECT * FROM jikwon where jikwonname like ?");
		pstmt.setString(1, name + "%");
		rs = pstmt.executeQuery();
		
		while(rs.next()) {
			JSONObject obj = new JSONObject();
			obj.put("jikwonno", rs.getInt("jikwonno"));
			obj.put("jikwonname", rs.getString("jikwonname"));
			obj.put("jikwonjik", rs.getString("jikwonjik"));
			jikwons.add(obj);
			
		}	
		out.print(jikwons.toString());
	} catch(Exception e) {
		System.out.println("=== DB 연동 오류 ===");
		e.printStackTrace();
		return;
	} finally {
		if (rs != null) rs.close();
		if (pstmt != null) pstmt.close();
		if (conn != null) conn.close();
	}
	
%>

 

톰캣 서버, JSON 객체로 받아오는 것을 확인

findgogek.jsp

<%@page import="org.json.simple.JSONObject"%>
<%@page import="org.json.simple.JSONArray"%>
<%@page import="java.sql.*"%>
<%@ page language="java" contentType="text/json; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
String phone = request.getParameter("phone");
JSONArray jikwons = new JSONArray();

	Connection conn = null;
	PreparedStatement pstmt = null;
	ResultSet rs = null;
	String result = "";
	
	try {
		Class.forName("org.mariadb.jdbc.Driver");
		String url = "jdbc:mariadb://localhost:3306/test";
		conn = DriverManager.getConnection(url, "root", "1111");
		
		
	} catch(Exception e) {
		System.out.println("=== DB 연동 오류 ===");
		e.printStackTrace();
		return;
	}
	
	try {
		pstmt = conn.prepareStatement("SELECT * FROM gogek where gogektel like ?");
		pstmt.setString(1, phone + "%");
		rs = pstmt.executeQuery();
		
		while(rs.next()) {
			JSONObject obj = new JSONObject();
			obj.put("gogekno", rs.getInt("gogekno"));
			obj.put("gogekname", rs.getString("gogekname"));
			obj.put("gogektel", rs.getString("gogektel"));
			jikwons.add(obj);
			
		}	
		out.print(jikwons.toString());
	} catch(Exception e) {
		System.out.println("=== DB 연동 오류 ===");
		e.printStackTrace();
		return;
	} finally {
		if (rs != null) rs.close();
		if (pstmt != null) pstmt.close();
		if (conn != null) conn.close();
	}
	
%>

톰캣 서버, JSON 객체로 받아오는 것을 확인

Redux 의존성 추가 : npm install @reduxjs/toolkit react-redux

store.js

import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./reducer";

const store = configureStore({
    reducer:rootReducer,

});

export default store;

action.js

// action을 정의하는 파일을 별도 작성!
// 액션 객체를 생성하는 액션 생성자 함수를 정의

// jikwon data를 처리하는 액션 객체 생성
// dispatch(setEmployees([{jikwonno:1, jikwonname:"홍길동"}, ...]))
export const setEmployees = (employees) => ({
    type:"SET_EMPLOYEES", // 타입의 이름은 기왕이면 대문자로 주자!
    payload:employees,
});

// gogek data를 처리하는 액션 객체 생성
export const setCustomers = (customers) => ({
    type:"SET_CUSTOMERS",
    payload:customers,
});

reducer.js

// 리듀서의 역할 : 디스패치된 액션은 리듀서에서 처리되며, 상태를 업데이트함
// combineReducers : 리듀서를 여러개 결합 
import { combineReducers } from "redux";

const employeeReducer = (state=[], action) => {
    switch(action.type){
        case "SET_EMPLOYEES":
            return action.payload;
            // 리턴되는 값 dispatch(setEmployees([{jikwonno:1, jikwonname:"홍길동"}, ...]))
        default:
            return state;
    }
};

const customerReducer = (state=[], action) => {
    switch(action.type){
        case "SET_CUSTOMERS":
            return action.payload;
        default:
            return state;
    }
};

// 여러 리듀서를 결합하고 리덕스 스토어에서 사용할 수 있도록 함
export default combineReducers({
    employees:employeeReducer,
    customers:customerReducer,

});

EmployeeSearch.js

axios 의존성 추가 : npm install axios
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { setEmployees } from "./action"; // action import
import axios from "axios";

const EmployeeSearch = () => {
    const [searchTerm, setSearchTerm] = useState("");

    // 리듀서의 employeeReducer에서 관리되는 state(상태)를 조회
    const employees = useSelector((state) => state.employees); // 리듀서에 익스포트한 키값을 걸어줌
    
    // dispatch 객체 생성
    const dispatch = useDispatch();

    // ajax 비동기 처리를 위해 axios를 써줄 때 async, await 써주는 것이 오리지널 방법, 프로미스 객체로 감싼다.
    const handleSearch = async() => {
        const response = await axios.get("/findJikwon.jsp", {
            params:{name:searchTerm}, // 입력한 값을 가지고 감
        });
        // 웹 서버로부터 요청된 결과를 받아 Redux 상태에 저장
        dispatch(setEmployees(response.data)); // dispatch로 action을 부르고 response의 데이터를 줌
    };

    return (
        <div>
            <h2>직원이름으로 직급 검색</h2>
            <input type="text" value={searchTerm}
            onChange={(e) => setSearchTerm(e.target.value)} // onChange의 경우 입력될 때마다 리렌더링되므로 잘 생각해줘야한다. 따로 설정해주는 것이 좋아을 것 같다.
            placeholder="직원명 입력"/>
            <button onClick={handleSearch}>검색 결과 확인</button>
            <ul>
                {employees.map((emp) => (
                    <li key={emp.jikwonno}>
                        {emp.jikwonname}님의 직급은 {emp.jikwonjik}
                    </li>
                ))}
            </ul>
        </div>
    );

}

export default EmployeeSearch;

CustomSearch.js

(위와 동일하게 만들어 주자!)

import axios from "axios";
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { setCustomers } from "./action";

const CustomSearch = () => {
    const [searchTerm, setSearchTerm] = useState("");

    const customers = useSelector((state) => state.customers);

    const dispatch = useDispatch();

    const handleSearch = async() => {
        const response = await axios.get("/findgogek.jsp", {
            params:{phone:searchTerm},
        });

        dispatch(setCustomers(response.data));
    };

    return (
        <div>
            <h2>고객 전화번호로 검색</h2>
            <input type="text" value={searchTerm}
            onChange={(e) => setSearchTerm(e.target.value)}
            <button onClick={handleSearch}>검색 결과 확인</button>
            <ul>
                {customers.map((cus) => (
                    <li key={cus.gogekno}>
                        {cus.gogekname}님의 번호는 {cus.gogektel} 입니다.
                    </li>
                ))}
            </ul>
        </div>
    )
}

export default CustomSearch;

App.js

import React from "react";
import { Provider } from "react-redux";
import store from "./store";
import EmployeeSearch from "./EmployeeSearch";
import CustomSearch from "./CustomSearch";


function App() {
  return (
    <Provider store={store}>
    <div className="App">
      <EmployeeSearch />
      <hr />
      <CustomSearch />
    </div>
    </Provider>
  );
}

export default App;

리액트 실습 (RESTful)

서버단은 기존 수업 자료인 sprweb35restful_mem의 데이터들을 사용
jpa > mybatis로 RESTful하게 해보자!, 야믈로 만들어보자!
클라이언트단은 리액트를 사용하여 연결해보자 

dependencies

application.properties

(yml을 써줘서 이거는 안쓸거임!)

spring.application.name=sprweb48restful_react

server.port=80
spring.thymeleaf.cache=false

#mariadb server connect
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://127.0.0.1:3306/memberdb
spring.datasource.username=root
spring.datasource.password=1111

#mybatis
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=pack.dto.*

application.yml

# yml 파일은 데이터 직렬화 언어로 환경 구성 작성에 효과적이다.
# key: value로 구성 (value를 입력할 때 반드시 한칸 띄어주어야함)
# 확장자는 .yml, .yaml을 쓰면 된다.
# 탭 사용 불가
server:
  port: 80

spring:
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://127.0.0.1:3306/memberdb
    username: root
    password: 1111

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: pack.dto.**
💡 type-aliases-package: pack.dto.**
dto의 하위 디렉토리를 찾아야 하기 때문에 ** 입력해준다.

MemberMapper.xml

(classpath의 루트인 resource에 xml 만들어줌)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="member">
	<select id="selectDataAll" resultType="memberDto">
		select num,name,addr from mem
	</select>
	<select id="selectData" parameterType="int"
		resultType="memberDto">
		select num,name,addr from mem where num=#{num}
	</select>

	<insert id="insert" parameterType="memberDto">
		insert into mem values(#{num},#{name},#{addr})
	</insert>
	<update id="update" parameterType="memberDto">
		update mem set name=#{name},addr=#{addr} where num=#{num}
	</update>
	<delete id="delete" parameterType="int">
		delete from mem where num=#{num}
	</delete>
</mapper>

MemberDto.java

package pack.dto;

import org.apache.ibatis.type.Alias;

import lombok.Data;

@Data
@Alias("memberDto")
public class MemberDto {
	private int num;
	private String name;
	private String addr;
}

MemberDao.java

package pack;

import java.util.List;

import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import pack.dto.MemberDto;

@Repository
public class MemberDao {
	@Autowired
	private SqlSession session;
	
	public List<MemberDto> getDataAll(){
		return session.selectList("member.selectDataAll"); // SqlSession이 지원하는 메소드
	}
	
	public MemberDto getData(int num) {
		return session.selectOne("member.selectData", num); // SqlSession이 지원하는 메소드
	}
	
	public void insert(MemberDto fbean) {
		session.insert("member.insert", fbean); // SqlSession이 지원하는 메소드
	}
	
	public void update(MemberDto fbean) {
		session.update("member.update", fbean); // SqlSession이 지원하는 메소드
	} 
	
	public void delete(int num) {
		session.delete("member.delete", num); // SqlSession이 지원하는 메소드
	}
}

MemberController.java

package pack.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import pack.dto.MemberDto;
import pack.repository.MemberDao;

@Controller
public class MemberController {
	@Autowired
	private MemberDao dao;
	
	@GetMapping("/members")
	public String list(Model model) {
		List<MemberDto> list = dao.getDataAll();
		model.addAttribute("list", list);
		return "list";
	}
	
	@GetMapping("/insertform")
	public String insertform() {
		return "insertform";
	}
	
	@PostMapping("/insert")
	public String insert(@RequestParam("num")int num,
			@RequestParam("name")String name,
			@RequestParam("addr")String addr) {
		MemberDto fbean = new MemberDto();
		fbean.setNum(num);
		fbean.setName(name);
		fbean.setAddr(addr);
		
		dao.insert(fbean);
		
		return "redirect:/members";
	}
}

index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>메인 페이지</h1>
메뉴 1 메뉴 2 <a href="/members">회원목록</a>

</body>
</html>

list.html

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">

<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<a th:href="@{/insertform}">회원 추가</a>
<h3>회원 목록</h3>
<table>
  <thead>
    <tr>
      <th>번호</th><th>이름</th><th>주소</th>
    </tr>
  </thead>
  <tbody>
  	<tr th:each="m:${list}">
  	  <td th:text="${m.num}"></td>
  	  <td th:text="${m.name}"></td>
  	  <td th:text="${m.addr}"></td>
  	</tr>
  </tbody>
</table>
</body>
</html>

insertform.html

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h3>* 회원 등록 *</h3>
<form th:action="@{/insert}" method="post">
	<div>
		<label for="num">번호 : </label>
		<input type="text" name="num" id="num">
	</div>
	<div>
		<label for="name">이름 : </label>
		<input type="text" name="name" id="name">
	</div>
	<div>
		<label for="addr">주소 : </label>
		<input type="text" name="addr" id="addr">
	</div>
	<button type="submit">등록</button>
</form>
</body>
</html>

회원 읽기, 추가 확인

RESTful로 바꿔보자!

MemberController.java

package pack.controller;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import pack.dto.MemberDto;
import pack.repository.MemberDao;

// RESTful api 사용
@RestController
public class MemberController {
	@Autowired
	private MemberDao dao;
	
	// 전체 자료 읽기
	@GetMapping("/members")
	public List<MemberDto> getList() {
		return dao.getDataAll();
	}
	
	// 추가
	@PostMapping("/members")
	public Map<String, Object> insert(@RequestBody MemberDto fbean) { // JSON으로 들어오기 때문에 @RequestBody으로 받아줌 
		dao.insert(fbean);
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("isSuccess", true);
		return map;
	}
	
	// 한 개의 데이터 읽기
	@GetMapping("/members/{num}")
	public MemberDto getData(@PathVariable("num")int num) {
		return dao.getData(num);
	}
	
	// 수정
	@PutMapping("/members/{num}")
	public Map<String, Object> update(@RequestBody MemberDto fbean,
			@PathVariable("num")int num) { // 수정을 위해서 수정할 내용을 담을 fbean과 num을 넘겨준다.
		fbean.setNum(num);
		dao.update(fbean);
		return Map.of("isSuccess", true);
	}
	
	// 삭제
	@DeleteMapping("/members/{num}")
	public Map<String, Object> delete(@PathVariable("num")int num) {
		dao.delete(num);
		return Map.of("isSuccess", true);
	}
}

회원 조회

(RestController의 ResponseBody, 잭슨 라이브러리의 지원으로 이렇게 바뀌었다. AJAX 요청하면 너무 좋겠다)

회원 추가!

한개의 데이터 읽기!

수정

삭제

서버단은 완성했다. 리액트로 가보자!

AJAX 의존성 추가 : npm install axios
Router 의존성 추가 : npm install react-router-dom

Link와 Route를 떨어뜨려놓아보자!

package.json

{
생략 ...
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "proxy":"http://localhost:80"
}

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <BrowserRouter>
    <App />
    </BrowserRouter>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Home.js

import { Link } from "react-router-dom";

export default function Home() {
    return(
        <>
            <h1>메인(Home)</h1>
            <h2>정보 확인용 페이지</h2>
            <ul>
                <li>
                    <Link to="/members">회원목록</Link>
                </li>
            </ul>
        </>
    );
}

Member.js

import axios from "axios";
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";

export default function Member() {
    const [members, setMembers] = useState([]);

    // 목록보기!
    const refresh = () => {
        axios.get("/members")
        .then(res => {
            setMembers(res.data) // ajax 요청 결과가 members에 들어감!
        })
        .catch(error => {
            console.log(error);
        })
    };

    useEffect(() => {
        refresh()
    },[]) // mount 이후 최초 화면이 뜰 때 refresh를 1회만 수행!

    // 삭제 버튼 처리 이벤트 핸들러 함수
    const handleDelete = (num) => {
        axios.delete("/members/" + num)
        .then(res => {
            refresh(); // 삭제 후 목록 보기
        })
        .catch(error => {
            console.log(error);
        })
    }


    // 페이지 이동하기 함수 객체 생성
    // useNavigate : Link를 써서 페이지 이동(단순 이동)을 할 수 있지만
    // useNavigate를 사용하면 특정 이벤트가 실행됐을 때 동작하거나 추가적인 로직이 필요한 경우 효과적이다. 
    const navigate = useNavigate();
    
    return (
        <>
        <Link to="/">홈페이지</Link>&nbsp;&nbsp;
        <Link to="/members/new">회원추가</Link>
        <h1>* 회원 목록 *</h1>
        <table>
            <thead>
                <tr>
                    <th>번호</th><th>이름</th><th>주소</th><th>수정</th><th>삭제</th>
                </tr>
            </thead>
            <tbody>
                {members.map(item => 
                <tr key={item.num}>
                    <td>{item.num}</td>
                    <td>{item.name}</td>
                    <td>{item.addr}</td>
                    <td>
                        <button onClick={() => {
                            navigate(`/members/${item.num}/edit`) // 라우트를 만나게 하려고 함!
                        }}>수정</button>
                    </td>
                    <td>
                    <button onClick={() => handleDelete(item.num)}>삭제</button>
                    </td>
                </tr>)
                }
            </tbody>
        </table>
        </>
    );
}

App.js

import { Route, Routes } from 'react-router-dom';
import './App.css';
import Home from './pages/Home';
import Member from './pages/Member';
import MemberInsForm from './pages/MemberInsForm';
import MemberUpdateForm from './pages/MemberUpdateForm';

function App() {
  return (
    <div className="App">
      <Routes>
        <Route path="/" element={<Home/>}/>
        <Route path="/members" element={<Member/>}/>
        <Route path="/members/new" element={<MemberInsForm/>}/>
        <Route path="/members/:num/edit" element={<MemberUpdateForm/>}/>
      </Routes>
    </div>
  );
}

export default App;

MemberInsForm.js

import axios from "axios";
import { useState } from "react";
import { useNavigate } from "react-router-dom";

export default function MemberInsForm() {
    // useNavigate를 사용하기 위한 navigate 객체 생성
    const navigate = useNavigate();
    const [state, setState] = useState({});

    const handleChange = (e) => {
        setState({
            ...state,
            [e.target.name]:e.target.value
        })
    }
    
    const handleSave = () => {
        axios.post("/members", state)
        .then(res => {
            if(res.data.isSuccess) {
                alert("추가 성공");
                navigate("/members"); // 추가 후 목록보기 (Link), App.js에서의 Member로 가는 것이다.
            }
        })
        .catch(error => {
            console.log(error);
        })
    }

    return (
        <>
            <h2>새 회원 입력</h2>
            번호 : <input onChange={handleChange} type="text" name="num" placeholder="번호 입력" /><br/>
            이름 : <input onChange={handleChange} type="text" name="name" placeholder="이름 입력" /><br/>
            주소 : <input onChange={handleChange} type="text" name="addr" placeholder="주소 입력" /><br/>
            <button onClick={handleSave}>추가</button>
        </>
    );
}

MemberUpdateForm.js

import axios from "axios";
import { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";

export default function MemberUpdateForm() {
    // /members/:num/edit 이렇게 들어오기때문에 파라미터값을 받아준다.
    const {num} = useParams();

    const [state, setState] = useState({ // 수정할 회원을 state로 관리, DB로부터 데이터를 받을 예정
        num:0,
        name:"",
        addr:""
    });

    useEffect(() => {
        axios.get("/members/" + num) // 수정할 회원을 읽어오기
        .then(res => {
            setState(res.data); // 넘어온 데이터를 받고 state에 넣어준다.
        })
        .catch(error => {
            console.log(error);
        })
    },[num]) // num값에 변화가 있을 때마다 리렌더링

    const handleChange = (e) => {
        setState({
            ...state,
            [e.target.name]:e.target.value
        })
    }

    const navigate = useNavigate();

    const handleUpdate = () => {
        axios.put("/members/" + num, state)
        .then(res => {
            if(res.data.isSuccess) {
                alert("수정 성공");
                navigate("/members"); // 수정 후 목록 보기 (Link)
            }
        })
        .catch(error => {
            console.log(error);
        })
    }

    return(
        <>
            <h2>회원 수정</h2>
            이름 : <input onChange={handleChange} type="text" name="name" value={state.name} /><br/>
            주소 : <input onChange={handleChange} type="text" name="addr" value={state.addr} /><br/>
            <button onClick={handleUpdate}>수정</button>
        </>
    );
}

결과

Home Component                                                                          Member Component

MemberInsForm Component                                              MemberUpdateForm Component  

삭제 성공                                                   추가 성공                                                   수정 성공