v9
★ MILESTONE · REST

비동기 댓글

새로고침이 사라지는 순간

학습 목표

  • fetch 로 서버에 비동기 요청을 보낸다
  • JSON 응답을 받아 DOM 에 직접 추가한다
  • 댓글이 새로고침 없이 화면에 등장하는 경험을 만든다
  • async/await 의 기본 사용법을 안다

⚠️ v6/v8 의 답답함

댓글 한 번 달 때마다
  1. 댓글 입력 + 제출 클릭
  2. 화면 전체가 깜빡 (새로고침)
  3. 모든 이미지·스크립트 다시 다운로드
  4. 스크롤이 맨 위로 — 어디 있었는지 잃어버림

현대 웹사이트는 이런 식으로 동작 안 합니다. 댓글은 즉시 추가되고 화면은 그대로여야 합니다.

🛠️ 비동기 통신의 발상

「부분만 갱신」

전체 페이지를 새로 받는 대신, JS 로 백그라운드에서 서버에 요청 → JSON 받아 → 화면의 일부분만 직접 수정.

동기 (전통) 비동기 (Ajax) ────────── ────────── form 제출 fetch() 호출 ↓ ↓ 서버 → HTML 전체 서버 → JSON 만 ↓ ↓ 페이지 다시 그림 JS 가 일부만 변경 (깜빡) (그대로 매끄럽게)

오늘 만들 것

v6 게시판의 글 상세 페이지에 댓글 기능을 추가합니다:

  • 입력창 + 등록 버튼
  • 등록 시 fetch 로 서버에 POST
  • 서버는 댓글 객체를 JSON 으로 반환
  • JS 가 그 JSON 으로 댓글 목록 끝에 새 댓글 추가
  • 페이지 새로고침 0 회

① DB — 댓글 테이블 신설


CREATE TABLE myreply (
    num        INT          PRIMARY KEY AUTO_INCREMENT,
    content    VARCHAR(500) NOT NULL,
    boardnum   INT          NOT NULL,
    writer     VARCHAR(50)  NOT NULL,
    created_at DATETIME     DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (boardnum) REFERENCES myboard(num) ON DELETE CASCADE
);

👉 boardnummyboard.num 을 가리키는 FK 첫 등장. ON DELETE CASCADE — 글이 지워지면 댓글도 함께 삭제.

② Reply 도메인 + Mapper


// com.smhrd.domain.Reply
@Data @AllArgsConstructor @NoArgsConstructor
public class Reply {
    private int num;
    private String content;
    private int boardnum;
    private String writer;
    private LocalDateTime createdAt;
}

// com.smhrd.mapper.ReplyMapper
public interface ReplyMapper {
    List<Reply> selectList(int boardnum);
    void insert(Reply r);
}

② Mapper XML


