개발 학습일지(TIL)

TIL : Next.js, useState와 useEffect 이용하여 댓글 실시간 업데이트

Veams 2023. 5. 12.

문제상황

Next.js 환경이다. Next.js는 리액트의 기능을 많이 통합하는 프레임워크로, 리액트 훅에 대한 지식이 있어야 한다. 

 

현재 기능 상황은 댓글 작성시 새로고침을 해야만 댓글 목록이 업데이트 된다. 

이 기능을, 댓글 작성시 새로고침 없이 댓글 목록이 최신화 되도록 업그레이드 하려고 한다.

 

코드는 다음과 같다.

'use client'
import { useEffect, useState } from "react";

export default function Comment(props){

    let [comment, setComment] = useState('')
    let [data, setData] = useState([])

    useEffect(()=> {
        fetch('/api/comment/list?id=' + props._id, {method: "GET"}).then(r=>r.json())
        .then((result)=>{
            setData(result)}) //state 변경 함수 적용
    }, [])
   
 
    const addComment = () => {
        fetch('/api/comment/new' , {
            method : 'POST',
            body : JSON.stringify({comment : comment, _id : props._id })})
        .then(() => {
            alert('작성 완료')
            
        })
    }
 

    return (
      <div>
        <hr></hr>
        <div>
          <input onChange={(e)=>{ setComment(e.target.value)}}/>

         <button onClick={addComment}>작성</button>
        </div>
        <hr></hr>
        <div>댓글 목록</div>
        {
        data.length > 0 ?
        data.map((v, i)=>{
                return (
                <p key={i}>
                    <div>{v.author_name}</div>
                    <div>{v.content}</div>
                </p>)}
                ) : '댓글 없음'}
      </div>
    );
}

 

 

시도1 : deps 위치 [] 삭제하기

useEffect(function, deps)

useEffect에서  deps에 빈배열 []을 넣은 상태라, 현재는 컴포넌트가 렌더링 될 때 한 번만 userEffect실행된다. 

 

[]를 생략하면 해당 컴포턴트가 렌더링 될 때마다 useEffect가 실행된다.

이 경우, 댓글 추가와 함께 동시에 업데이트 되지만, useEffect 가 계속 실행되면서 서버에서 댓글 목록을 계속 반환 받게 된다. 

    useEffect(()=> {
        fetch('/api/comment/list?id=' + props._id, {method: "GET"}).then(r=>r.json())
        .then((result)=>{
            setData(result)}) 
    }, [])   //   []을 없애면 계속 업데이트 된다.

deps의 빈배열 []을 삭제한뒤, console.log(result) 코드를 기재하는 순간, 개발자도구에 콘솔 로그 결과가 급속도로 반복 찍히는 것을 확인할 수 있었다. 불필요하게 계속 렌더링이 되면서, 서버에서 데이터 조회도 반복되는 것이다. 이것은 비효율적인 방법이여서 대안을 찾았다.

시도2 : deps 위치에 data 기재

    let [data, setData] = useState([])
    
    useEffect(()=> {
        fetch('/api/comment/list?id=' + props._id, {method: "GET"}).then(r=>r.json())
        .then((result)=>{
            setData(result)}) // data 상태를 바꾸는 setData() 실행
    }, [data])   //   data가 변경되면 useEffect 실행

dependency배열에 data를 넣었다. 댓글 목록인 data가 변경될 때만 렌더링 되는 것을 기대했다. 

하지만 콘솔로그가 시도1과 같이 무한으로 찍히는 것으로 보아, 잘못된 것을 인지했다.

- (1)댓글 목록인 data 상태가 바뀔 때 useEffect 내부 함수가 다시 실행되게 의도했는데, 이로인한 문제가 있다.

- (2) useEffect내부에서 setData(result)의 'data'상태를 바꾸는 setData함수를 다시 호출한다. 이 때문에 무한루프에 빠지게 만든 상태이다. 

 

시도3 : useState 로 댓글 작성 후 댓글목록 리프레시 기능 추가

 

 

콘솔 로그가 두 번찍힌다. Next.js는 SSR 서버사이드 렌더링 이여서, 서버에서 미리 페이지를 렌더링하고, 클라이언트에 동적 부분을 다시 렌더링 하는 과정에서 콘솔 로그가 두 번찍힐 수 있다고 한다. 

 

23.09.12 추가

리액트는 strict mode모드가 있어서 개발과정에서 어떤 문제를 찾기 위해 엄격하게 관리할 수 있다고 한다. 

이전에는 마지막 시도를 통해 문제를 해결했지만, 동일한 현상이 생겼을 때 지금은 3번째 시도로 처리하려고 한다. 이유는 시도4번째 방법보다 더 편리하기 때문이다. 시도4는 이 문제를 이해하고 해결한 게 아니라고 생각한다.

https://stackoverflow.com/questions/48846289/why-is-my-react-component-is-rendering-twice

https://stackoverflow.com/questions/72711560/why-components-in-nextjs-render-twice-after-refreshing

    useEffect(()=> {
        fetch('/api/comment/list?id=' + props._id, {method: "GET"})
        .then(r=>r.json())
        .then((result)=>{
            console.log(result)
            setData(result)}) //state 변경 함수 적용
            setRefresh(false) // 댓글 목록이 업데이트 되었다면 refresh를 다시 false로 설정

    }, [refresh])

    const addComment = () => {
        fetch('/api/comment/new' , {
            method : 'POST',
            body : JSON.stringify({comment : comment, _id : props._id })})
        .then(() => {
            alert('작성 완료')
            setRefresh(true) // 새 댓글 작성 후 refresh를 true로 설정하여 댓글목록 리프레시
        })
    }

 

