241119 리액트 (Routing)

리액트 (Routing)


라우팅 개발 환경 세팅

외부 라이브러리 설치 (프로젝트별로 설치해야 함)

npm install react-router-dom

1. index.js에서 <App />를 <BrowserRouter> 태그로 감싸주기

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();

2. App.js에서 루트 엘리먼트로 <BrowserRouter>를 사용하기

import { BrowserRouter } from 'react-router-dom';
import MyTest from './mydir/Test';
import About from "./mydir/About"

function App() {
  return (
    <BrowserRouter>
      <div className='App'>
        <h2>라우팅 연습</h2>
        <MyTest />
      </div>
    </BrowserRouter>
  );
}

export default App;

라우팅 실습 1 (워밍업)

App.js

import { BrowserRouter, Link, Routes, Route } from 'react-router-dom';
import MyTest from './mydir/Test';
import About from "./mydir/About"

function App() {
  return (
    <BrowserRouter>
      <div className='App'>
        <h2>라우팅 연습</h2>
        <MyTest />
        <hr/>

        <nav>
          <Link to="/">TEST 화면</Link>&nbsp;|&nbsp;
          <Link to="about">ABOUT 화면</Link>
          <Routes>
            <Route path="/" element={<MyTest/>}></Route>
            <Route path="about" element={<About/>}></Route>
          </Routes>
        </nav>
      </div>
    </BrowserRouter>
  );
}

export default App;

Test.js

import React from "react";

const Test = () => {
    return(
        <h2>Test 문서</h2>
    );
}

export default Test;

About.js

import React from "react";

const HelloAbout = () => {
    return <div>라우터에 대한 소개!</div>
}

export default HelloAbout;

결과

하나의 페이지 내에서 링크 클릭 시 해당 컴포넌트가 보여주는 UI가 보여진다. 실제로는 다른 페이지로 이동하는 것이 아니기에 이 라우터를 이용하면 SPA(Single Page Application)을 구현할 수 있다.

라우팅 실습 2 (워밍업)

App.js

import { BrowserRouter, Link, Routes, Route } from 'react-router-dom';
import MyTest from './mydir/Test';
import About from "./mydir/About"
import Counter from './mydir/Counter';
import Input1 from './mydir/Input1';
import Input2 from './mydir/Input2';
import MultiData from './mydir/MultiData';

function App() {
  return (
    <BrowserRouter>
      <div className='App'>
        <h2>라우팅 연습</h2>
        <MyTest />
        <hr/>

        <nav>
          {/* Link는 a 태그와 같다고 생각해도 된다. 화면 상에 a태그를 나타내는 역할.
          실제로 라우팅 기능은 아래에 적을 <Route>가 실질적인 라우팅 기능을 수행한다. */}
          <Link to="/">TEST</Link>&nbsp;|&nbsp;
          <Link to="about">ABOUT</Link>&nbsp;|&nbsp;
          <Link to="count">친구 추가</Link>&nbsp;|&nbsp;
          <Link to="input1">입력 1</Link>&nbsp;|&nbsp;
          <Link to="input2">입력 2</Link>&nbsp;|&nbsp;
          <Link to="multi">멀티 데이터</Link>&nbsp;|&nbsp;
          {/* element 속성에는 컴포넌트명을 명시. 해당 element의 페이지로 이동된다.
          하나의 싱글 페이지에서 해당 컴포넌트로 이동하기에 SPA 원칙을 유지할 수 있다. */}
          <Routes>
            <Route path="/" element={<MyTest/>}></Route>
            <Route path="about" element={<About/>}></Route>
            <Route path="count" element={<Counter/>}></Route>
            <Route path="input1" element={<Input1/>}></Route>
            <Route path="input2" element={<Input2/>} />
            <Route path="multi" element={<MultiData/>} />
          </Routes>
        </nav>
      </div>
    </BrowserRouter>
  );
}

export default App;

input1.js

import { useState } from "react"


