요구사항 정의
구현해야 할 기능
1) 공통
- UI 구현하기
- API 명세서 작성하기
2) 본문 CRUD 구현
- 본문 리스트 조회하기
- 본문 조회하기
- 본문 추가하기
- 본문 삭제하기
- 본문 수정하기
3) 배포
- json-server 서버 배포 (heroku 사용)
- 리액트 프로젝트 배포 (S3, vercel 등 자유)
필수 요구사항
- 동적 라우팅 사용
- 1개 이상의 Custom Hook 구현
- Form에 유효성 검증 기능 적용
- 버튼 컴포넌트 1개로 모든 버튼 구현
- 배포된 결과물에서는 console.log()가 보이지 않도록 처리
- .env를 이용해서 API 서버의 URL 코드 상에서 숨기도록 처리
프로젝트 설명
- 자신이 좋아하는 과자를 소개할 수 있는 웹페이지를 만들고자 했다.
- 2023.04.28 ~ 2023.04.30 기간동안 진행하였으며 리액트를 사용하여 기본 CRUD를 구현했다.
기술 스택
구분 | 사용 언어 및 패키지 |
Front-End | React, Axios, React-query, React-router-dom |
Back-End | Json-server |
Environment | Visual Studio Code, Git, GitHub |
Communication | Notion, Gather Town |
구현 화면
폴더 구조
📦src
┣ 📂api
┃ ┗ 📜posts.js
┣ 📂components
┃ ┗ 📜Button.jsx
┣ 📂hooks
┃ ┗ 📜useDataFind.js
┣ 📂pages
┃ ┣ 📜Detail.jsx
┃ ┣ 📜Footer.jsx
┃ ┣ 📜Header.jsx
┃ ┣ 📜List.jsx
┃ ┣ 📜Main.jsx
┃ ┗ 📜Post.jsx
┣ 📜App.jsx
┣ 📜index.js
┣ 📜reportWebVitals.js
┗ 📜reset.css
메인 페이지
- 메인페이지 내 소개 이미지 hover 이벤트, 클릭 시 게시글 리스트 페이지로 이동
- 메인페이지 및 푸터 반응형으로 구현
- 디자인 레퍼런스는 그라운드 시소
게시글 리스트 페이지
- axios, 리액트 쿼리 사용해 db.json 데이터 조회
const MainContentBottom = styled.section`
padding: 30px 15px 30px 25px;
display: grid;
justify-content: center;
align-items: center;
grid-template-columns: repeat(5, 1fr);
grid-auto-rows: minmax(0, auto);
grid-gap: 40px 20px;
`
- CSS display: grid를 이용해 요소들이 일정하게 보이도록 구현
- 게시글마다 hover 이벤트 부여
게시글 상세 페이지
import { useState, useEffect } from 'react';
function useDataFind(data, id) {
const [foundData, setFoundData] = useState({});
useEffect(() => {
const foundItem = data.find((item) => item.id === id);
setFoundData(foundItem);
}, [data, id]);
return foundData;
}
export default useDataFind
- id가 일치하는 데이터를 찾아올 때 커스텀훅으로 구현
<Button size={'small'} color={'white'} onClick={() => {
navigate(`/snackRecos/post`, {
state: {
id: foundData.id,
author: foundData.author,
title: foundData.title,
body: foundData.body,
like: foundData.like,
url: foundData.url,
}
})
}}>수정</Button>
- 상세페이지에서 수정하기로 넘어갈 때 useNavigate를 이용해 해당 상세페이지의 정제된 데이터를 넘겨줌
게시글 작성 페이지
- 게시글 신규 작성 / 수정 모두 Post.jsx 하나의 컴포넌트로 재사용 처리
// 페이지가 처음 마운트되었을 때 location.state값이 있다면 (수정하기 버튼으로 넘어왔다면)
// 기존에 작성했던 내용 input에 보여지도록 처리
useEffect(() => {
if (location.state !== null) {
setForm({
...form,
author: location.state.author,
title: location.state.title,
body: location.state.body,
url: location.state.url,
});
}
}, [])
// location.state값이 있다면 (수정하기 버튼으로 넘어왔다면)
// 보여지는 버튼 명 및 연결된 onClick 함수 다르게 처리
<Button
type="button"
size={'small'}
color={'red'}
onClick={
location.state !== null ? onmodifyClickHandler : onSubmitClickHandler
}
>
{location.state !== null ? '수정하기' : '작성하기'}
</Button>
- useNavigate로 넘어온 location.state값 null 여부에 따라 기존 데이터 화면에 뿌려주기 또는 버튼명, 버튼의 onClick 함수 다르게 처리되도록 설정
트러블슈팅
버튼 컴포넌트 스타일 재사용 관련
스타일드 컴포넌트를 활용한 코드
import React from 'react'
import styled from 'styled-components'
function Button({ size, color, onClick, children }) {
const Button = styled.button`
${() => sizeHandler(size)};
${() => colorHandler(color)};
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
border-radius: 5px;
cursor: pointer;
&:hover {
filter: brightness(90%);
}
`
const sizeHandler = (size) => {
switch (size) {
case 'large':
return `width: 145px; height: 55px; font-size: 20px;`;
case 'small':
return `width: 100px; height: 45px; font-size: 15px;`;
default:
return;
}
}
const colorHandler = (color) => {
switch (color) {
case 'white':
return `border: 2px solid #000; background-color: #fff`;
case 'red':
return `border: 2px solid #000; background-color: #ff4429; color: white`;
default:
return;
}
}
return (
<Button
onClick={onClick}
>
{children}
</Button>
)
}
export default Button
스타일드 컴포넌트를 사용하지 않은 코드
function Button({ size, color, onClick, children }) {
const buttonStyle = getButtonStyle(size, color);
return (
<button
onClick={onClick}
style={buttonStyle}
>
{children}
</button>
)
}
// 버튼 기본 스타일 정의
function getButtonStyle(size, color) {
const sizeStyle = getSizeStyle(size);
const colorStyle = getColorStyle(color);
return {
...sizeStyle,
...colorStyle,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontWeight: 'bold',
borderRadius: '5px',
cursor: 'pointer',
'&:hover': {
filter: 'brightness(90%)',
},
};
}
// 버튼 사이즈 정의
function getSizeStyle(size) {
switch (size) {
case 'large':
return {
width: '145px',
height: '55px',
fontSize: '20px',
};
case 'small':
return {
width: '100px',
height: '45px',
fontSize: '15px',
};
default:
return {};
}
}
// 버튼 색상 정의
function getColorStyle(color) {
switch (color) {
case 'white':
return {
border: '2px solid #000',
backgroundColor: '#fff',
};
case 'red':
return {
border: '2px solid #000',
backgroundColor: '#ff4429',
color: 'white',
};
default:
return {};
}
}
export default Button
❓
버튼 컴포넌트 하나로 처리하기 위해 props에 따라 동적으로 처리해야 했다. 따라서 스타일드 컴포넌트 코드를 function 안에 넣어야 했는데 function 안에 스타일을 넣은 건 권장되지 않기 때문에 렌더링이 될 때마다 warning 문구를 콘솔창에 계속 띄워주었다. 이를 보지 않을 방법에 있을지 고민하다가 스타일드 컴포넌트를 아예 사용하지 않고 함수로 구현했다.
이에 대한 해답을 바로 구할 수 없어 스타일드를 컴포넌트를 사용해서 만든 컴포넌트와 함수만으로 구현한 컴포넌트 중에 어떤 것이 더 스타일드 컴포넌트를 잘 사용하고 있는지, 또한 더 효율적인지 chatGPT에 질문을 남겼다.
💡
두 코드 모두 버튼을 생성하는데 있어서 필요한 스타일을 정의하고 있다. 그러나 스타일드 컴포넌트 사용 코드(이하 코드 A)는 버튼 스타일을 생성할 때 styled-components를 사용하고 있고, 함수 사용 코드(이하 코드 B)는 일반 CSS 스타일을 JavaScript 객체로 생성해서 버튼에 적용하고 있다.
코드 B는 코드 A에 비해 몇 가지 장점을 가지고 있다. 먼저 B는 styled-components를 사용하지 않기 때문에 프로젝트에 추가적인 라이브러리를 설치할 필요가 없다. 또한 B는 스타일을 JavaScript 객체로 생성해서 버튼에 직접 적용하기 때문에 컴포넌트가 렌더링 될 때마다 스타일이 다시 생성되는 문제가 발생하지 않는다.
따라서, B가 리액트와 스타일드 컴포넌트를 더 효율적으로 사용하고 있다고 볼 수 있다.
https://github.com/hansololiviakim/whatTheSnack
질의사항
시니어 코치님과의 질의
❓ 리덕스는 애플리케이션의 전역 상태 관리 라이브러리, 리액트 쿼리는 데이터를 가져오기 위한 라이브러리라고 이해했다. 정말 간단한 게시판 CRUD를 만들면서 리액트 쿼리만 사용했는데 사용해 보니 바로 DB에 넘겨주면 되는데 리덕스를 거쳐야 하는지 궁금해졌다. 리액트 쿼리와 리덕스를 같이 사용하는 상황이 궁금하다. 예를 들어 리액트 쿼리로 데이터를 가져와서 그걸 리덕스에 저장한 다음 CRUD에 예외처리를 걸고 모든 요청이 Success일 때만 쿼리를 이용해 DB에 변경된 니용을 업데이트 처리하는 식으로 진행해야 하는지?
💡 CRUD 데이터를 굳이 리덕스로 옮겨서 관리할 필요는 없다. 클라이언트 사이드의 상태관리는 많이 줄어드는 추세다. 초기 세팅이 많고 하나를 바꾸면 연관해서 바꿔야 할 코드가 많기 때문이다. 어떤 회사들은 클라이언트 사이드를 아예 안쓰기도 한다. 서버 통신 없이 자체적으로 관리해야만 하는 state는 리덕스로 별도로 빼서 관리하거나, 최근 많이 쓰는 리코일을 사용하면 된다. (ContextAPI도 있음) 이런 경우의 예로는 다크모드가 있다.
✨ [답변을 듣고 추가로 찾아본 내용] 리덕스를 이용해 상태관리를 하는 예시
- 로그인 상태
👉🏼 애플리케이션 전체에서 사용되므로, 리덕스로 상태 관리를 하는 것이 유리
- 데이터 캐싱
👉🏼 애플리케이션에서 사용되는 데이터를 캐싱하여 불필요한 API 요청을 최소화하며, 캐시된 데이터는 리덕스로 관리 가능
- 사용자 인터페이스 설정
👉🏼 사용자가 선택한 언어, 테마 등의 설정 정보는 애플리케이션 전체에서 사용되므로 리덕스에서 상태 관리하는 것이 유리 (말씀해 주신 다크모드 등)
- 다국어 지원
👉🏼 위의 예시와 같이 현재 언어 상태 리덕스로 관리하면 됨
❓ TDD까지는 못하더라도 테스트 코드를 작성하면서 프로젝트를 진행해보고 싶은데, 아직 테스트 코드를 다양하게 작성하는게 어렵다. 결국 chatGPT를 이용해서 테스트 코드를 작성해 보게 될 텐데 지금 상황에서 테스트 코드를 연습하는게 더 도움 될지, 다양한 예외처리를 연습하는 게 더 도움 될지 궁금하다.
💡 테스트코드도 결국 예외처리가 잘 되어있어야 다양한 테스트를 진행할 수 있다. 예외 사항은 대부분 통신에서 비롯되므로 지금 상태에서는 HTTP에서 정의한 오류 케이스 (200, 400, 500대 에러 등) 에 대해 깊게 공부해해 보는 걸 추천한다. 그 지식이 쌓이 상태에서 예외 처리를 촘촘하게 한 다음 테스트 코드로 넘어가는 게 좋을 것 같다. 실무에서는 QA가 테스트를 전담으로 해주긴 하는데 테스트 코드와 예외 처리를 개발자가 미리 알고 작성하는 것과 모르고 작성하는 건 많이 다르다. 따라서 어느 하나만 해야 한다고 말할 수는 없고 나중에는 예외 처리와 테스트 코드 모두 잡고 가면 좋다.
❓ 그러면 예외처리를 할 때 axios를 콜 할때도 예외처리를 하고, 그 값을 받아오는 mutation에서도 예외처리를 하는 등 모든 상황에서 세세하게 예외처리를 하는게 좋은지? 아니면 겹치는 부분에 대해서는 하나로 처리하는 게 좋은지 궁금하다.
💡 예외처리는 예외 자체를 감싸는 것이다. 한꺼번에 해도 되고 메서드 전체를 감싸도 된다. 특정 상태에 따라 다르기 때문에 디테일하게 답변하긴 어렵고 프로젝트를 하면서 백엔드와 협업 시 시행착오를 겪어보면 좋을 것 같다. 그래야 감이 잡힌다. 백엔드와 협업 시 특정 예외처리에 대해 약속을 하고 진행할텐데 별도로 클라이언트에서 처리해줘야 하는 부분이 있다. 이것 또한 경험해봐야 감이 잡힌다.
기술매니저님과의 질의
❓ 버튼 컴포넌트 스타일드 컴포넌트 사용 코드, 미사용 코드 중 어느 것이 더 효율적인 코드인지 궁금하다.
💡 (답변 대기 중)
❓ 리렌더링을 최소화할 수 있도록 데이터 흐름을 파악하는 건 계속 연습해 보는 수밖에 없는지..!
💡 (답변 대기 중)
✅ mutation.mutate의 인자는 반드시 한 개 여야한다는걸 몰라서 인자를 두 개 넘겨주는 바람에 왜 통신이 안되는지 6시간을 고민해보기도 하고😂 heroku로 json server를 올리는데 자꾸 경로를 잘 못 잡아와서 보니 루트 폴더 하위에 프로젝트 폴더를 또 따로 만들어서 못 찾고 있는 것이기도 했다. 협업은 페어분이 아이디어가 좋으셔서 기획할 때부터 여러 주제가 나왔고, 의사소통도 수월해서 좋은 팀플 맛볼 수 있는 시간이었다. 개인적으로는 오류가 나면 궁금해 미칠지언정 괴롭다는 생각은 들지 않아서 역시 이 일이 잘 맞는구나, 잘 하고 있구나 하는 생각이 들었다.
'Diary > Retrospective' 카테고리의 다른 글
[항해99 수료 회고] 🚢 99일간의 항해를 마무리하며 (3) | 2023.07.06 |
---|---|
[항해99 / TIL] 🚢 23-05-25 짧은 일기 (2) | 2023.05.25 |
[항해99 / TIL] 🚢 23-04-29 Styled Components로 화면 요소 만들기 (0) | 2023.04.29 |
[항해99 / TIL] 🚢 23-04-19 리액트 Redux로 투두리스트 만들기 (0) | 2023.04.19 |
[항해99 / TIL] 🚢 23-04-17 리액트 useState로 투두리스트 만들기 (2) | 2023.04.17 |