여기까지 수정하는 과정 중 혹시 접속이 안될까봐 한번 접속을 해봤는데

다행히 접속은 제대로 되고 있다
포트번호는 8081로 사용중인데
예제로 쓰고있던 스프링 프로젝트와 개인 프로젝트를 구별하기 위해서 포트번호를 바꿔줬다
스프링 부트 서버는 application.yml 파일에 설정된 포트 번호를 기준으로 돌아가기 때문에
설정 파일의 port 번호와 브라우저 주소창의 번호를 일치시켜야 한다!
이제 sts 에 있는 jsp 를 리액트로 옮기려고 한다
혹시 jsp 를 리액트로 옮기면 문제가 생기지 않을까 걱정했는데
기능적으로는 문제가 없지만, 두 기술 사이의 차이로 인한 문법 변환이나 재설계가 필요하다
일단 기존 프로젝트의 jsp 를 그대로 리액트로 옮겨서
제대로 페이지가 나오는지 확인 먼저 한 다음에
피그마 디자인을 입혀보자
일단 프론트엔드 폴더에 하위 폴더를 만들어준다
컴포넌트 폴더는 재사용되는 부품이 들어가고 (로고, 버튼, 헤더 푸터 등등)
페이지는 화면 구성 전체가 들어간다
리액트에서 사용하는 파일은 첫글자를 대문자로 쓰는 것이 관례이다

그냥 src 폴더 아래에 새 폴더를 만들어주면 된다
jsp 방식은 어떤 요청(리스트 보여줘)이 들어오면 서버가 새로운 HTML 파일을 통째로 보내준다
이 때 화면이 깜빡거리면서 새로고침이 된다
하지만 리액트의 라우터 방식은 주소를 감시하고 있다가 주소가 바뀌면 해당하는 컴포넌트만 갈아끼워서 보여준다
즉 화면 깜빡임 없이 부드럽게 작동한다
리액트는 index.html 이라는 파일 위에서 돌아가는 SPA(단일 페이지 모던 웹 애플리케이션) 방식으로 작동한다
파일이 하나이기 때문에 사용자가 /article/list /login 같은 주소로 접속했을 때 각각 다른 화면을 보여줘야 하기 때문에 라우터가 일종의 길잡이 역할을 하게 된다
BrowserRouter: 라우팅 기능을 사용할 전체 영역을 감싸는 울타리
Routes: 여러 개의 길(Route) 중 현재 주소와 일치하는 딱 하나를 선택
Route: 특정 주소(path)와 그 주소에서 보여줄 페이지(element)를 연결
예: <Route path="/login" element={<Login />} />
Link: 기존 HTML의 <a> 태그 대신 사용, 페이지를 새로고침하지 않고 주소만 바꿔주는 버튼
리액트 라우터 설치는 깃배쉬로 할 예정이다
프론트엔드 폴더에서 깃배쉬 창을 연 다음
npm install react-router-dom
명령어를 입력하면 깃배쉬께서 알아서 설치를 해주신다


길잡이 코드는 App.js 파일에 쓰면 되는데

