프로젝트를 진행하며 협업, 새로운 기술 스택, 코드 작성 기술 등 다양한 배움이 있겠지만, 개인적으로 그 중에서 가장 큰 의미로 다가오는 건 실제로 직면한 문제를 해결하기 위해 다양한 방식을 고민해보고 그에 대한 해결책을 몸소 습득하게 되는 것이다.
2차 프로젝트를 진행하면서 많은 고민을 하게 만들었던 문제들과, 그 고민을 통해 얻은 결과를 정리해보고자 한다.
리뷰의 좋아요 기능을 구현하며 여러가지 문제를 맞이했는데, 그 중 가장 날 애먹였던 건 좋아요 버튼 클릭 시 모든 버튼의 숫자가 동기화 된다는 문제였다. map
으로 묶여있는 버튼들이라 어떤 버튼이 클릭되었는지 명확하게 파악할 수 없어 발생하는 문제라고 판단이 되었음에도, 어떻게 각 버튼에 접근해야할 지 감이 잡히지 않았다.
긴 고민 끝에 멘토님에게 얻은 힌트.
map
메소드를 활용하여 복사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
- 새로운 변수를 선언하고
reviews
를map
하여 저장- 배열의 얕은 복사본을 만드는
스프레드 연산자
를 활용{...review, likes: res.results.likes}
의 형태로 사용할 수 있다는 점 기억하기
리뷰 작성 섹션은 어찌저찌 submit까지 무사히 되도록 구현해놓았는데, submit 이후 input 창이 비워지지 않는 문제가 발생했다. 그냥 넘어갈 수도 있겠지만, 실제 사용자가 이용하는 상황이라면 실수로라도 동일한 값이 재등록되거나, 리뷰가 등록되었는지 헷갈리게 할 수 있으므로 비우는게 맞겠다는 판단이 들었다. textarea
로 이루어진 부분은 value와 state값을 동기화함으로써 생각보다 쉽게 처리할 수 있었으나 input type='radio'
나 input type='file'
의 경우, 이런저런 방법을 써도 생각대로 되지 않았다.
이 문제는 함수형 컴포넌트에서 DOM에 직접 접근할 수 있게 도와주는 useRef
를 사용하여 해결할 수 있었다.
useRef()
를 선언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
는 객체의 키값을 배열로 저장해주는 메소드!