const Input1 = () => {
    const [txtValue, setTxtValue] = useState("");

    const changFunc = (e) => {
        setTxtValue(e.target.value);
    }

    return(
        <div>
        <input type="text" value={txtValue} onChange={changFunc} />
        <br/>
        {txtValue}
        </div>
    );
}

export default Input1;

Input2.js

import { useState } from "react"

const Input2 = () => {
    const [params, setParams] = useState({
        name:'',
        age:'',
        addr:''
    });

    // 구조 분해 할당 이용.
    const {name, age, addr} = params;

    const changeFunc = event => {
        const id = event.target.id;
        const value = event.target.value;

        setParams({
            ...params, // 깊은 복사(구조 분해 할당, destructuring assignment)
            [id]:value // 계산된 프로퍼티. id내의 값이 실질적 프로퍼티가 된다.
            // 예를 들어, id='name'일 경우 => [id]: value => name:'염정섭'
            // 덮어쓰기
        });
    }

    return (
        <div>
            <br/>
            <div>
                이름 : <input type="text" value={name} id="name" onChange={changeFunc} />
            </div>
            <div>
                나이 : <input type="text" value={age} id="age" onChange={changeFunc} />
            </div>
            <div>
                주소 : <input type="text" value={addr} id="addr" onChange={changeFunc} />
            </div>
            <br/>
            <table>
                <tr>
                    <td>이름 : {name}</td>
                    <td>나이 : {age}</td>
                    <td>주소 : {addr}</td>
                </tr>
            </table>
        </div>
    );
}

export default Input2;

MultiData.js

// 멤버 배열 자료 처리용
const MemberComp = ({memeberData}) => {
    return (
        <tr>
            <td>{memeberData.name}</td>
            <td>{memeberData.tel}</td>
        </tr>
    );
};

const MultiData = () => {
    const members = [
        {name:"가가가", tel:"010-1111-2222"},
        {name:"나나나", tel:"010-3333-4444"},
        {name:"다다다", tel:"010-5555-6666"},
    ];
    
    return (
        <table>
            <thead>
                <tr>
                    <th>이름</th>
                    <th>번호</th>
                </tr>
            </thead>
            <tbody>
                {members.map((mem, idx) =>
                    (<MemberComp key={idx} memeberData={mem} />)
                )}
            </tbody>
        </table>
    )
}

export default MultiData;

결과

입력 1

입력 2

여러 데이터


라우팅 실습 3 (AJAX)

실습을 위해 이클립스를 킨다. JSP로 작성!

“Dynamic Web Project”로 새 프로젝트 폴더 생성

practice.jsp

<%@ page language="java" contentType="text/plain; charset=UTF-8"
    pageEncoding="UTF-8"%>
{
	"items": 
	[
		{"id":1, "name":"바닐라라떼", "price":"4500"},
		{"id":2, "name":"아메리카노", "price":"1500"},
		{"id":3, "name":"아인슈페너", "price":"5500"}
	]
}
plain으로 변경하는 이유! JSON 객체로 넘기기 위함, json으로 바꾸어주어도 괜찮다.

서버 실행 후 JSON 형식으로 만들어진 것을 확인!

리액트로 AJAX 요청해보자!

package.json

CROS 문제를 해결하기 위해 package.json에 서버 주소를 입력!
"proxy": "http://localhost:8080"
{
  "name": "my-app13route",
  "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.7.7",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-router-dom": "^6.28.0",
    "react-scripts": "5.0.1",
    "web-vitals": "^2.1.4"
... 생략
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "proxy": "http://localhost:8080"
}

MyProduct.js

import React, { useEffect, useState } from "react";