원래 적혀있던 코드는 싹 지워도 된다
왜?? : 리액트 설치할 때 자동으로 제공되는 일종의 샘플이기 때문에
코드 내용
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// -- 공통 컴포넌트 불러오기 --
import Header from './components/common/Header';
import Footer from './components/common/Footer';
// -- 각 페이지 컴포넌트 불러오기 --
import Main from './pages/home/Main';
import List from './pages/article/List';
import Detail from './pages/article/Detail';
import Write from './pages/article/Write';
import Login from './pages/member/Login';
import Join from './pages/member/Join';
function App() {
return (
<Router>
<div className="App">
{/* -- 모든 페이지에서 공통으로 보이는 상단바 -- */}
<Header />
{/* -- 주소에 따라 바뀌는 콘텐츠 영역 -- */}
<Routes>
{/* localhost:3000/ 접속 시 메인 페이지 */}
<Route path="/" element={<Main />} />
{/* 게시판 관련 주소들 */}
<Route path="/article/list" element={<List />} />
<Route path="/article/detail" element={<Detail />} />
<Route path="/article/write" element={<Write />} />
{/* 회원 관련 주소들 */}
<Route path="/member/login" element={<Login />} />
<Route path="/member/join" element={<Join />} />
</Routes>
{/* -- 모든 페이지에서 공통으로 보이는 하단바 -- */}
<Footer />
</div>
</Router>
);
}
export default App;
주소창에 파라미터가 뭐가 들어오는지에 따라서 가는 길을 다르게 해주고 있다
하지만 지금은 각 js 파일이 텅텅 비어있어서 무슨 내용이라도 채워줘야 한다
import React from 'react';
function Main() { // 파일 이름에 맞춰서 이름 바꾸기
return <div>메인</div>;
}
export default Main; // 파일 이름에 맞춰서 이름 바꾸기
이렇게 만들어놓은 js 파일을 대강 채워넣는다
그리고 vs code 작업 확인은 localhost:3000 주소에서 확인할 수 있다
8081 은 스프링 개발 환경
3000 은 리액트 개발 환경
이제 테스트 결과물을 확인하려고
npm start 명령어를 입력했는데PS C:\jje_works\sts5.0.1_workspace\docentNote\frontend> npm start
npm : 이 시스템에서 스크립트를 실행할 수 없으므로 C:\Program Files\nodejs\npm.ps1
파일을 로드할 수 없습니다. 자세한 내용은 about_Execution_Policies(https://go.micros
oft.com/fwlink/?LinkID=135170)를 참조하십시오.
위치 줄:1 문자:1
+ npm start
+ ~~~
+ CategoryInfo : 보안 오류: (:) [], PSSecurityException
+ FullyQualifiedErrorId : UnauthorizedAccess
일단 메세지가 떴으니 검색을 해본다
npm : 이 시스템에서 스크립트를 실행할 수 없으므로
이 검색어로 검색해보니 많은 결과가 나왔다
아무래도 흔하게 나타나는 오류인모양
시스템의 설정에서 스크립트를 실행할 수 있는 권한이 부여되어있지 않은 상태라서
해당 작업 명령을 수행할 수 없어서 발생하는 오류
이미 데이터가 너무 많아서 아무 블로그에나 들어가도 똑같은 해결 방법만 존재한다
권한 설정만 바꿔주면 끝
Windows PowerShell 을 관리자 권한으로 열어준다
꼭 관리자 권한으로 실행해야 한다
난 그냥 실행했는데 갑자기 빨간 오류가 우르르 떠서 당황했다
이 설정을 바꾸려면 관리자 권한으로 실행해야 한다고...
get-ExecutionPolicy 권한 상태를 확인하는 명령어이다

권한 상태값 "Restricted" == 로컬에서 작성한 스크립트를 실행할 수 없는 상태
모든 권한 상태값 목록
Restricted : default설정값으로, 스크립트 파일을 실행할 수 없습니다.
AllSigned : 신뢰할 수 있는(서명된) 스크립트 파일만 실행할 수 있습니다.
RemoteSigned : 로컬에서 본인이 생성한 스크립트와, 신뢰할 수 있는(서명된) 스크립트 파일 실행할 수 있습니다.
Unrestricted : 모든 스크립트 실행가능
ByPass : 경고/차단 없이 모든 것을 실행가능하도록함
Undefined : 권한을 설정하지 않겠음
여기서 우리는 설정을 RemoteSigned 로 바꿔야 한다
> Set-ExecutionPolicy RemoteSigned
질문창이 뜨면
> Y
명령어를 입력하면 권한이 바뀐다

쓴 명령어를 박스처리 해놨다
다시 vs code 터미널에서 npm start 를 입력하면

성공했다는 메세지와 함께

