React
SPA
router/index.jsx 자동 인식
index 라서 해당 폴더명까지만 import 해도 자동으로 index 파일을 가져와 사용한다import router from './router'
라우팅 설정을 컴포넌트별로 파일로 분리하면 코드가 더 깔끔하고 유지보수가 쉬워진다
const paths = [
{path:"/members", element:<Member/>},
{path:"/members/:num", element:<MemberDetail/>}
]
export default paths;
import paths from "./member"
const routes = [
...paths
];
# 없이 자연스러운 url 주소를 표시하고 싶으면 사용
단 클라이언트가 새로고침을 누르면 문제 발생
➜ createHashRouter 사용 : 새로고침 대응 가능
router2 추가const router2 = createBrowserRouter([{
path:"/",
element:<App/>,
children: routes.map((route)=>{
return {
index: route.path === "/", //자식의 path 가 "/" 면 index 페이지 역활을 하게 하기
path: route.path === "/" ? undefined : route.path, // path 에 "/" 두개가 표시되지 않게
element: route.element //어떤 컴포넌트를 활성화 할것인지
}
})
}]);
export default router2;
라우트 경로에 :변수명 형태를 사용하면 클라이언트의 현재 URL에 따라 다른 값을 변수로 받을 수 있다
{path:"/members/:num", element:<MemberDetail/>}
/members/1
/members/2
/members/3
C:\Users\USER>node
> const arr = [10, "kim"];
undefined
> const [a, b] = arr;
undefined
> const obj = {num:10, name:"park"};
undefined
> const {num} = obj;
undefined
> num
10
> const {name} = obj;
undefined
> name
'park'
URL 파라미터를 포함한 객체(object)를 반환하는 React Router hook
import { useParams } from 'react-router-dom';
const {num} = useParams();
function MemberForm() {
return (
<>
<h1>회원 추가 양식</h1>
<form action="">
</form>
</>
);
}
MemberForm 라우트 경로 추가{path:"/members/new", element:<MemberForm/>}
<NavLink to="/members/new">회원 추가</NavLink>
function MemberForm() {
return (
<>
<h1>회원 추가 양식</h1>
<form action="/api/v1/members" method="post">
<div>
<label htmlFor="name">이름</label>
<input type="text" name="name" id="name"/>
</div>
<div>
<label htmlFor="addr">주소</label>
<input type="text" name="addr" id="addr"/>
</div>
<button type="submit">저장</button>
</form>
</>
);
}
onSubmit 이벤트 핸들러 추가<form action="/api/v1/members" method="post" onSubmit={handleSubmit}>
e.preventDefault()e.target// form에 submit 이벤트가 발생했을 때 실행할 함수
const handleSubmit = (e) => {
// 기본 동작 막기 (form 제출 막기)
e.preventDefault();
// 이벤트가 발생한 요소의 참조값 (form 요소의 참조값)
const form = e.target;
// 폼에 입력한 내용을 담고 있는 FormData 객체를 얻어낸다
const formData = new FormData(form);
// 폼에 입력한 내용을 object로 변환
const obj = Object.fromEntries(formData);
// object를 JSON 문자열로 변환
const json = JSON.stringify(obj);
// fetch() 함수를 이용해서 전송하기
fetch(form.action, {
method: form.method, // 요청 method
headers: { "Content-Type": "application/json" }, // 요청 header
body: json // 요청 body
})
.then(res => res.json())
.then(data => {
// 방금 추가한 회원의 정보가 data 에 들어 있다
});
};


useNavigate() 함수 추가const navigate = useNavigate();
navigate() 코드 추가"/members/x" 위치로 이동<NavLink to="/members/x">이동</NavLink> 이런 링크를 눌른 효과navigate(`/members/${data.num}?message=success`);
http://localhost:5173/#/members/6?message=success
useSearchParams() 함수 추가useSearchParams() : query 파라미터를 추출할 때 사용하는 hookconst [params] = useSearchParams();
console.log(params);
// success or null
console.log(params.get("message"));
return (
<>
{params.get("message") && <Alert className="m-4" variant="success">저장 완료</Alert>}
</>
);
src 폴더 안에 componenets 폴더 생성
import { NavLink } from 'react-router-dom';
function Location() {
return<>
<nav>
<ol className="breadcrumb">
<li className="breadcrumb-item">
<NavLink to="/">Home</NavLink>
</li>
</ol>
</nav>
</>
}
Location 컴포넌트 사용<Location/>
list 파라미터 추가Location({list}) : props 로 전달되는 값 중에서 list 만 구조 분해 할당을 해서 담겠다는 의미<Location list={["aaa", "bbb", "ccc"]}/> 형식으로 사용한다면["aaa", "bbb", "ccc"] 배열이 들어 있다function Location(list) {
return<>
<nav>
<ol className="breadcrumb">
<li className="breadcrumb-item">
<NavLink to="/">Home</NavLink>
</li>
</ol>
</nav>
</>
}
Location 에 list 전달<Location list={["회원 목록", "자세히 보기"]}/>
bread 경로 정보 배열 추가const bread = [
{name:"Members", to:"/members"},
{name:"Detail"}
];
Location 에 경로 배열 넘기기<Location list={bread}/>
map 으로 breadcrumb 렌더링to 값이 있으면 링크로 표시to 값이 없으면 현재 위치(active) 로 표시function Location({list}) {
return<>
<nav>
<ol className="breadcrumb">
<li className="breadcrumb-item">
<NavLink to="/">Home</NavLink>
</li>
{list.map(item=>
<li className={`breadcrumb-item ${item.to ? '':'active'}`}>
{item.to ? <NavLink to={item.to}>{item.name}</NavLink> : item.name }
</li>
)}
</ol>
</nav>
</>
}