<mapper namespace="com.smhrd.mapper.ReplyMapper">

    <select id="selectList" parameterType="int"
            resultType="com.smhrd.domain.Reply">
        SELECT num, content, boardnum, writer, created_at
        FROM myreply
        WHERE boardnum = #{boardnum}
        ORDER BY num ASC
    </select>

    <insert id="insert" parameterType="com.smhrd.domain.Reply"
            useGeneratedKeys="true" keyProperty="num">
        INSERT INTO myreply(content, boardnum, writer)
        VALUES(#{content}, #{boardnum}, #{writer})
    </insert>

</mapper>

👉 useGeneratedKeys + keyProperty: INSERT 후 자동 증가된 num 이 객체로 돌아옴.

③ Service


package com.smhrd.service;

@Service
public class ReplyService {

    @Autowired
    private ReplyMapper mapper;

    public List<Reply> list(int boardnum) {
        return mapper.selectList(boardnum);
    }

    public Reply add(Reply r) {
        mapper.insert(r);
        // insert 후 num 이 채워진 r 이 그대로 돌아옴
        return r;
    }
}

④ REST Controller — 조회


package com.smhrd.controller;

@RestController
@RequestMapping("/api/replies")
public class ReplyApiController {

    @Autowired
    private ReplyService service;

    // GET /api/replies?boardnum=3
    @GetMapping
    public List<Reply> list(@RequestParam int boardnum) {
        return service.list(boardnum);
    }
}

👉 글 번호로 댓글 목록을 JSON 배열로 반환.

④ REST Controller — 등록


    // POST /api/replies
    @PostMapping
    public Reply add(@RequestBody Reply r,
                     HttpSession session) {
        Member u = (Member) session.getAttribute("loginUser");
        if (u == null) {
            throw new RuntimeException("로그인이 필요합니다");
        }
        r.setWriter(u.getId());   // 세션에서 작성자 ID
        return service.add(r);
    }

👉 세션에서 작성자를 채워 넣고 새 댓글 객체를 그대로 응답.

⑤ JSP — 댓글 영역 마크업


<!-- /WEB-INF/views/board/view.jsp 의 일부 -->
<h1>${board.title}</h1>
<div>${board.content}</div>

<hr/>
<h3>댓글</h3>

<div id="replyList">
    <!-- 여기에 JS 로 댓글 추가됨 -->
</div>

<c:if test="${not empty sessionScope.loginUser}">
    <form id="replyForm" onsubmit="addReply(event)">
        <input type="text" id="replyInput" placeholder="댓글" />
        <button type="submit">등록</button>
    </form>
</c:if>

<script>
    const BOARD_NUM = ${board.num};
    /* 다음 슬라이드의 JS */
</script>

⑥ JS — 댓글 목록 불러오기


// 페이지 로드 시 기존 댓글 가져오기
async function loadReplies() {
    const res = await fetch(`/api/replies?boardnum=${BOARD_NUM}`);
    const replies = await res.json();

    const list = document.getElementById('replyList');
    list.innerHTML = '';
    replies.forEach(r => {
        list.appendChild(renderReply(r));
    });
}

// 페이지 로드 시 한 번 실행
loadReplies();

👉 fetch 로 JSON 배열을 받아 한 줄씩 DOM 으로 그립니다.

⑥ JS — 댓글 한 줄 렌더링


function renderReply(r) {
    const div = document.createElement('div');
    div.className = 'reply';
    div.innerHTML = `
        <strong>${r.writer}</strong>
        <span>${r.createdAt}</span>
        <p>${r.content}</p>
    `;
    return div;
}

👉 댓글 객체 하나를 받아 DOM 노드 하나를 만듭니다. 등록 시에도 재사용.

⑦ JS — 댓글 작성 — 요청 보내기


async function addReply(event) {
    event.preventDefault();   // form 의 기본 제출 막기

    const input = document.getElementById('replyInput');
    const content = input.value.trim();
    if (!content) return;

    const res = await fetch('/api/replies', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({
            boardnum: BOARD_NUM,
            content: content
        })
    });
    /* 다음 슬라이드 — 응답 처리 */
}

👉 preventDefault() 로 페이지 새로고침을 막고, JSON 으로 POST 합니다.

⑦ JS — 댓글 작성 — 응답 처리


    try {
        if (!res.ok) throw new Error('서버 오류');

        const newReply = await res.json();
        // 응답으로 받은 댓글을 화면 끝에 추가
        document.getElementById('replyList')
                .appendChild(renderReply(newReply));

        input.value = '';   // 입력창 비움
    } catch (e) {
        alert('댓글 등록 실패: ' + e.message);
    }
}

👉 응답 JSON 을 같은 renderReply 로 그려서 끝에 붙이고, 입력창만 비움. 페이지 그대로.

전체 흐름 — 시간순

t=0 사용자: "좋은 글이네요" 입력 + 등록 버튼 t=1 JS: addReply(event) 호출 event.preventDefault() ← 새로고침 막음 fetch POST /api/replies t=2 서버: @RequestBody Reply r 로 자동 변환 session 에서 사용자 확인 ReplyService.add(r) ReplyMapper.insert(r) DB 에 한 행 추가 반환: 채워진 Reply 객체 t=3 서버 → 클라이언트: JSON 응답 { "num": 7, "content": "좋은 글이네요", "writer": "hong", ... } t=4 JS: const newReply = await res.json() renderReply(newReply) → DOM 요소 만듦 replyList.appendChild(...) 입력창 비움 t=5 화면: 댓글 목록 끝에 새 댓글이 「등장」 새로고침 없이, 스크롤 그대로