제대로 페이지가 뜨기 시작한다
속성 이름 변경 (가장 중요)
class → className: HTML의 class는 자바스크립트의 예약어라 리액트에서는 className 으로 바꿔야 한다
onclick → onClick: 모든 이벤트 속성은 카멜 케이스(CamelCase)를 사용
모든 태그는 반드시 닫기
JSP에서는 <br>, <img>, <input> 등을 닫지 않아도 됐지만, 리액트에서는 반드시 끝에 /를 붙여야 한다
<br />
<img src="..." />
<input type="text" />
외부 라이브러리 이동이 필요한데
헤더에 있는 외부 라이브러리를 이동시켜서 리액트 프로젝트 전체에서 쓰기 편하게 할 수 있다
<!-- 제이쿼리 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<!-- 폰트어썸 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<!-- 폰트어썸 FREE 아이콘 리스트 : https://fontawesome.com/v5.15/icons?d=gallery&p=2&m=free -->
<!-- 테일윈드 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.1.4/tailwind.min.css">
<!-- 테일윈드 치트시트 : https://nerdcave.com/tailwind-cheat-sheet -->
<!-- 데이지 UI -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/daisyui/4.12.23/full.css" />
public 폴더의 index.html 파일이 있는데, 미리 제공되어 있는 초록색 주석은 싹 삭제한다
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using create-react-app" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>docent note.</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/daisyui/4.12.23/full.css" />
</head>
<body>
<noscript>이 앱을 실행하려면 자바스크립트가 필요합니다.</noscript>
<div id="root"></div>
</body>
</html>
이렇게 리액트 문법에 맞게 변환해주고
남은 헤더 부분도 리액트 문법에 맞게 변환한다
import React from 'react';
import { Link } from 'react-router-dom'; // -- 페이지 새로고침 방지를 위해 사용 --
function Header() {
// -- 로그인 상태를 확인하는 임시 변수 (나중에 서버와 연결) --
const isLogined = false;
// -- 로그아웃 클릭 시 확인창 --
const handleLogout = (e) => {
if (!window.confirm('로그아웃 하시겠습니까?')) {
e.preventDefault(); // -- 취소 클릭 시 페이지 이동 방지 --
}
};
return (
<header>
{/* -- class -> className으로 변경 -- */}
<div className="flex h-20 mx-auto items-center text-3xl container">
{/* -- a href -> Link to로 변경 (리액트 방식) -- */}
<Link to="/">LOGO</Link>
<div className="flex-grow"></div>
<ul className="flex gap-4">
<li className="hover:underline">
<Link to="/">HOME</Link>
</li>
<li className="hover:underline">
<div className="dropdown dropdown-hover">
{/* -- tabindex 등 속성 이름 유지 -- */}
<div tabIndex="0" role="button">LIST</div>
<ul tabIndex="0" className="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow text-sm">
<li><Link to="/article/list?boardId=1">NOTICE</Link></li>
<li><Link to="/article/list?boardId=2">FREE</Link></li>
<li><Link to="/article/list?boardId=3">QnA</Link></li>
</ul>
</div>
</li>
{/* -- <c:if> 대신 삼항 연산자나 논리 연산자 사용 -- */}
{!isLogined ? (
<>
<li className="hover:underline">
<Link to="/member/login">LOGIN</Link>
</li>
<li className="hover:underline">
<Link to="/member/join">JOIN</Link>
</li>
</>
) : (
<>
<li className="hover:underline">
<Link to="/article/write">WRITE</Link>
</li>
<li className="hover:underline">
{/* -- onclick -> onClick으로 변경 -- */}
<Link to="/member/doLogout" onClick={handleLogout}>LOGOUT</Link>
</li>
</>
)}
</ul>
</div>
</header>
);
}
export default Header;
Link: <a href="...">를 그대로 쓰면 페이지가 새로고침되어 리액트의 장점이 사라진다
그래서 react-router-dom의 <Link to="...">를 사용
className: HTML의 class 속성은 리액트에서 무조건 className으로 적어야 에러가 나지 않는다
tabIndex: 소문자로만 된 속성은 리액트에서 카멜케이스로 변환되어서 tabIndex 로 표기한다
JSP의 <c:if>는 자바스크립트의 {조건 && (결과)} 또는 {조건 ? (참) : (거짓)} 문법으로 대체한다
요약
인덱스: 외부 라이브러리 링크만 깔끔하게 보관
헤더: 리액트 문법으로 옷을 갈아입은 실제 메뉴 부품
푸터에도 들어갈 내용이 있긴 하지만 아직 뭐 들어갈게 없으니 프로젝트 이름만 넣어준다

일단 무조건 리액트 문법으로 변환해서 상세보기 페이지를 변환하고 있었는데

컴파일 오류가 떴다
제이쿼리라는 모듈을 찾을 수 없다는 오류
그럼 설치를 해줘야지?
이것도 너무 쉽게 해결할 수 있는 오류인데
그냥 터미널에서 설치만 해주면 된다

npm start가 돌아가고 있는 터미널에서
Ctrl + C 를 누르고 y를 입력해 현재 실행 중인 서버를 잠시 멈추고
npm install jquery == 제이쿼리 설치
npm start == 서버 재시작