bread 경로 정보 배열 추가const bread = [
{name:"Members", to:"/members"},
{name:"New Form"}
];
Location 에 경로 배열 넘기기<Location list={bread}/>

axios 설치
axios 검색 후 클릭

npm i axios
MemberDetail.jsx 에 수정, 삭제 버튼을 만들고 동작하도록 만들어 보세요
수정은 MemberUpdateForm.jsx 컴포넌트를 만들어서 동작하도록 하고
삭제는 정말로 삭제 하시겠습니까? 라는 bootstrap Modal 을 띄워서 동작하도록 해 보세요
"/members/1/edit" ➜ 수정 routing 경로
Location.jsx 파일명을 Bread.jsx 로 수정
MemberUpdateForm 라우트 경로 추가{path:"/members/:num/edit", element:<MemberUpdateForm/>}
<Button variant="success" as={NavLink} to={`/members/${dto.num}/edit`}>수정</Button>
function MemberUpdateForm() {
return<>
<Bread list={bread}/>
<h1>회원 수정 양식</h1>
<form>
<div>
<label htmlFor="name">이름</label>
<input type="text" name="name" id="name"/>
</div>
<div>
<label htmlFor="addr">주소</label>
<input type="text" name="addr" id="addr"/>
</div>
<button type="submit">수정</button>
</form>
</>
}
const {num} = useParams();
const [dto, setDto] = useState({});
useEffect(()=>{
fetch(`/api/v1/members/${num}`)
.then(res=>res.json())
.then(data=>setDto(data));
}, []);
return<>
<Bread list={bread}/>
<h1><i>{dto.num}</i>번 회원 수정 양식</h1>
<form>
<div>
<label htmlFor="name">이름</label>
<input type="text" name="name" id="name" value={dto.name}/>
</div>
<div>
<label htmlFor="addr">주소</label>
<input type="text" name="addr" id="addr" value={dto.addr}/>
</div>
<button type="submit">수정</button>
</form>
</>
input 요소에 onChage 이벤트 추가<div>
<label htmlFor="name">이름</label>
<input onChange={handleChange} type="text" name="name" id="name" value={dto.name}/>
</div>
<div>
<label htmlFor="addr">주소</label>
<input onChange={handleChange} type="text" name="addr" id="addr" value={dto.addr}/>
</div>
input 요소에 change event 가 일어났을 때 실행할 handleChange 함수 추가const handleChange = (e) => {
// 입력한 value 값을 읽어온다
const value = e.target.value;
// 입력한 input 요소의 name 속성의 값을 읽어온다
const name = e.target.name;
// 입력한 내용을 이용해서 상태값을 변경한다
setDto({
...dto,
[name]:value // 여기서 name 은 "name" 또는 "addr" 이 된다
})
};
action & onSubmit 이벤트 추가<form action={`/api/v1/members/${dto.num}`} onSubmit={handleSubmit}>
const navigate = useNavigate();
form 에 submit 이벤트가 일어났을 때 실행할 함수 handleSubmit 함수 추가const handleSubmit = (e)=>{
e.preventDefault();
const form = e.target; // 수정 form 요소의 참조값
// put 방식 요청을 이용해서 회원 정보 수정 요청을 한다
fetch(form.action, {
method : "put", // put 방식
headers : {"Content-Type":"application/json"}, // json 문자열을 보낸다고 알림
body : JSON.stringify(dto) // 상태값으로 관리하는 member object 를 json 문자열로 변경
})
.then(res=>res.json())
.then(data=>{
navigate(`/members/${data.num}`)
});
};