v6/v8 → v9 핵심 차이 — 기존 동기 폼


<form action="/reply/add"
      method="post">
    <input name="content"/>
    <button>등록</button>
</form>

// 서버 (@Controller)
return "redirect:/board/view?num=...";

// → 페이지 전체 새로고침

👉 등록 버튼을 누르면 페이지가 통째로 다시 그려집니다. 스크롤 위치도 잃음.

v6/v8 → v9 핵심 차이 — 새 비동기 fetch


<form onsubmit="addReply(event)">
    <input id="replyInput"/>
    <button>등록</button>
</form>

// 서버 (@RestController)
return r;   // JSON

// → JS 가 화면 끝에 새 댓글만 추가
//   페이지 그대로

👉 같은 화면 안에서 댓글만 살짝 추가됩니다. 스크롤·입력 상태 보존.

async / await — JS 의 비동기


// 옛 방식 (Promise)
fetch('/api/replies?boardnum=3')
    .then(res => res.json())
    .then(replies => {
        replies.forEach(r => { ... });
    })
    .catch(err => console.error(err));

// 현대 (async / await — 가독성 ↑)
async function load() {
    try {
        const res = await fetch('/api/replies?boardnum=3');
        const replies = await res.json();
        replies.forEach(r => { ... });
    } catch (err) {
        console.error(err);
    }
}

👉 awaitasync 함수 안에서만. 비동기를 동기처럼 읽기 좋게.

흔한 함정 4 가지

함정증상
event.preventDefault() 빠짐fetch 도 가지만 form 도 제출됨 → 화면 전체 새로고침
Content-Type: application/json 누락서버에서 @RequestBody 매핑 실패
JSON.stringify 누락객체가 [object Object] 문자열로 보내짐
await 누락res 가 Promise 객체 그대로 → res.json() 에러

새로고침 없이 — 두 가지 의미

  • UX 개선 — 화면 깜빡 없음, 스크롤 유지, 빠른 반응
  • 네트워크 절약 — HTML/CSS/이미지 다시 안 받음 (JSON 만 작게)

두 효과가 모이면 사용자는 「부드러운 앱」 같은 경험을 합니다. 이게 현대 웹의 표준.

다음 차시 미리보기 — v10

기존 myboard 에 ALTER 두 번: - 조회수: ADD COLUMN view_count INT DEFAULT 0 - 사진: ADD COLUMN photo VARCHAR(200) NULL v9 의 fetch 패턴을 그대로 응용해 화면 갱신. 동적 SQL 의 첫 등장.

v9 의 의미

v8 에서 만든 REST API 가 처음으로 클라이언트와 만나는 차시. v9 부터 우리 사이트는:

  • 기존 v6 의 JSP 게시판 동작 (그대로)
  • 그 위에 비동기 댓글이 매끄럽게 동작

두 형태가 공존하며, 부분 부분 비동기로 옮겨가는 길.

🔄 Before / After

v8 끝

REST API 가 만들어졌다. 하지만 클라이언트가 안 씀.

v9 끝

JS 가 fetch 로 REST API 호출. 댓글이 새로고침 없이 등장. 새 시대의 웹 인터랙션.

이번 차시의 데이터 흐름

사용자 클릭
JS fetch
REST API
DB
JSON 응답
JS DOM 추가
JS 가 흐름의 두 끝(시작·끝)을 잡고 화면 전환을 직접 통제

정리

오늘 들고 가는 것

  • fetch + JSON.stringify 로 비동기 POST
  • 응답 JSON 으로 직접 DOM 조작
  • event.preventDefault() 로 form 기본 동작 막기
  • async/await 로 깔끔한 비동기 코드
  • v9 — 새로고침 없는 사용자 경험의 첫 구현

다음: v10 자잘한 비동기 + 첨부파일 — 같은 패턴 응용.