const MyProduct = () => {
    const [error, setError] = useState(null);
    const [isLoaded, setIsLoaded] = useState(false);
    const [items, setItems] = useState([]);

    // 컴포넌트가 mount된 후 AJAX 요청을 수행
    useEffect(() => {
        // 같은 도메인에 대해서는 도메인 생략이 가능하다.
        fetch(`/abcReact/practice.jsp`, {method:'GET'})
        .then(response => {
            if(!response.ok) {
                throw new Error("HTTP Responsw Status Not OK");
            }
            return response.json();
        })
        .then(result => {
            setIsLoaded(true); // 로드 성공
            setItems(result.items); // AJAX로 가져온 데이터를 state에 저장
        },
        error => {
            setIsLoaded(true);
            setError(error);
            console.log(error);
        });
    },[]); // 매 렌더링 후에만 1회 실행

    if (error) {
        return <div>Error : {error.message}</div>;
    }
    if (!isLoaded) {
        return <div>Loading</div>
    }

    return (
        <ul>
            {items.map(item => (
                <li key={item.id}>
                    <p>상품명 : {item.name}</p>
                    <p>가격 : {item.price}원</p>
                </li>
            ))}
        </ul>
    );
}

export default MyProduct;

AJAX 요청 결과 확인

리액트에서는 fetch보다 axios 사용!

먼저 axios 의존성 추가 : npm install axios

MyProduct2.js

import axios from "axios";
import { useState, useEffect } from "react";

const MyProduct2 = () => {
    const [error, setError] = useState(null);
    const [isLoaded, setIsLoaded] = useState(false);
    const [items, setItems] = useState([]);

    // 컴포넌트가 mount된 후 AJAX 요청을 수행
    useEffect(() => {
        // 같은 도메인에 대해서는 도메인 생략이 가능하다.
        axios(`/abcReact/practice.jsp`, {method:'GET'})
        .then(response => {
            setIsLoaded(true) // 로드 성공
            setItems(response.data.items) // 서버에서 받아온 자료로 상태(state) 갱신
        })
        .catch(error => {
            setIsLoaded(true);
            setError(error);
            console.log(error);
        })
    },[]); // 매 렌더링 후에만 1회 실행

    if (!isLoaded) {
        return <div>Loading</div>
    }

    if (error) {
        return <div>Error : {error.message}</div>;
    }
    
    return (
        <ul>
            {items.map(item => (
                <li key={item.id}>
                    <p>상품명 : {item.name}</p>
                    <p>가격 : {item.price}원</p>
                </li>
            ))}
        </ul>
    );
};

export default MyProduct2;

App.js

import { BrowserRouter, Link, Routes, Route } from 'react-router-dom';
import MyTest from './mydir/Test';
import About from "./mydir/About"
import Counter from './mydir/Counter';
import Input1 from './mydir/Input1';
import Input2 from './mydir/Input2';
import MultiData from './mydir/MultiData';
import MyProduct from './mydir/MyProduct';
import MyProduct2 from './mydir/MyProduct2';

function App() {
  return (
    <BrowserRouter>
      <div className='App'>
        <h2>라우팅 연습</h2>
        <MyTest />
        <hr/>

        <nav>
          {/* Link는 a 태그와 같다고 생각해도 된다. 화면 상에 a태그를 나타내는 역할.
          실제로 라우팅 기능은 아래에 적을 <Route>가 실질적인 라우팅 기능을 수행한다. */}
          <Link to="/">TEST</Link>&nbsp;|&nbsp;
          <Link to="about">ABOUT</Link>&nbsp;|&nbsp;
          <Link to="count">친구 추가</Link>&nbsp;|&nbsp;
          <Link to="input1">입력 1</Link>&nbsp;|&nbsp;
          <Link to="input2">입력 2</Link>&nbsp;|&nbsp;
          <Link to="/kbs/product">상품 정보 (AJAX)</Link>&nbsp;|&nbsp;
          <Link to="/kbs/product2">상품 정보 (axios)</Link>&nbsp;|&nbsp;
          {/* element 속성에는 컴포넌트명을 명시. 해당 element의 페이지로 이동된다.
          하나의 싱글 페이지에서 해당 컴포넌트로 이동하기에 SPA 원칙을 유지할 수 있다. */}
          <Routes>
            <Route path="/" element={<MyTest/>}></Route>
            <Route path="about" element={<About/>}></Route>
            <Route path="count" element={<Counter/>}></Route>
            <Route path="input1" element={<Input1/>}></Route>
            <Route path="input2" element={<Input2/>} />
            <Route path="multi" element={<MultiData/>} />
            <Route path="/kbs/product" element={<MyProduct/>} />
            <Route path="/kbs/product2" element={<MyProduct2/>} />
          </Routes>
        </nav>
      </div>
    </BrowserRouter>
  );
}

