(React) 4. 복습 번외편 : Comment 기능 -5-

김동우·2021년 8월 1일
1

wecode

목록 보기
26/32
post-thumbnail

잠깐! 시작하기 전에

이 글은 wecode에서 실제 공부하고(이제 사전 스터디는 아닙니다.), 이해한 내용들을 적는 글입니다. 글의 표현과는 달리 어쩌면 실무와는 전혀 상관이 없는 글일 수 있습니다.

또한 해당 글은 다양한 자료들과 작성자 지식이 합성된 글입니다. 따라서 원문의 포스팅들이 틀린 정보이거나, 해당 개념에 대한 작성자의 이해가 부족할 수 있습니다.

설명하듯 적는게 습관이라 권위자 발톱만큼의 향기가 날 수 있으나, 엄연히 학생입니다. 따라서 하나의 참고자료로 활용하시길 바랍니다.

글의 내용과 다른 정보나 견해의 차이가 있을 수 있습니다.
이럴 때, 해당 부분을 언급하셔서 제가 더 공부할 수 있는 기회를 제공해주시면 감사할 것 같습니다.


구현(이제 진짜 마지막)

Like 기능 추가

좋아요를 구현하는 것은 다른 글들을 읽고 지나왔다면 이제는 생각보다 쉬운 일일겁니다.

어떻게 자료에 접근할지를 생각하고, 해당 기능을 가진 컴포넌트를 따로 분리해주는 작업만 하면 됩니다.

지난 글들은 처음 마주하는 상황에 대해 생각하는게 주제였다면, 이번 기능은 빠르게 넘어가겠습니다.

또한, 기능을 추가하는 과정에서 코드를 수정하거나 첨삭하는 과정을 보여드릴 수 있을 것 같습니다.

1. 접근 가능 컴포넌트

댓글을 그렸다면, 제 컴포넌트들 중 누군가는 이미 데이터를 활용할 수 있는 자격이 생겼습니다.

물론 댓글을 그린 컴포넌트가 접근할 수 있을거라고 생각합니다.

그럼 댓글을 그리는 컴포넌트에서 받는 데이터는 어떤 구조를 띄고 있는지부터 생각해봅시다.

import React from "react";
import CommentForm from "./commentForm";
import PaintComments from "./paintComments";

class Main extends React.Component {
  constructor() {
    super();
    this.state = {
      comments: [],
    };
  }
  setCommentInfo = (commentInfo) => {
    this.setState((prevState) => {
      return { ...prevState, comments: [...prevState.comments, commentInfo] };
    });
  };
  render() {
    const { setCommentInfo } = this;
    const { comments } = this.state;
    return (
      <div>
        <CommentForm setCommentInfo={setCommentInfo} comments={comments} />
        <PaintComments comments={comments} />
      </div>
    );
  }
}

export default Main;

PaintComments 의 경우 댓글정보 배열 전체를 받아 내부에서 map() 메서드로 그려내는 작업을 합니다.

그런데 배열 데이터 구조는 아래와 같습니다.

comments = [ ... , { ... , liked:bool, like:number}, { ... , liked:bool, like:number}]

제가 생각해도 해당 객체들에 접근할 방법을 즉시 떠올리는건 막막한 것 같습니다.

그런데 우린 생성 당시 유일무이한 값, id를 집어넣었습니다.

즉, 컴포넌트별로 각자 id를 가지고 있죠.

그렇다면 id 값이 동일한 객체에 대한 접근이 가능해집니다.

그걸 가시적으로 표현하기 위해서는 코드를 조금 수정할 필요가 있습니다.

import React from "react";
import CommentForm from "./commentForm";
import PaintComments from "./paintComments";

class Main extends React.Component {
  //... 위 부분은 중복입니다.
  render() {
    const { setCommentInfo } = this;
    const { comments } = this.state;
    return (
      <div>
        <CommentForm setCommentInfo={setCommentInfo} comments={comments} />
        {comments.map((elem) => {
          return (
            <PaintComments
              key={elem.id}
              userName={elem.userName}
              commentText={elem.commentText}
              like={elem.like}
            />
          );
        })}
      </div>
    );
  }
}

자, 이런식으로 map()을 Main에 가져오고, id를 key에 활용하는것까지 볼 수 있습니다.

id는 분명 고유한 값이니, CRUD 과정 어디에서도 문제가 될 일이 없을겁니다.