시도4 및 해법 : fetch 변경~댓글 작성후, 변경된 댓글목록 보내주기

useEffect로만 뭘 하려는 대신, fetch 로 api를 호출할 때 서버쪽의 기능을 바꾸기로 했다.

 

기존에는 새글 작성시, insertOne으로 MongoDB에 댓글DB만 추가되는 형태였다.

이 기능을 댓글 작성후, 업데이트 된 댓글 배열을 클라이언트로 다시 보내주는 것으로 변경하였다. 

 

그리고 클라이언트에서는 addComment에서 업데이트 된 배열을 받아 useState에 저장하는 것으로 수정하였다.

 

//서버쪽 (변경전)
    try {
      const db = (await connectDB).db("forum");
      let result = await db.collection("comment").insertOne(comment);

      return res.status(200).json(result, "작성 완료");
    } catch (error) {
      return res.status(500).json("작성 실패");
    }
  }
 
 //서버쪽 (변경 후)
  try {
      const db = (await connectDB).db("forum");
      let result = await db.collection("comment").insertOne(comment);

      let newresult = await db.collection('comment').find({ parent : new ObjectId(req.body._id)}).toArray();  // 기능추가
      return res.status(200).json(newresult, "작성 완료");
    } catch (error) {
      return res.status(500).json("작성 실패");
    }
   //클라이언트 쪽 (변경 후)
'use client'
import { useEffect, useState } from "react";

export default function Comment(props){

    let [comment, setComment] = useState('')
    let [showComment, updateComment] = useState([])   //useState 추가

 
    useEffect(()=> {
        fetch('/api/comment/list?id=' + props._id, {method: "GET"})
        .then(r=>r.json())
        .then((result)=>{
            console.log(result)
            updateComment(result)})
    }, [data])          //초기 렌더링 시, 그리고 data가 업데이트 될 때 작동

    const addComment = () =>{
        fetch('/api/comment/new' , {
            method : 'POST',
            body : JSON.stringify({comment : comment, _id : props._id })})
            .then(r=>r.json())
            .then((newResult)=>{
            updateComment(newResult)       //서버에서 반환된 newResult 배열을 useState 변경 함수에 저장
            })
        alert('작성 완료')
    }

    return (
      <div>
        <hr></hr>
        <div>
          <input onChange={(e)=>{ setComment(e.target.value)}}/>

          <button onClick={addComment}>작성</button>
        </div>
        <hr></hr>
        <div>댓글 목록</div>
        <hr></hr>
        {
        showComment.length > 0 ?
        showComment.map((v, i)=>{
                return (
                <div key={i}>
                    <div>{v.author_name}</div>
                    <div>{v.content}</div>
                    <hr></hr>
                </div>)}
                ) : '댓글 없음'}
      </div>
    );
}
 

 

 

+기능 추가 : 댓글 작성 후 입력란 초기화

댓글 입력 후, 댓글 입력란이 초기화 되지 않아서, 새로운 내용을 작성하려면 이미 채워진 내용을 삭제해야하는 불편함이 있다.

그래서 댓글 작성시, 댓글 입력란에 기재된 내용도 초기화하는 기능을 추가한다.

 

 

    const addComment = () =>{
        fetch('/api/comment/new' , {
            method : 'POST',
            body : JSON.stringify({comment : comment, _id : props._id })})
            .then(r=>r.json())
            .then((newResult)=>{
                updateComment(newResult)
            })
        setComment('') // 댓글 작성 후 입력란 초기화
        alert('작성 완료')
    }

    return (
      <div>
        <hr></hr>
        <div>
          <input value={comment} onChange={(e)=>{ setComment(e.target.value)}}/>   // input태그 value 값을 comment와 연결

          <button onClick={addComment}>작성</button>
        </div>
        <hr></hr>
        <div>댓글 목록</div>

 

23.09.02 추가

다른 프로젝트 개발 중에 비슷한 일이 일어나서 다시 확인해보았다.

세 번째 시도 했던 방법과 거의 동일하다.

이전에는 두 번씩 리프레시 되었지만 지금은 한 번만 리프레시 된다.

useEffect의 의존성 배열 []에 사용할

useState(0)으로 생성하고, useEffect 내의 함수를 작동할 때 마다 useState()의 기존 값에서 +1 씩 증가하게했더니 리프레시 없이 비동기적으로 작동한다. useState()초기값을 null에서 하고, 즉 useState(null)에서  useState(true) 값으로 바꾸어도 잘 작동한다.

 

 

참고 내용

 

16. useEffect를 사용하여 마운트/언마운트/업데이트시 할 작업 설정하기 · GitBook

16. useEffect를 사용하여 마운트/언마운트/업데이트시 할 작업 설정하기 이번에는 useEffect 라는 Hook 을 사용하여 컴포넌트가 마운트 됐을 때 (처음 나타났을 때), 언마운트 됐을 때 (사라질 때), 그리

react.vlpt.us

 

[React] 리액트 Hooks : useEffect() 함수 사용법

🚀 useEffect()란? useEffect() 함수는 React component가 렌더링 될 때마다 특정 작업(Sied effect)을 실행할 수 있도록 하는 리액트 Hook입니다. 여기서 Side effect는 component가 렌더링 된 이후에 비동기로 처리되

cocoon1787.tistory.com

댓글