export default App;

axios 확인!


라우팅 실습 4 (AJAX -axios / DB)

DB 데이터를 가져와 라우팅 실습을 해보자!
JSP를 사용할 예정이어서, 이클립스 내에 maridb driver를 넣어주자

practice.jsp

<%@page import="java.sql.DriverManager"%>
<%@page import="java.sql.ResultSet"%>
<%@page import="java.sql.PreparedStatement"%>
<%@page import="java.sql.Connection"%>
<%@ page language="java" contentType="text/plain; charset=UTF-8"
    pageEncoding="UTF-8"%>

<%--
{
	"items": 
	[
		{"id": 1, "name": "바닐라라떼", "price": "4500"},
		{"id": 2, "name": "아메리카노", "price": "1500"},
		{"id": 3, "name": "아인슈페너", "price": "5500"}
	]
}
--%>

{
	"items": [
<%
	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 sangdata");
		rs = pstmt.executeQuery();
		
		while(rs.next()) {
			// {"id": 1, "name": "바닐라라떼", "price": "3000"},
			/*
			String strFormat = """
					{\"id\": %s, \"name\": %s, \"price\": %d},
					""".trim();
			result += String.format(strFormat,
					rs.getString("code"),
					rs.getString("sang"),
					rs.getInt("su") * rs.getInt("dan")
			);*/
			
			result += "{";
			result += "\"id\":" + "\"" + rs.getString("code") + "\",";
			result += "\"name\":" + "\"" + rs.getString("sang") + "\",";
			result += "\"price\":" + "\"" + (rs.getInt("su") * rs.getInt("dan")) + "\"";
			result += "},";
			
		}
		
		if (result.length() > 0) {
			result = result.substring(0, result.length() - 1);
		}
			
		out.print(result);
	} 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();
	}
	
%>
	]
}

서버에서 DB 데이터를 가져온 것을 확인

리액트에서도 확인!

참고! 로딩중 메세지 띄워보기!

practice.jsp

<%@page import="java.sql.DriverManager"%>
<%@page import="java.sql.ResultSet"%>
<%@page import="java.sql.PreparedStatement"%>
<%@page import="java.sql.Connection"%>
<%@ page language="java" contentType="text/plain; charset=UTF-8"
    pageEncoding="UTF-8"%>

<%--
{
	"items": 
	[
		{"id": 1, "name": "바닐라라떼", "price": "4500"},
		{"id": 2, "name": "아메리카노", "price": "1500"},
		{"id": 3, "name": "아인슈페너", "price": "5500"}
	]
}
--%>

{
	"items": [
<%
	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 {
		Thread.sleep(3000); // 지연을 재현하기 위한 코드 추가
		
		pstmt = conn.prepareStatement("SELECT * FROM sangdata");
		rs = pstmt.executeQuery();
		
		while(rs.next()) {
			// {"id": 1, "name": "바닐라라떼", "price": "3000"},
			/*
			String strFormat = """
					{\"id\": %s, \"name\": %s, \"price\": %d},
					""".trim();
			result += String.format(strFormat,
					rs.getString("code"),
					rs.getString("sang"),
					rs.getInt("su") * rs.getInt("dan")
			);*/
			
			result += "{";
			result += "\"id\":" + "\"" + rs.getString("code") + "\",";
			result += "\"name\":" + "\"" + rs.getString("sang") + "\",";
			result += "\"price\":" + "\"" + (rs.getInt("su") * rs.getInt("dan")) + "\"";
			result += "},";
			
		}
		
		if (result.length() > 0) {
			result = result.substring(0, result.length() - 1);
		}
			
		out.print(result);
	} 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();
	}
	
%>
	]
}

문제!

1. 라우트 사용하기
2. 백엔드는 Spring Boot와 Spring data JPA 사용