이제 이 값을 통해 좋아요를 구현해봅시다.

2. 이벤트 발생 컴포넌트

다시, 이벤트 발생 컴포넌트를 먼저 생각합니다.

댓글을 그리는 컴포넌트에서 버튼을 그렸으니, 당연히 PaintComments 컴포넌트가 해당 기능을 구현할 대상이 될겁니다.

이벤트의 종류는 마침 맨 처음 고민했던 onClick 이벤트가 있겠네요.

이어서 기능 추가의 과정에서 state의 당위성은 짤막하게 생각하면 됩니다.

원래 있던 부모의 데이터를 수정한다.

고로 state는 필요가 없습니다.

그럼 간단하게 handle 메서드를 하나 작성하고, 다시 부모인 Main으로 넘어옵니다.

import React from "react";

class PaintComments extends React.Component {
  handleLikeButton = () => {
    console.log(`hi`);
  };

  render() {
    const { handleLikeButton } = this;
    const { userName, commentText, like } = this.props;
    return (
      <>
        <div>
          <span>{userName}</span>
          <span>{commentText}</span>
          <button onClick={handleLikeButton}>{like}</button>
        </div>
      </>
    );
  }
}

export default PaintComments;

3. 좋아요 구현 컴포넌트

좋아요를 구현하는 컴포넌트는 이제 Main입니다.

map() 메서드를 통해 각 PaintComments 컴포넌트를 그려내고 있으니까요.

그럼 이제 우린 어떻게 해당 데이터를 수정할 것인가? 고민해야 합니다.

like 기능은 기존 comments 배열 내부 객체와 일치하는 컴포넌트만 like 할 수 있어야 합니다.

다시, comments 배열 내 객체 하나는 컴포넌트 하나이니, 해당 컴포넌트의 like만 반영하면 됩니다.

그걸 위해서는 어쩔 수 없이 배열은 전체를 순회해야 합니다(search에서의 단점).

index 접근은 자바스크립트에서 배열 = hash table이므로, 시간복잡도를 줄일 수 있습니다.

대부분 index를 활용한 데이터의 접근은 정적인 부분이 아니면 꺼려지는 것은 사실입니다만, 이번 경우는 2가지 방식으로 구현해보고, 생각해보겠습니다.

배열 순회 setState 메서드 (id ver.)

먼저, 다음과 같이 Main의 state인 comments 배열을 수정하는 코드를 작성했습니다.

  addCommentLike = (id) => {
    this.setState((prevState) => {
      let addLikeArr = prevState.comments.map((post) => {
        if (post.id === id) {
          if (!post.liked) {
            return { ...post, like: post.like + 1, liked: true };
          } else {
            return { ...post, like: post.like - 1, liked: false };
          }
        }
        return post;
      });
      return { ...prevState, comments: addLikeArr };
    });
  };

이제 이 메서드를 버튼에 할당해주기만 하면 사실상 기능 구현은 마무리가 됩니다.

또한 당장은 비효울적인 것 같아 보이지만, 한 포스트에 댓글이 2M 이상으로 달리는 경우에는 다른 자료구조를 선택하지 않을까? 하는 생각도 있습니다.

Main 전체를 한 번 봅시다.

import React from "react";
import CommentForm from "./commentForm";
import PaintComments from "./paintComments";

class Main extends React.Component {
  constructor() {
    super();
    this.state = {
      comments: [],
    };
  }
  setCommentInfo = (commentInfo) => {
    this.setState((prevState) => {
      return { ...prevState, comments: [...prevState.comments, commentInfo] };
    });
  };

  addCommentLike = (id) => {
    this.setState((prevState) => {
      let addLikeArr = prevState.comments.map((post) => {
        if (post.id === id) {
          if (!post.liked) {
            return { ...post, like: post.like + 1, liked: true };
          } else {
            return { ...post, like: post.like - 1, liked: false };
          }
        }
        return post;
      });
      return { ...prevState, comments: addLikeArr };
    });
  };

  render() {
    const { setCommentInfo, addCommentLike } = this;
    const { comments } = this.state;
    return (
      <div>
        <CommentForm setCommentInfo={setCommentInfo} comments={comments} />
        {comments.map((elem) => {
          return (
            <PaintComments
              key={elem.id}
              id={elem.id}
              userName={elem.userName}
              commentText={elem.commentText}
              like={elem.like}
              addCommentLike={addCommentLike}
            />
          );
        })}
      </div>
    );
  }
}

