댓글의 자바스크립트 처리

강상은·2023년 12월 5일

REST 방식의 서비스는 브라우저에서 Ajax를 이용해서 처리됩니다.
특정한 게시물을 조회하는 화면에서 Ajax를 통해 GET/POST/PUT/DELETE 방식으로 ReplyController를 호출하고 JSON 문자열을 처리하는 방식으로 진행할 예정!

비동기 처리와 Axios

동기화된 방식?

대부분의 프로그래밍의 시작은 항상 동기화된 방식을 이용
동기화된 방식이라는 의미를 간단한 코드를 통해 알아보자

result1 = doA();
result2 = doB(result1);
result3 = doC(result2);

앞의 코드는 흔하게 볼 수 있는 순차적 동기화된 코드이다.
doA()를 실행해서 나온 결과 - > doB()를 호출하는 방식
순서 : doA() -> doB() -> doC()

동기화 된 방식의 단점은 doA()의 실행이 완료 되어야만 doB()의 실행이 가능하다는 점!
즉, doA()가 결과를 반환할 때까지 다른 작업은 실행되지 않기 때문에 동시에 여러 작업을 처리할 수 없다

그렇다면 비동기 방식이란?

비동기 방식은 커피가게에 여러 명의 점원이 있는 상황과 유사
점원이 여러 명이면 A는 주문을 받고 B는 커피를 제조, C는 음료 제공
=> 손님들은 대기하고 있다가 자신이 주문한 음료가 나왔다는 사실을 '통보'받고 음료를 가져가게 됨

비동기 방식의 핵심은 '통보'에 있다!

비동기는 여러 작업을 처리하기 때문에 나중에 결과가 나오면 이를 '통보'해 주는 방식을 이용
이러한 방식을 전문용어로 콜백(callback)이라고 합니다.

비동기 방식은 'doA()'를 호출할 때 doB()를 해 줄 것을 같이 파라미터로 전달합니다.
이를 그림으로 표현하면 다음과 같은 구조가 됩니다.

function doA(callback) {
...
result1 = ...
callback(result1)
}

앞의 코드는 파라미터로 전달되는 콜백을 내부에서 호출하는데 자바 개발자들에게는 익숙하지 않은 코드이다.
자바에서 함수를 파라미터로 전달하는 개념은 Java8에서 람다식이 도입되면서 부터이다.

반면에 자바스크립트에서 함수는 '일급 객체(first-class object')로 일반 객체와 동일한 위상을 가지고 있으므로 파라미터가 되거나 리턴타입이 될 수 있어서 위의 코드와 같은 모습이 가능

