2024. 5. 12. 19:52ㆍ· FRONT-END/└ React
환경 : Visual Studio Code, Spring Boot Tool4
반복되는 부분의 코드들을 덜 작성하기 위해(매 파일 마다 설정해주는 것은 귀찮으니까..) axios를 개조하여 사용하려고 한다..
그러기 위해서 axios 관련한 파일을 모듈화하여 사용해보려고 한다.
axios 공식 문서 : https://axios-http.com/kr/docs/req_config
- CustomAxios.js
//CustomAxios.js
//이 파일은 axios를 우리가 원하는 대로 개조하여
//프로그램 전체에서 불러서 사용할 수 있도록 만들기 위한 파일
//[1] 라이브러리가 기본적으로 제공하는 axios를 불러온다
import axios from "axios";
//[2] 필요한 설정을 추가한다
const instance = axios.create({
baseURL : "http://localhost:8080", //기본 통신 URL 접두사 - 포트번호까지만 작성
timeout : 5000, //통신의 최대 지연시간(ms) - 통신 만료 시간
})
//[3] 외부에서 사용 가능하도록 내보낸다.
export default instance;
여기서...
[ baseURL의 값 정보를 "http://localhost:8080/" 이 아닌 "http://localhost:8080"으로 사용하는 이유 ]
URL 경로의 유연성과 정확성
사실 별 큰 문제는 없다 ㅎ
코드의 일관성과 가독성을 위해 baseURL을 일관되게 사용하는 것이 더 중요하다
1. 경로 조합의 편의성 : baseURL이 http://localhost:8080/일 경우에는 요청을 보낼 때마다 경로를 "/api/somePath"와 같이 설정해야 한다. 그런데 baseURL이 http://localhost:8080으로 끝나면 요청을 보낼 때 baseURL과 경로를 간단히 조합할 수 있다
2. URL 정규화 : baseURL을 http://localhost:8080/으로 설정할 경우, 사용자가 실수로 baseURL에 슬래시를 중복해서 입력할 수 있다.
예를 들어, baseURL을 "http://localhost:8080/"로 설정하고 요청을 보낼 때 "/api/somePath"를 붙이면 "http://localhost:8080//api/somePath"와 같이 슬래시가 중복될 수 있다.
3. 표준화된 URL 형식 : baseURL이 슬래시로 끝나지 않는 것이 표준 URL 형식에 더 가깝다. 대부분의 URL은 슬래시 없이 끝나기 때문에 baseURL을 "http://localhost:8080"으로 설정하는 것이 일반적인 URL 형식을 따르는 것이라고 할 수 있다.
[ 적용 ]
모듈화 한 CustomAxios 파일을 적용해보자
- axios 라는 이름으로 위의 파일을 임포트 시켜준다
import axios from "../utils/CustomAxios"; //내가 만든 파일로 임포트 시켜주기
- 그리고 코드를 작성한다
const loadData = useCallback(() =>{
//원래 사용하던 코드 (1)
axios({
url:"http://localhost:8080/student/",
method : "get"
})
.then(resp=>{
setStudents(resp.data);
});
// CustomeAxios.js 코드 적용 (2)
axios.get("/student/") //baseURL을 제외하고 작성
.then(resp => {
setStudents(resp.data);
});
}, [students]);
(+) (1) 코드도 좋은 코드.. 근데 loadData 방 안에 방이 또 있는 느낌이고, 코드가 기니까 귀찮아서.. 더 간결한 코드를 찾은게 (2)번.
(+) (2)번 코드는 우리가 만든 axios 모듈화 파일을 가져왔기 때문에 baseURL에 설정해준 부분의 주소는 제외하고 작성.
// - 자바스크립트는 너무나도 많은 비동기 코드를 가지고 있다(특히 ajax)
// - 필요 이상으로 코드가 중첩되는 것을 막기 위해 ES6에서 Promise 패턴이 나온다
// - async 함수를 만들고 내부에서 await 키워드를 사용하면 비동기 코드를 동기처럼 사용할 수 있다
const loadData = useCallback(async () => {
const resp = await axios.get("/student/");
setStudents(resp.data);
}, [students]);
(+) 그런데 필요 이상으로 코드가 중첩되는 부분들을 막고 더 간결하게 사용하기 위해서 Promise 패턴이 나오게 된다.
(+) async 함수를 만들고 내부에서 await 키워드를 사용하면 비동기 코드를 동기처럼 사용할 수 있다
=> 코드가 훨씬 간결해짐
- Student.js 전체코드
import Jumbotron from './../Jumbotron';
import { useCallback, useEffect, useState } from 'react';
import { HiArchiveBoxXMark } from "react-icons/hi2";
import axios from "../utils/CustomAxios"; //내가 만든 파일로 임포트 시켜주기
const Student = () => {
//state
const [students, setStudents] = useState([]);
//effect
// useEffect(loadData, []);//loadData 함수를 최초 1회 실행하라!
//함수를 이름만 쓰면, "실행시켜 주세요"가 됨
useEffect(() => {
loadData();
}, []);
//callback
// - 자바스크립트는 너무나도 많은 비동기 코드를 가지고 있다(특히 ajax)
// - 필요 이상으로 코드가 중첩되는 것을 막기 위해 ES6에서 Promise 패턴이 나온다
// - async 함수를 만들고 내부에서 await 키워드를 사용하면 비동기 코드를 동기처럼 사용할 수 있다
const loadData = useCallback(async () => {
const resp = await axios.get("/student/");
setStudents(resp.data);
}, [students]);
//view
return (
<>
{/* 제목 */}
<Jumbotron title="학생 관리" content="학생관련 CRUD" />
{/* 데이터 출력 (표) */}
<div className='row mt-4'>
<div className='col'>
<table className='table table-striped'>
<thead className='text-center'>
<tr>
<th>번호</th>
<th>이름</th>
<th>국어</th>
<th>영어</th>
<th>수학</th>
<th>관리</th>
</tr>
</thead>
<tbody className='text-center'>
{students.map(student => (
<tr key={student.studentId}>
<td>{student.studentId}</td>
<td>{student.name}</td>
<td>{student.koreanScore}</td>
<td>{student.englishScore}</td>
<td>{student.mathScore}</td>
<td>
<HiArchiveBoxXMark className='text-danger' />
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</>
);
};
export default Student;
코드에 대한 이해를 위해 목록 출력에 대해 axios 모듈화 사용법을 정리했다.
이 다음으론 남은 CRUD를 구현해보겠다!
[ 삭제 ]
axios.delete({
url : "/student/" + target.studentId,
})
.then(resp=>{
loadData();
});
const deleteStudent = useCallback(async(target) =>{
const choice = window.confirm("정말 삭제하시겠습니까?");
if(choice === false) return;
//target에 있는 내용을 서버에 지워달라고 요청하고 목록을 다시 불러온다
const resp = await axios.delete("/student/" + target.studentId);
loadData();
}, [students]);
(+) axios 대신 async와 await를 사용하여 코딩할 수 있다.
(+) resp 변수에 굳이 담을 필요 없음
[ 등록 ]
//신규 등록 화면 입력값 변경
const changeInput = useCallback((e)=>{
setInput({
...input,
[e.target.name] : e.target.value
});
}, [input]);
//등록
const saveInput = useCallback(async ()=>{
//입력값에 대한 검사 코드가 필요하다면 이 자리에 추가하고 차단!
//if(검사 결과 이상한 데이터가 입력되어 있다면) return;
//input에 들어있는 내용을 서버로 전송하여 등록한 뒤 목록 갱신 + 모달 닫기
const resp = await axios.post("/student/", input);
loadData();
clearInput();
closeModal();
}, [input]);
//등록 취소
const cancelInput = useCallback(()=>{
const choice = window.confirm("작성을 취소하시겠습니까?");
if(choice === false) return;
clearInput();
closeModal();
}, [input]);
//입력값 초기화
const clearInput = useCallback(()=>{
setInput({
name:"",
koreanScore:"",
englishScore:"",
mathScore:""
});
}, [input]);
(+) changeInput은 직접적으로 버튼이 눌렸을 때 실행되는 함수
(+) saveInput은 저장하는 함수
(+) cancelInput은 취소하는 함수
(+) clearInput은 입력창에 남아있는 데이터를 지워주는 함수
(+) closeModal()의 경우 모달함수도 닫아주는 기능을 수행한다
모달
//ref + modal
const bsModal = useRef();
const openModal = useCallback(()=>{
const modal = new Modal(bsModal.current);//현재 리모콘이 가리키고 있는 대상
modal.show();
}, [bsModal]);
const closeModal = useCallback(()=>{
const modal = Modal.getInstance(bsModal.current);
modal.hide();
}, [bsModal]);
[ 수정 ]
//해당 줄을 수정상태(edit==false)로 만드는 함수
//target은 수정을 누른 줄의 학생 정보
const editStudent = useCallback((target)=>{
//1, students를 복제한다
const copy = [...students];
//(+추가) 이미 수정 중인 항목이 있을 수 있으므로 해당 항목은 취소 처리가 필요
const recover = copy.map(student => {
if(student.edit === true) { //수정 중인 항목이 있다면
return {...backup, edit:false};//백업으로 갱신 + 수정모드 취소
}
else {
return {...student}; //그대로
}
});
//(+추가) 나중을 위해 target를 백업해둔다 (target은 수정버튼 누른 항목)
setBackup({...target});
//2, recover를 고친다
//- copy 중에서 target과 동일한 정보를 가진 항목을 찾아서 edit:true로 만든다
//- 배열을 변환시켜야 하므로 map 함수를 사용한다
const copy2 = recover.map(student=>{
//target : 수정버튼을 누른 학생정보, student: 현재 회차의 학생정보
if(target.studentId === student.studentId){ //target이랑 student가 동일하다면 //원하는 정보일 경우
return {
...student,//나머지 정보는 유지하되
edit:true,//edit 관련된 처리를 추가하여 반환
};
}
else { //원하는 정보가 아닐 경우 - 데이터를 그대로 반환
return {...student}; //데이터를 그대로 반환
}
});
//3, copy를 students에 덮어쓰기 한다
setStudents(copy2);
}, [students]);
const cancelEditStudent = useCallback((target)=>{
//1, students를 복제한다
const copy = [...students];
//2, copy를 고친다
//- copy 중에서 target과 동일한 정보를 가진 항목을 찾아서 edit:true로 만든다
//- 배열을 변환시켜야 하므로 map 함수를 사용한다
const copy2 = copy.map(student=>{
//target : 수정버튼을 누른 학생정보, student: 현재 회차의 학생정보
if(target.studentId === student.studentId){ //target이랑 student가 동일하다면 //원하는 정보일 경우
return {
...backup,//백업 정보를 전달
edit:false,//edit 관련된 처리를 추가하여 반환
};
}
else { //원하는 정보가 아닐 경우 - 데이터를 그대로 반환
return {...student}; //데이터를 그대로 반환
}
});
//3, copy를 students에 덮어쓰기 한다
setStudents(copy2);
}, [students]);
//수정 입력창에서 입력이 발생할 경우 실행할 함수
//- students 중에서 대상을 찾아 해당 필드를 교체하여 재설정
//- e는 입력이 발생한 창의 이벤트 정보
//- target은 입력이 발생한 창이 있는 줄의 학생 정보
const changeStudent = useCallback((e, target)=>{
const copy = [...students];
const copy2 = copy.map(student=>{
if(target.studentId === student.studentId) { //이벤트 발생한 학생이라면
return {
...student,//나머지 정보는 유지
[e.target.name] : e.target.value//단, 입력항목만 교체
};
}
else {//다른 학생이라면
return {...student};//현상유지
}
});
setStudents(copy2);
}, [students]);
//수정된 결과를 저장 + 목록 갱신 + 수정모드 해제
const saveEditStudent = useCallback(async (target)=>{
//서버에 target을 전달하여 수정 처리
const resp = await axios.patch("/student/", target);// (주소, 데이터, 설정)
//목록 갱신
loadData();
}, [students]);
(+) 수정이 진짜 어렵다... 따로 글로 다루도록 하겠음...
- Student.js 전체 코드
import Jumbotron from './../Jumbotron';
import { useCallback, useEffect, useRef, useState } from 'react';
import { HiArchiveBoxXMark } from "react-icons/hi2";
import axios from "../utils/CustomAxios"; //내가 만든 파일로 임포트 시켜주기
import { Modal } from 'bootstrap';
import { FiEdit } from "react-icons/fi";
import { FaCheck } from "react-icons/fa";
import { TbPencilCancel } from "react-icons/tb";
const Student = () => {
//state
const [students, setStudents] = useState([]);
const [input, setInput] = useState({
name:"",
koreanScore:"",
englishScore:"",
mathScore:""
});
const [backup, setBackup] = useState(null); //수정 시 복원을 위한 백업
//effect
// useEffect(loadData, []);//loadData 함수를 최초 1회 실행하라!
//함수를 이름만 쓰면, "실행시켜 주세요"가 됨
useEffect(() => {
loadData();
}, []);
//callback
//const loadData = useCallback(() =>{
//원래 사용하던 코드
/*axios({
url:"http://localhost:8080/student/",
method : "get"
})
.then(resp=>{
setStudents(resp.data);
});*/
// CustomeAxios.js 코드 적용
/*
axios.get("/student/") //baseURL을 제외하고 작성
.then(resp => {
setStudents(resp.data);
});*/
//}, [students]);
// - 자바스크립트는 너무나도 많은 비동기 코드를 가지고 있다(특히 ajax)
// - 필요 이상으로 코드가 중첩되는 것을 막기 위해 ES6에서 Promise 패턴이 나온다
// - async 함수를 만들고 내부에서 await 키워드를 사용하면 비동기 코드를 동기처럼 사용할 수 있다
const loadData = useCallback(async () => {
const resp = await axios.get("/student/");
setStudents(resp.data);
}, [students]);
//삭제
const deleteStudent = useCallback(async(target) =>{
const choice = window.confirm("정말 삭제하시겠습니까?");
if(choice === false) return;
//target에 있는 내용을 서버에 지워달라고 요청하고 목록을 다시 불러온다
const resp = await axios.delete("/student/" + target.studentId);
loadData();
// axios.delete({
// url : "/student/" + target.studentId,
// })
// .then(resp=>{
// loadData();
// });
}, [students]);
//신규 등록 화면 입력값 변경
const changeInput = useCallback((e)=>{
setInput({
...input,
[e.target.name] : e.target.value
});
}, [input]);
//등록
const saveInput = useCallback(async ()=>{
//입력값에 대한 검사 코드가 필요하다면 이 자리에 추가하고 차단!
//if(검사 결과 이상한 데이터가 입력되어 있다면) return;
//input에 들어있는 내용을 서버로 전송하여 등록한 뒤 목록 갱신 + 모달 닫기
const resp = await axios.post("/student/", input);
loadData();
clearInput();
closeModal();
}, [input]);
//등록 취소
const cancelInput = useCallback(()=>{
const choice = window.confirm("작성을 취소하시겠습니까?");
if(choice === false) return;
clearInput();
closeModal();
}, [input]);
//입력값 초기화
const clearInput = useCallback(()=>{
setInput({
name:"",
koreanScore:"",
englishScore:"",
mathScore:""
});
}, [input]);
//해당 줄을 수정상태(edit==false)로 만드는 함수
//target은 수정을 누른 줄의 학생 정보
const editStudent = useCallback((target)=>{
//1, students를 복제한다
const copy = [...students];
//(+추가) 이미 수정 중인 항목이 있을 수 있으므로 해당 항목은 취소 처리가 필요
const recover = copy.map(student => {
if(student.edit === true) { //수정 중인 항목이 있다면
return {...backup, edit:false};//백업으로 갱신 + 수정모드 취소
}
else {
return {...students}; //그대로
}
});
//(+추가) 나중을 위해 target를 백업해둔다 (target은 수정버튼 누른 항목)
setBackup(target);
//2, recover를 고친다
//- copy 중에서 target과 동일한 정보를 가진 항목을 찾아서 edit:true로 만든다
//- 배열을 변환시켜야 하므로 map 함수를 사용한다
const copy2 = recover.map(student=>{
//target : 수정버튼을 누른 학생정보, student: 현재 회차의 학생정보
if(target.studentId === student.studentId){ //target이랑 student가 동일하다면 //원하는 정보일 경우
return {
...student,//나머지 정보는 유지하되
edit:true,//edit 관련된 처리를 추가하여 반환
};
}
else { //원하는 정보가 아닐 경우 - 데이터를 그대로 반환
return {...student}; //데이터를 그대로 반환
}
});
//3, copy를 students에 덮어쓰기 한다
setStudents(copy2);
}, [students]);
const cancelEditStudent = useCallback((target)=>{
//1, students를 복제한다
const copy = [...students];
//2, copy를 고친다
//- copy 중에서 target과 동일한 정보를 가진 항목을 찾아서 edit:true로 만든다
//- 배열을 변환시켜야 하므로 map 함수를 사용한다
const copy2 = copy.map(student=>{
//target : 수정버튼을 누른 학생정보, student: 현재 회차의 학생정보
if(target.studentId === student.studentId){ //target이랑 student가 동일하다면 //원하는 정보일 경우
return {
...backup,//백업 정보를 전달
edit:false,//edit 관련된 처리를 추가하여 반환
};
}
else { //원하는 정보가 아닐 경우 - 데이터를 그대로 반환
return {...student}; //데이터를 그대로 반환
}
});
//3, copy를 students에 덮어쓰기 한다
setStudents(copy2);
}, [students]);
//수정 입력창에서 입력이 발생할 경우 실행할 함수
//- students 중에서 대상을 찾아 해당 필드를 교체하여 재설정
//- e는 입력이 발생한 창의 이벤트 정보
//- target은 입력이 발생한 창이 있는 줄의 학생 정보
const changeStudent = useCallback((e, target)=>{
const copy = [...students];
const copy2 = copy.map(student=>{
if(target.studentId === student.studentId) { //이벤트 발생한 학생이라면
return {
...student,//나머지 정보는 유지
[e.target.name] : e.target.value//단, 입력항목만 교체
};
}
else {//다른 학생이라면
return {...student};//현상유지
}
});
setStudents(copy2);
}, [students]);
//수정된 결과를 저장 + 목록 갱신 + 수정모드 해제
const saveEditStudent = useCallback(async (target)=>{
//서버에 target을 전달하여 수정 처리
const resp = await axios.patch("/student/", target);// (주소, 데이터, 설정)
//목록 갱신
loadData();
}, [students]);
//ref + modal
const bsModal = useRef();
const openModal = useCallback(()=>{
const modal = new Modal(bsModal.current);//현재 리모콘이 가리키고 있는 대상
modal.show();
}, [bsModal]);
const closeModal = useCallback(()=>{
const modal = Modal.getInstance(bsModal.current);
modal.hide();
}, [bsModal]);
//view
return (
<>
{/* 제목 */}
<Jumbotron title="학생 관리" content="학생관련 CRUD" />
{/* 추가 버튼 */}
<div className='row mt-4'>
<div className='col text-end' >
<button className='btn btn-primary' onClick={e=>openModal()}>
신규등록
</button>
</div>
</div>
{/* 데이터 출력 (표) */}
<div className='row mt-4'>
<div className='col'>
<table className='table table-striped'>
<thead className='text-center'>
<tr>
<th width="100">번호</th>
<th>이름</th>
<th>국어</th>
<th>영어</th>
<th>수학</th>
<th width="100">관리</th>
</tr>
</thead>
<tbody className='text-center'>
{students.map(student => (
<tr key={student.studentId}>
{ student.edit === true ? (
//수정화면
<>
<td>{student.studentId}</td>
<td>
<input type='text' className='form-control' name='name'
value={student.name} onChange={e =>changeStudent(e, student)}/>
</td>
<td>
<input type='number' className='form-control' name='koreanScore'
value={student.koreanScore} onChange={e =>changeStudent(e, student)}/>
</td>
<td>
<input type='number' className='form-control' name='englishScore'
value={student.englishScore} onChange={e =>changeStudent(e, student)}/>
</td>
<td>
<input type='number' className='form-control' name='mathScore'
value={student.mathScore} onChange={e =>changeStudent(e, student)}/>
</td>
<td>
<FaCheck className="text-success me-2"
onClick={e=>saveEditStudent(student)}/>
<TbPencilCancel className='text-danger'
onClick={e=>cancelEditStudent(student)}/>
</td>
</>
) : (
//일반화면
<>
<td>{student.studentId}</td>
<td>{student.name}</td>
<td>{student.koreanScore}</td>
<td>{student.englishScore}</td>
<td>{student.mathScore}</td>
<td>
<FiEdit className="text-warning me-2"
onClick={e=>editStudent(student)}/>
<HiArchiveBoxXMark className='text-danger' onClick={e=>deleteStudent(student)}/>
</td>
</>
) }
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Model */}
<div ref={bsModal} className="modal fade" id="staticBackdrop" data-bs-backdrop="static" data-bs-keyboard="false" tabIndex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h1 className="modal-title fs-5" id="staticBackdropLabel">신규 학생 등록</h1>
<button type="button" className="btn-close" aria-label="Close" onClick={e=>cancelInput()}></button>
</div>
<div className="modal-body">
{/* 등록 화면 */}
<div className='row mt-4'>
<div className='col'>
<label>학생명</label>
<input type='text' name='name' value={input.name} onChange={e=>changeInput(e)} className='form-control'/>
</div>
</div>
<div className='row mt-4'>
<div className='col'>
<label>국어점수</label>
<input type='text' name='koreanScore' value={input.koreanScore} onChange={e=>changeInput(e)} className='form-control'/>
</div>
</div>
<div className='row mt-4'>
<div className='col'>
<label>영어점수</label>
<input type='text' name='englishScore' value={input.englishScore} onChange={e=>changeInput(e)} className='form-control'/>
</div>
</div>
<div className='row mt-4'>
<div className='col'>
<label>수학점수</label>
<input type='text' name='mathScore' value={input.mathScore} onChange={e=>changeInput(e)} className='form-control'/>
</div>
</div>
</div>
<div className="modal-footer">
<div className="row mt-4">
<div className="col text-end">
<button className="btn btn-success me-2" onClick={e => saveInput()}>등록</button>
<button className="btn btn-danger" onClick={e => cancelInput()}>취소</button>
</div>
</div>
</div>
</div>
</div>
</div>
</>
);
};
export default Student;
개인 공부 기록용입니다:)
'· FRONT-END > └ React' 카테고리의 다른 글
[ JavaScript / React ] recoil 사용 - 외부 상태 저장소 만들기 (0) | 2024.05.13 |
---|---|
[ React ] 백엔드와 연동해서 CRD 구현하기 (0) | 2024.05.12 |
[ React ] 아.추하기 (아이콘 추가하기라는 뜻) (0) | 2024.05.11 |
[ JavaScript / React ] 등록/조회/삭제 해보기 (0) | 2024.05.09 |
[ JavaScript / React ] 삭제 효과 내보기 (0) | 2024.05.08 |