export default Main;

눈에 띄는 것은 key와 id 전달값이 동일하다는 점인데, 이는 변경 가능성이 없더라도 마음에 걸리기만 하는 부분입니다.

아무리 그렇더라도 key로 전달하는 props를 key로만 사용하지 않는 것은 Array.map()-key 에 대한 React 팀의 생각에 분명 반하는 행동이니까 말이죠.

그럼 이제 index로 접근하는 방법을 고민해봅시다.

index로 접근하는 방식


  addCommentLike = (index) => {
    this.setState((prevState) => {
      let addLikeArr = prevState.comments.map((post, idx) => {
        if (idx === index) {
          if (!post.liked) {
            return { ...post, like: post.like + 1, liked: true };
          } else {
            return { ...post, like: post.like - 1, liked: false };
          }
        }
        return post;
      });
      return { ...prevState, comments: addLikeArr };
    });
  };

자, index로 해당 컴포넌트에 접근하는 방법입니다.

이 때, props 에는 index를 부여해줘야 하는데, 원본 배열의 index 값을 렌더마다 부여하게 됩니다.

문제는 이렇게 해도 큰 영향이 있나? 고민하게 된다는 점입니다.

import React from "react";
import CommentForm from "./commentForm";
import PaintComments from "./paintComments";

class Main extends React.Component {
  constructor() {
    super();
    this.state = {
      comments: [],
    };
  }
  setCommentInfo = (commentInfo) => {
    this.setState((prevState) => {
      return { ...prevState, comments: [...prevState.comments, commentInfo] };
    });
  };

  // addCommentLike = (id) => {
  //   this.setState((prevState) => {
  //     let addLikeArr = prevState.comments.map((post) => {
  //       if (post.id === id) {
  //         if (!post.liked) {
  //           return { ...post, like: post.like + 1, liked: true };
  //         } else {
  //           return { ...post, like: post.like - 1, liked: false };
  //         }
  //       }
  //       return post;
  //     });
  //     return { ...prevState, comments: addLikeArr };
  //   });
  // };

  addCommentLike = (index) => {
    this.setState((prevState) => {
      let addLikeArr = prevState.comments.map((post, idx) => {
        if (idx === index) {
          if (!post.liked) {
            return { ...post, like: post.like + 1, liked: true };
          } else {
            return { ...post, like: post.like - 1, liked: false };
          }
        }
        return post;
      });
      return { ...prevState, comments: addLikeArr };
    });
  };

  render() {
    const { setCommentInfo, addCommentLike } = this;
    const { comments } = this.state;
    return (
      <div>
        <CommentForm setCommentInfo={setCommentInfo} comments={comments} />
        {comments.map((elem, idx) => {
          return (
            <PaintComments
              key={elem.id}
              index={idx}
              userName={elem.userName}
              commentText={elem.commentText}
              like={elem.like}
              addCommentLike={addCommentLike}
            />
          );
        })}
      </div>
    );
  }
}

export default Main;

위 코드가 Main 컴포넌트입니다.

사실 두 로직의 차이가 없어보이지만, 내부에서 전달받고, 사용하는 값은 각각 다릅니다.

key로 제공받는 값과 중복을 피했지만, 뭔가 부족하다면 userName 등에 접근한다던가 하는 방법도 있을 것 같습니다.

혹은 DB에는 있는 user_id 라는 데이터로도 접근할 수 있겠죠.

이상으로 좋아요 구현도 마치겠습니다.

마치며

해당 시리즈가 얼마나 유익했는가 고민해봤는데, 아직 제 수준이 상당히 낮은것 같다는 생각을 했습니다.

더 발전해서 좋은 시리즈를 쓸 수 있다면 좋을텐데, 이게 참 마음처럼 빨리 되는 것 같지는 않습니다.

그럼에도 항상 보다 나은 저를 맞이할 수 있기를 바라는 마음으로 글을 작성합니다.

언제까지 이런 마음일지 모르겠지만, 아직은 그렇습니다.

그럼 읽어주셔서 감사합니다.

다음엔 더 좋은 자료들로 찾아뵙겠습니다.

1. intro
2. 댓글 정보 구조
3. 이벤트 컴포넌트
4. 댓글 구현 컴포넌트

3개의 댓글

comment-user-thumbnail
2021년 8월 1일

우동님 오늘도 고봉밥 글 잘 보고 갑니다~💔

1개의 답글