⚠️ 기능에는 문제가 없지만 콘솔에 경고 메시지가 발생하는 문제 해결
key 값 추가 <li key={item.name} className={`breadcrumb-item ${item.to ? '':'active'}`}>
useState 에 빈 값 삽입const [dto, setDto] = useState({
num:null,
name:"",
addr:""
});
show : boolean type 모달을 띄울지 여부message : string type 모달 메세지 내용onYes : function type 확인 버튼을 눌렀을 때 실행할 함수onCancle : function type 취소 버튼을 눌렀을 때 실행할 함수import React from 'react';
import { Button, Modal, ModalBody } from 'react-bootstrap';
function ConfirmModal({show, message, onYes, onCancel}) {
return (
<Modal show={show}>
<Modal.Header>알림</Modal.Header>
<ModalBody>
{message}
</ModalBody>
<Modal.Footer>
<Button variant='success' onClick={onYes}>확인</Button>
<Button variant='warning' onClick={onCancel}>취소</Button>
</Modal.Footer>
</Modal>
);
}
<Button variant="danger" className="ms-1">삭제</Button>
const [showModal, setShowModal] = useState(false);
onClick 이벤트 추가 & ConfirmModal 사용 import ConfirmModal from '../components/ConfirmModal';
<Button variant="danger" className="ms-1" onClick={()=>setShowModal(true)}>삭제</Button>
<ConfirmModal show={showModal} message="삭제 하시겠습니까?"/>
onCancle 이벤트 추가<ConfirmModal show={showModal}
message="삭제 하시겠습니까?"
onCancel={()=>setShowModal(false)}
</>
onYes 이벤트 추가<ConfirmModal show={showModal}
message="삭제 하시겠습니까?"
onCancel={()=>setShowModal(false)}
onYes={handleYes}
/>
useNavigate 함수 추가const navigate = useNavigate();
handleYes 함수 추가const handleYes = () => {
fetch(`/api/v1/members/${dto.num}`,{
method : "delete"
})
.then(res=>res.json())
.then(data=>{
navigate("/members");
});
};

import { Container, Nav, Navbar } from 'react-bootstrap';
import { NavLink } from 'react-router-dom';
function BsNavbar(props) {
return<>
<Navbar fixed="top" expand="md" className="bg-warning mb-2">
<Container>
<Navbar.Brand as={NavLink} to="/">Acorn</Navbar.Brand>
<Navbar.Toggle aria-controls="collapseContent"/>
<Navbar.Collapse id="collapseContent">
<Nav className='me-auto'>
<Nav.Link as={NavLink} to="/">Home</Nav.Link>
<Nav.Link as={NavLink} to="/study">Study</Nav.Link>
<Nav.Link as={NavLink} to="/play">Play</Nav.Link>
<Nav.Link as={NavLink} to="/members">Members</Nav.Link>
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
</>
}
BsNavbar 사용function App() {
const currentOutlet = useOutlet();
return<>
<BsNavbar/>
<div className="container pt-4" style={{marginTop:"50px"}}>
{currentOutlet}
</div>
</>
}
for(int i=0; i<100; i++) {
Member tmp = Member.builder().name("이름"+i).addr("주소"+i).build();
em.persist(tmp);
}
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class MemberPageResponse {
private List<MemberDto> list;
private int startPageNum;
private int endPageNum;
private int totalPageNum;
private int pageNum;
}
{
"list":[{], {}, {}, ...]
"startPageNum":1,
"endPageNum":10,
"totalPageNum" : 11,
"pageNum" : 3
}
getPage 메소드 생성public MemberPageResponse getPage(int pageNum);
// 한 페이지에 몇 개의 row 를 출력할 것인지에 대한 값
final int PAGE_ROW_COUNT = 10;
// 페이징 처리 UI 에 페이지 번호를 몇 개씩 출력할 지에 대한 값
final int PAGE_DISPLAY_COUNT = 5;
@Override
public MemberPageResponse getPage(int pageNum) {
// num 에 대해서 내림차순 정렬하겠다는 Sort 객체
Sort sort = Sort.by(Sort.Direction.DESC, "num");
// pageNum 과 page row count 와 정렬 객체를 전달해서 원하는 PageRequest 를 만들어내고
PageRequest pageRequest = PageRequest.of(pageNum-1, PAGE_ROW_COUNT, sort);
// drg.springframework.data.domain 패키지의 Page type 을 import 해야 한다
Page<Member> page = memberRepo.findAll(pageRequest);
// Page 객체를 stream 으로 만들어서 dto 의 list 를 얻어낸다
List<MemberDto> list = page.stream().map(MemberDto :: toDto).toList();
return null;
}