자바스크립트에서는 Promise라는 개념을 도입해서 '비동기 호출을 동기화된 방식'으로 작성할 수 있는 문법적인 장치를 만들어 주었는데 Axios는 이를 활용하는 라이브러리임
(http://axios-http.com/kr/docs/intro)
아래 이미지는 공식 문서의 Axios에 대한 설명이다

Axios를 이용하면 Ajax를 호출하는 코드를 작성할 때 마치 동기화된 방식처럼 작성할 수 있어서 자바스크립트를 기반으로 하는 프레임워크(Angular)나 라이브러리들(React,Vue)에서 많이 사용된다.

Axios를 위한 준비

Axios 라이브러리 추가

자바스크립트 코드의 경우 read.html에서는 주로 이벤트 관련 처리를 하고
별도의 JS 파일에서 Axios를 이용하는 통신을 처리하도록 구성

static 폴더에 있는 js 폴더에 reply.js 파일 추가

read.html 구성

댓글추가버튼

Axios 호출해보기

reply.js에 간단하게 Axios를 이용하는 코드를 추가해봅니다.

Axios를 이용할 때 async/await를 같이 이용하면 비동기 처리를 동기화 된 코드처럼 작성할 수 있습니다.
async는 함수 선언 시에 사용하는데 해당 함수가 비동기 처리를 위한 함수라는 것을 명시하기 위해서 사용하고
await는 async 함수 내에서 비동기 호출하는 부분에 사용합니다.

reply.js 함수 적용

async function get1(bno) {

    const result = await axios.get(`/replies/list/${bno}`)

    console.log(result)

}

read.html에서는 get1()을 호출하는 코드를 작성

<script layout:fragment="script" th:inline="javascript">
	const bno = [[${dto.bno}]]
    get1(bno)
</script>

브라우저에는 '/board/read?bno=109'과 같이 많은 댓글이 있는 게시물 조회해보기
업로드중..

앞의 코드들이 정상적으로 동작 시 화면에는 아무것도 나타나지 않지만 콘솔창에는 다음과 같이 데이터가 출력되는 것을 확인
업로드중..

비동기 함수의 반환

화면에서 결과가 필요하다면 Axios의 호출 결과를 반환받아야 하기 때문에 reply.js에서는 다음과 같이 작성

async function get1(bno) {

    const result = await axios.get(`/replies/list/${bno}`)

    return result.data;
}

get1()을 호출하는 쪽에서는 호출 결과를 받기 위해 다음과 같이 처리를 시도하려고 할 것

read.html

<script layout:fragment="script" th:inline="javascript">

    const bno = [[${dto.bno}]]
    
    console.log(get1(bno))
</script>

앞의 코드를 실행해보면 예상과 달리 Promise가 반환되는 것을 볼 수 있다.
정작 실행 결과는 console.log(get1(bno)) 이후에 실행 된다.

업로드중..

이것은 get1()이 비동기 함수이므로 get1()을 호출한 시점에서는 반환할 것이 없지만 나중에 무언가를 반환할 것이므로 반환하기로 한 '약속' 만을 반환하기 때문(금융에서 약속어음과 비슷한 개념이라고 생각하면 쉽습니다)

  • 만일 비동기 처리되는 결과를 반환해서 처리한다면 then()과 catch()등을 이용해서 작성합니다

reply.js에서는 결과를 반환하도록 구성
업로드중..

read.html에서는 then()과 catch()를 이용

<script layout:fragment="script" th:inline="javascript">

    const bno = [[${dto.bno}]]

    console.log(get1(bno))

    get1(bno).then(data => {
        console.log(data)
    }).catch(e => {
        console.error(e)
    })
 </script>

업로드중..

비동기 처리 방식의 결정

비동기 처리할 때는 앞선 방법처럼 함수와 동작 방식이 다르므로 이를 어떻게 일관성있게 처리할 것인지를 결정해야 합니다.

비동기 함수를 이용해서 결과 데이터를 처리하는 방식은 크게 다음과 같습니다.

  • 비동기 함수에서는 순수하게 비동기 통신만 처리하고 호출한 쪽에서 then()이나 catch()등을 이용해서 처리하는 방식
  • 비동기 함수를 호출할 때 나중에 처리해야 하는 내용을 같이 별도의 함수로 구성해서 파라미터로 전송하는 방식

현재 예제에서 비동기 통신은 reply.js가 담당하고 화면은 read.html에서 처리하도록 예제에서 구성

업로드중..

reply.js는 Axios를 이용해서 Ajax 통신하는 부분이므로 코드의 양이 많지는 않지만 통신하는 영역과 이벤트나 화면 처리 영역을 분리하기 위해서 사용합니다(이러한 방식의 개발은 Vue나 React에서도 많이 사용되는 방식)

댓글 처리와 자바스크립트

댓글 목록 처리

reply.js에 개발하려는 함수의 이름은 getList()라 하고, 파라미터는 다음과 같이 결정

  • bno : 현재 게시물 번호
  • page : 페이지 번호
  • size : 페이지당 사이즈
  • goLast : 마지막 페이지 호출 여부

이 중에서 goLast는 조금 특별한 용도를 위해서 설계됩니다.
댓글의 경우 한 페이지에서 모든 동작이 이루어지므로 새로운 댓글이 등록되어도 화면에는 아무런 변화가 없다는 문제가 생깁니다.
또한 페이징 처리가 되면 새로 등록된 댓글이 마지막 페이지에 있기 때문에 댓글된 결과를 볼 수 없다는 문제가 생기게 됩니다. 예제에서는 goLast 변수를 이용해서 강제적으로 마지막 댓글 페이지를 호출하도록 합니다.

reply.js에는 getList 함수를 작성합니다.

async function getList({bno, page, size, goLast}){

    const result = await axios.get(`/replies/list/${bno}`, {params: {page, size}})
        return result.data
}

read.html에는 getList()를 호출하는 함수와 현재 페이지가 로딩되면 해당 함수를 호출하도록 작성

 function printReplies(page,size,goLast){

        getList({bno, page,size, goLast}).then(
            data => {
               console.log(data)
            }
        ).catch(e => {
            console.error(e)
        })

    }
        printReplies(1,10, true) //무조건 호출

결과 데이터는 dtoList로 화면에 목록(replyList)을 처리하고, 나머지 정보들로 페이지 번호들을 출력
read.html에는 댓글 목록을 출력하는 printList()와 페이지 번호를 출력하는 printPage() 함수를 작성하고 Axios의 결과를 출력하도록 수정


    const replyList = document.querySelector('.replyList') //댓글 목록 DOM
    const replyPaging = document.querySelector('.replyPaging') //페이지 목록 DOM

    function printList(dtoList){ //댓글 목록 출력
        let str = '';

        if(dtoList && dtoList.length > 0){

            for (const dto of dtoList) {

                str += `<li class="list-group-item d-flex replyItem">
                      <span class="col-2">${dto.rno}</span>
                      <span class="col-6" data-rno="${dto.rno}">${dto.replyText}</span>
                      <span class="col-2">${dto.replyer}</span>
                      <span class="col-2">${dto.regDate} </span>
                    </li>`
            }
        }
        replyList.innerHTML = str
    }

    function printPages(data){ //페이지 목록 출력

        //pagination
        let pageStr = '';

        if(data.prev) {
            pageStr +=`<li class="page-item"><a class="page-link" data-page="${data.start-1}">PREV</a></li>`
        }

        for(let i = data.start; i <= data.end; i++){
            pageStr +=`<li class="page-item ${i == data.page?"active":""} "><a class="page-link" data-page="${i}">${i}</a></li>`
        }

        if(data.next) {
            pageStr +=`<li class="page-item"><a class="page-link" data-page="${data.end +1}">NEXT</a></li>`
        }
        replyPaging.innerHTML = pageStr
    }

Axios 결과를 가져오면 앞의 함수들에게 전달

    function printReplies(page,size,goLast){

        getList({bno, page,size, goLast}).then(
            data => {
                printList(data.dtoList) //목록 처리
                printPages(data) //페이지 처리
            }
        ).catch(e => {
            console.error(e)
        })

    }
    printReplies(1,10, true)

페이징 처리를 확인할 수 있다

업로드중..

@JSONFormat, @JSONIgnore

출력된 댓글의 모양을 보면 댓글 등록 시간(regDate) 부분이 배열로 처리되어서 조금 지저분해 보이므로 ReplyDTO에 @JsonFormat을 이용해서 JSON 처리 시에 포맷팅을 지정

댓글 수정 시간(modDate) 또한 화면에서 출력할 일이 없으므로 JSON으로 변환될 때 제외하도록 @JsonIgnore를 적용

업로드중..

마지막 페이지로 이동 처리

댓글 페이징은 새로 댓글이 추가되는 상황이면 마지막으로 등록되기 때문에 확인이 어려움
따라서 댓글 목록 데이터의 total을 이용해서 다시 마지막 페이지 호출

reply 복사해서 댓글 추가

업로드중..

업로드중..

여러 댓글의 페이징 처리가 있는 경우 최신 댓글을 볼 수 없다는 문제점이 발생

이를 해결하려면 현재 게시물의 댓글에 마지막 페이지를 알아낸 후, 마지막 페이지를 다시 호출하는 방식으로 동작

마지막 페이지의 호출은 total 값과 size 값을 이용해 마지막 페이지 계산 후 Axios로 호출

async function getList({bno, page, size, goLast}){

    const result = await axios.get(`/replies/list/${bno}`, {params: {page, size}})

    if(goLast){
        const total = result.data.total
        const lastPage = parseInt(Math.ceil(total/size))

        return getList({bno:bno, page:lastPage, size:size})

    }

    return result.data
}

read.html에서 처음부터 댓글의 마지막 페이지를 보고 싶다면 printReplies()를 호출할 때 true 값 추가

printReplies(1,10,true)

댓글 등록

댓글 추가는 모달창을 이용
파라미터를 JS의 객체로 받아서 axios.post()를 이용해 전달

async function addReply(replyObj) {
    const response = await axios.post(`/replies/`,replyObj)
    return response.data
}

업로드중..

    //댓글 등록 모달
    const registerModal = new bootstrap.Modal(document.querySelector(".registerModal"))
    //registerModel
    const registerBtn = document.querySelector(".registerBtn")
    const replyText = document.querySelector(".replyText")
    const replyer = document.querySelector(".replyer")
    const closeRegisterBtn = document.querySelector(".closeRegisterBtn")

화면상의 ADD REPLY 버튼 눌렀을 때 모달창을 보여주도록 이벤트 처리와 모달창의 Close 버튼 처리 추가

    document.querySelector(".addReplyBtn").addEventListener("click", function (e){
        registerModal.show()
    },false)

    closeRegisterBtn.addEventListener("click", function (e){
        registerModal.hide()
    },false)

[Register] 버튼 눌렀을 때 이벤트 처리 추가


    registerBtn.addEventListener("click", function(e){
        const replyObj = {
            bno:bno,
            replyText:replyText.value,
            replyer:replyer.value}

        addReply(replyObj).then(result => {
            alert(result.rno)
            registerModal.hide()
            replyText.value = ''
            replyer.value =''
            printReplies(1,10, true) //댓글 목록 갱신
        }).catch(e => {
            alert("Exception...")
        })
    }, false)

새로운 댓글이 추가되면 댓글의 번호가 alert()를 통해 보여지고 새로운 댓글이 추가된 목록으로 갱신
업로드중..
업로드중..
업로드중..

0개의 댓글