[ JavaScript / React ] 비동기 통신.. 개조.. 그리고 CRUD.......

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;

 

 

 

 

 

개인 공부 기록용입니다:)

728x90