그럼 오류화면이 사라진다
jsp 를 변환하는 작업은 그냥 다 반복작업이라
페이지마다 변환하는 작업 기록은 스킵하고 중간에 나오는 트러블 슈팅만 추가로 기록하기로 했다
일단 컴파일 오류 없이 페이지는 다 뜨지만
DB 에서 데이터를 불러올 수 없는 상황이다

테스트 게시글을 카테고리마다 만들어뒀는데 목록에 뜨지 않는다
살펴보면
리액트에서는 데이터를 요청하고 있는데
스프링에서는 데이터를 보내주는 코드는 없고, jsp 에 화면을 보여주는 코드만 있다
하지만 리액트로 프론트를 전환하면 jsp 는 사용할 필요가 없어지기 때문에
jsp 경로를 리턴하는 대신, 리액트가 이해할 수 있는 json 데이터를 보내주는 방식으로 수정해야한다
나는 여기서 기존 컨트롤러 코드의 수정을 최소화 하고 싶었다
여기서 컨트롤러 클래스를 아예 갈아 엎어야 하는데, 이제 스프링은 jsp 로 화면을 보여주는게 아니라 프론트를 담당하게 된 리액트로 데이터를 넘겨줘야 한다

컨트롤러를 RestController 로 변경

기존 메서드는 주석처리 하고 리액트에서 사용할 만한 데이터를 보내주는 메서드를 만든다
여기서 기존에 쓰던 model 대신 Map 을 사용하는 이유는 Json 데이터 형식이 Map 과 같은 키밸류 방식이기 때문이다
그래서 데이터 변환이 아주 쉬워진다

리액트는 ajax 가 아니라 axios 를 사용한다
그리고 axios 는 따로 설치를 해야하는 도구이기 때문에, 오류 2번에서 한 것 처럼 서버 실행중인 터미널을 열고 잠시 서버를 멈춘 뒤 axios 를 설치하고 다시 서버를 실행해줬다
ctrl + c
npm install axios
npm start
이렇게 하고 다시 서버를 시작해줬는데

에러 메세지를 잘 살펴보자................
404 : 사용자가 웹사이트의 특정 페이지나 파일을 요청했으나 서버가 이를 찾지 못했을 때 발생하는 대표적인 클라이언트 측 HTTP 오류
그러니까 지금 리액트가 스프링 서버를 찾지 못했다는 뜻
리액트는 3000번 포트에서 돌고 있고, 스프링 부트는 8081번 포트에서 돌고 있기 때문에 발생한 문제겠지???
여기서 프록시 설정이란 걸 해야한다는 걸 알았는데
프록시(Proxy)는 '대리인'이라는 뜻으로, 컴퓨터 네트워크에서 클라이언트(사용자)와 서버 사이에서 중계 역할을 하는 서버 또는 프로그램
그러니까 리액트에게 "모르는 주소로 요청을 보내면 스프링 서버로 물어봐!"라고 알려줘야 한다는 것
리액트 프로젝트 폴더(frontend) 안에 있는 package.json 파일을 열고
proxy 설정을 추가해야 하는데

밑에 새 설정을 추가할 땐 진짜 반드시 쉼표를 넣어줘야한다 이걸 빼면 문법오류이다
설정은 무조건 중괄호 안쪽에 추가해줘야 하고
그리고 다시 터미널에서 Ctrl + C로 서버를 끄고
npm start를 입력해서 서버를 켜준다
설정 파일을 바꿨을 때는 반드시 서버를 껐다 켜야 적용이 된다
프록시 설정까지 끝냈는데도 아직도 오류가 사라지지 않고 있다
8081 에서는 제대로 데이터가 보이는 상황이니까 리액트의 요청이 스프링까지 제대로 가지도 않고 있다는건데
이 문제를 찾아보니까 Cross Origin 개념이라는걸 찾았다
CORS(Cross-Origin Resource Sharing)
오리진(Origin)은 프로토콜(http) + 도메인(localhost) + 포트번호(3000/8081) 3개가 합쳐진 주소
이 중 포트번호 하나만 달라도 브라우저는 "어? 다른 동네네?"라고 인식해서보안상데이터를 못 가져오게 막아버린다, 이게 CORS 정책
보안상 차단
기본적으로 서버(스프링)는 외부(리액트)에서 자기 데이터를 마음대로 가져가는 걸 싫어한다
그래서 "누가 나한테 접근해도 되는지" 허락 리스트를 가지고 있어야 한다

