React | '프립' 모티브 프로젝트 코드공유

연정·2021년 11월 28일
0

Personal Projects

목록 보기
6/6
post-thumbnail

프로젝트를 진행하며 협업, 새로운 기술 스택, 코드 작성 기술 등 다양한 배움이 있겠지만, 개인적으로 그 중에서 가장 큰 의미로 다가오는 건 실제로 직면한 문제를 해결하기 위해 다양한 방식을 고민해보고 그에 대한 해결책을 몸소 습득하게 되는 것이다.

2차 프로젝트를 진행하면서 많은 고민을 하게 만들었던 문제들과, 그 고민을 통해 얻은 결과를 정리해보고자 한다.

고민 no.1

[ PROBLEM ]

리뷰의 좋아요 기능을 구현하며 여러가지 문제를 맞이했는데, 그 중 가장 날 애먹였던 건 좋아요 버튼 클릭 시 모든 버튼의 숫자가 동기화 된다는 문제였다. map으로 묶여있는 버튼들이라 어떤 버튼이 클릭되었는지 명확하게 파악할 수 없어 발생하는 문제라고 판단이 되었음에도, 어떻게 각 버튼에 접근해야할 지 감이 잡히지 않았다.

[ SOLUTION ]

긴 고민 끝에 멘토님에게 얻은 힌트.

  • 리뷰 전체를 map 메소드를 활용하여 복사
  • 클릭된 버튼의 id값과 같은 id를 가진 리뷰는 좋아요 수를 대체
  • 나머지 버튼은 그대로 복사
  • reviews state를 새롭게 정의하여 화면 재랜더링

얻은 힌트를 열심히 조합한 결과 아래와 같은 코드를 작성하였고, 성공적으로 기능 구현을 할 수 있었다.

export default function Review() {
  const [reviews, setReviews] = useState([]);

  function handleLikes(id) {
    fetch(`${API.reviews}/review/like`, {
      method: 'POST',
      headers: {
        Authorization: localStorage.getItem('token'),
      },
      body: JSON.stringify({
        review_id: id,
      }),
    })
      .then(res => res.json())
      .then(res => {
        const newData = reviews.map(review => {
          if (id === review.id) {
            return { ...review, likes: res.results.likes };
          } else {
            return review;
          }
        });

        setReviews(newData);
      });
  }
  
  return 
  	<Likes onClick={() => {handleLikes(review.id);}} />

Key Point

  • 새로운 변수를 선언하고 reviewsmap하여 저장
  • 배열의 얕은 복사본을 만드는 스프레드 연산자를 활용
  • {...review, likes: res.results.likes}의 형태로 사용할 수 있다는 점 기억하기

고민 no.2

[ PROBLEM ]

리뷰 작성 섹션은 어찌저찌 submit까지 무사히 되도록 구현해놓았는데, submit 이후 input 창이 비워지지 않는 문제가 발생했다. 그냥 넘어갈 수도 있겠지만, 실제 사용자가 이용하는 상황이라면 실수로라도 동일한 값이 재등록되거나, 리뷰가 등록되었는지 헷갈리게 할 수 있으므로 비우는게 맞겠다는 판단이 들었다. textarea로 이루어진 부분은 value와 state값을 동기화함으로써 생각보다 쉽게 처리할 수 있었으나 input type='radio'input type='file'의 경우, 이런저런 방법을 써도 생각대로 되지 않았다.

[ SOLUTION ]

이 문제는 함수형 컴포넌트에서 DOM에 직접 접근할 수 있게 도와주는 useRef를 사용하여 해결할 수 있었다.

  • 새로운 변수에 useRef()를 선언
  • 컨트롤이 필요한 부분에 ref 속성을 부여
  • ref의 current 속성을 다양하게 활용하여 원하는 형태로 처리

라디오 버튼의 경우에는 각각의 라디오 버튼을 scoresRef라는 변수 내 배열의 요소로 저장하고, submit 후 각 버튼을 검사하며 (배열을 돌며) 모든 버튼의 체크 값을 false로 변경해줌으로써 원하는 형태를 구현할 수 있었다.

input type='file'은 조금 더 간단했는데, submit이 완료되면 reviewRef의 값을 빈 string으로 재할당하여 input을 비울 수 있었다.

또한 깨알같이 코드를 간결하게 만들어주었던 tip 중 하나!
resetReviewInputs() 함수를 선언하고, 해당 함수 내에서 reset 부분을 한 번에 컨트롤 할 수 있게 하였다.

import React, { useState, useRef } from 'react';
import { API } from '../../../config';

export default function ReviewInputBox() {
  const [reviewImg, setReviewImg] = useState();
  const [reviewScore, setReviewScore] = useState();
  const [reviewDescription, setReviewDescription] = useState();

  const scoresRef = useRef([]);
  const reviewRef = useRef();

  function resetReviewInputs() {
    setReviewImg(null);
    setReviewScore(null);
    setReviewDescription('');
    scoresRef.current.forEach(input => (input.checked = false));
    reviewRef.current.value = '';
  }

  function submitReview(e) {
    e.preventDefault();

    const formData = new FormData();
    formData.append('filename', reviewImg);
    formData.append('star_rate', reviewScore);
    formData.append('comment', reviewDescription);
    formData.append('option_id', 1);

    fetch(`${API.reviews}/review`, {
      method: 'POST',
      headers: {
        Authorization: localStorage.getItem('token'),
      },
      body: formData,
    })
      .then(res => res.json())
      .then(res => {
        if (res.message === 'SUCCESS') {
          alert('리뷰 등록이 완료되었습니다.');
        } else if (res.message === 'INVALID_TOKEN') {
          alert('먼저 로그인해주세요');
        } else {
          alert('모든 리뷰 칸을 작성해주세요.');
        }
      });

    resetReviewInputs();
  }
[라디오 버튼]

export default function StarRateInput(props) {
  return STAR_RATE_DATA.map((starRate, idx) => (
    <React.Fragment key={idx}>
      <StarRate
        type="radio"
        name="starRate"
        id={starRate.id}
        ref={ref => (props.scoresRef.current[idx] = ref)}
        onClick={() => {
          props.handleScoreChange(starRate.value);
        }}
      />
      <StarImg alt="star rate" src={starRate.img} />
    </React.Fragment>
  ));
}
[이미지 파일]

        <ImageInput
          id="ImageInput"
          type="file"
          accept="image/jpeg image/png"
          onChange={handleImgChange}
          ref={reviewRef}
        />

그 외의 소소한 TIP

  • input type='file'을 서버로 전달하면 해당 이미지를 서버에서 받아 AWS에 저장하고 업로드 링크를 재저장하는 프로세스로 진행됨. 우리가 그 이미지를 요청할 땐 AWS에 저장된 링크를 받아 화면에 그려주는 것!
  • input type='file'을 서버로 전달할 때는 json 형식이 아닌 formData 형식으로 전달해주어야 함
  • 조건부 랜더링 시 Array.length > 0을 주로 활용하는 배열과 달리, 길이를 판단할 수 없는 객체를 조건부 랜더링하기 위해서 Object.keys를 활용하면 쉽다.
    Object.keys는 객체의 키값을 배열로 저장해주는 메소드!
profile
성장형 프론트엔드 개발자

0개의 댓글