그래서 리스트js 에 스프링 주소를 풀로 싹 때려넣어서 리액트가 어디로 가야할지 정해주고

컨트롤러 클래스의 어노테이션에 @CrossOrigin 을 추가하고 스프링 서버의 Origin 을 때려넣는다
@CrossOrigin의 역할
"나(8081)는 리액트(3000)가 보내는 요청은 안전하다고 믿으니까 데이터를 보낼것이다"라고 공식적으로 허가 도장을 찍어주는 것과 같다...고 한다!!
이렇게 하니까 제대로 DB에서 데이터를 가지고 온다...
이제 캡쳐따서 표시하는게 너무 힘들어졌다...너무너무힘들다 시간도 오래걸리고...
이제 리스트에서는 제대로 게시글이 뜨는데 상세보기 페이지가 먹통이었다
오류메세지 뜨는건 없었지만 흰 페이지밖에 보이지 않는 상황
이것도 오류 3번이랑 다를게 없는 문제였다
스프링에서는 jsp 로 화면을 보여주고만 있어서 detail 메서드 대공사가 필요했다
여러가지 복합적인 문제들을 단계별로 해결해야 했다
주소창에 주소를 쳐도 화면이 하얗게만 나오고 아무것도 안 뜨는 근본적인 이유는 주소 불일치 때문인데
App.js에는 /article/detail로 등록했지만
스프링에서 정의한 루트는 /usr/article/detail
해결 방법: App.js의 path URL을 스프링 서버에서 정의한 URL 과 토씨 하나 틀리지 않게 일치시켰다
화면(헤더, 푸터)은 나오는데, 본문 내용이 "불러오는 중..."에서 멈춰있는 상태
결과적인 원인은 포트 번호 누락 때문이었다
리액트(3000번)가 스프링(8081번) 주소를 적을 때 http://localhost:8081을 빼먹고 상대 경로만 적었다
즉 3-4 번 문제와 같은 CORS 거부 상황
자바 컨트롤러가 데이터를 보내야 하는데, 계속 JSP 파일을 찾으려 하고 있다
그래서 컨트롤러에 @ResponseBody와 @CrossOrigin을 추가하고, 리액트 요청 주소에 리액트 전체 오리진을 작성
서버에 요청은 가는데, id값을 찾을 수가 없어서 게시글 상세 내용을 가져올 수 없었다
그 이유는, 스프링에서 설계한 주소창은 ?id=1 형태인데
리액트로 코드를 변환할 때 useParams 를 사용하고 있었다
useParams 는 url 이 /detail/1 형태일 때만 작동
useParams 대신에
useSearchParams를 import 하고
searchParams.get('id')를 사용해 쿼리 스트링 값을 정확히 읽어낸다
이제 여기까지는 게시글이 보이게 되긴 했는데 다른 문제가 또 생겼다
리스트에서 다른 카테고리를 눌러도 이동이 되지 않았다
useEffect가 처음 켜질 때만 작동하고, 주소창의 boardId가 바뀔 때는 가만히 있었던게 원인
useEffect의 대괄호 안에 [searchParams] 혹은 [boardId]를 넣어 주소가 바뀔 때마다 다시 실행되게 만들면 된다
// 핵심 로직: URL(searchParams)이 바뀔 때마다 실행
// 이제 메뉴를 클릭해서 URL의 boardId가 바뀌면 이 useEffect가 즉시 감지하고 데이터를 새로 가져옴
useEffect(() => {
fetchArticles();
}, [searchParams]); // 👈 URL이 변하면 무조건 다시 실행
리액트 주소(App.js)와
사용자 주소는 100% 같아야 한다
리액트에서 서버 요청할 때는
반드시 8081 포트 주소를 다 적는다
스프링 컨트롤러에는
@ResponseBody와 @CrossOrigin 이 필수다
화면이 안 보이면 F12를 눌러
Console(오류 메시지) 과
Network(통신 여부) 를 먼저 확인한다
솔직히 진짜 하나도 모르겠다
다른거 다 손도 못대고 이것만 했는데 연휴 3일 남아서
기능 옮기기는 미루고 다음 포스팅에서 디자인만 옮기는걸로 마무리 하겠다 ...