2024-05-24 리액트 숙련 과제 (1)
리액트 숙련과제 의사코드
#공부/코딩
과제 요구사항 체크
필수 구현사항 체크하기
- 지출 CRUD 구현 (작성, 조회, 수정, 삭제)
- 월별 지출 조회 기능 구현 (Home - Read)
- 월별 지출 항목 등록 구현 (Home - Create)
- 지출 상세 화면 구현 (Detail - Read)
- 상세화면에서 지출 항목 수정 구현 (Detail - Update)
- 상세화면에서 지출 항목 삭제 구현 (Detail - Delete)
필수 요구사항 체크하기
- styled-components 를 이용하여 스타일링
- 인라인 스타일링이나 일반 css 파일을 이용한 스타일링 방식 지양 (이번 과제 한정)
- 모든 태그를 styled-components 화 할 필요는 없으나 스타일링이 들어가는 경우는 styled-components 화 할 것
- styled-components에 props를 넘김으로 인한 조건부 스타일링 적용
- 월 선택 탭에 적용해 보세요
- react-router-dom 을 이용해서 페이지 전환을 합니다.
- 지출을 수정하기 위한 페이지 이동 시에 사용해주세요.
- useState, useEffect, useRef 사용
- 과제 안내 순서에 각각 어디에서 사용되면 좋을지 가이드를 드렸습니다. 해당 부분에서 위의 기능들을 각각 사용해주세요
- 지출 항목 등록 시 id는 uuid 라이브러리를 이용 (npm i uuid) or (yarn add uuid)
https://www.npmjs.com/package//uuid과제 구현하기
프로젝트 세팅하기
npm creat vite@latest
패키지 설치하기
npm install react-router-dom npm install styled-components
프로젝트 파일 비우기
- Index.html, App.jsx, App.css, Index.css
브랜치 만들기
props-drilling
이 브랜치에서는 context API나 Redux 사용 안 함.페이지 라우팅하기
- main.jsx 세팅
// main.jsx
// import 하기
import { BrowserRouter } from "react-router-dom";
// App 컴포넌트 감싸기
// route import하기
import { Routes, Route } from "react-router-dom";
// 컴포넌트 모조리 import하기
import Home from "./components/Home";
import List from "./components/List";
import Detail from "./components/Detail";
3. 메인컴포넌트 return문에서 라우트 호출
```jsx
function App() {
return (
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/List" element={<List/>}/>
<Route path="/Detail" element={<Detail/>}/>
{/* 모든 컴포넌트에 해당하지 않는 이상한 url parameter 넣었을 때 보여줄 컴포넌트(페이지) */}
<Route path="*" element={<NotFound/>}/>
</Routes>
)
}
GlobalStyle로 Reset-CSS 적용하기
- 설치하기
npm install styled-reset
- GlobalStyle.js 셋업
// public/ponts 폴더에 폰트 위치
// /src/GlobalStyle.js
import { createGlobalStyle } from 'styled-components';
import reset from 'styled-reset';
const GlobalStyle = createGlobalStyle`
${reset}
@font-face {
font-family: 'AppleSDGothicNeoB';
src: url('/fonts/AppleSDGothicNeoB.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'AppleSDGothicNeoH';
src: url('/fonts/AppleSDGothicNeoH.ttf') format('truetype');
font-weight: bold;
font-style: normal;
}
body {
font-family: 'AppleSDGothicNeoB', sans-serif;
margin: 0;
padding: 0;
box-sizing: border-box;
}
`;
export default GlobalStyle;
3. main.jsx나 App.jsx에서 전역 스타일링 적용
```jsx
// main.jsx
import GlobalStyle from './GlobalStyle';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<GlobalStyle />
<App />
</BrowserRouter>
</React.StrictMode>,
);
Layout 컴포넌트 스타일링 하기 (styled-components 사용)
- import 후
MainDiv
스타일링하기// src/components/Layout.jsx
import styled from 'styled-components';
const MainDiv = styled.divdisplay: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f8f9fa;
;
const Layout = ({ children }) => {
return
};
// children 프로퍼티의 의미는 해당 컴포넌트가 호출된 곳, 즉 Layout 컴포넌트의 시작과 끝 사이에 들어와 있는 모든 자식컴포넌트에
// 작성된 HTML, 즉 jsx에 있는 모든 태그를 전부 포함시키겠다는 의미이다.
// 이렇게 레이아웃을 구성하면 부모 컴포넌트는 건드리지 않고 자식 컴포넌트에만 영향을 줄 수 있다.
export default Layout;
2. App,jsx 메인 컴포넌트에서 Layout 컴포넌트로 감싸기
```jsx
function App() {
return (
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/detail" element={<Detail />} />
</Routes>
</Layout>
);
}
참고 GlobalStyle.js에서 전체 레이아웃을 정하는 게 아니라 별도의 컴포넌트로 하는 이유는 GlobalStyle에서는 일반적으로 ResetCSS나 폰트 정도만 전역으로 설정하고, 레이아웃은 별도의 컴포넌트에서 설정하는 것이 좋다. 이번 과제의 경우에서는 가운데 정렬 정도를 전역 스타일링으로 생각하고 있기에 글로벌 스타일에서 정의해도 괜찮겠으나, 그 외의 레이아웃 스타일링은 별도의 레이아웃용 컴포넌트에서 처리하는 것이 좋다. 그 이유는 각 컴포넌트는 별도의 스타일을 가질 수 있는데, 이를 컴포넌트로 관리하면 예기치 않은 충돌을 예방할 수 있으나, 글로벌 스타일에서 정의한 레이아웃과 각 컴포넌트의 레이아웃이 다르다면 충돌을 발생시킬 수 있기도 하고 추후 유지보수하는 측면에서도 컴포넌트에서 레이아웃을 관리하는 것이 훨씬 가독성 등에서도 유리하기 때문이다. 즉 책임을 분산시켜야 하기 때문이다.
Form 컴포넌트 만들기
Calendar 컴포넌트 만들기 (props-drilling 발생)
// src/components/Calendar.jsx
// 스타일드 컴포넌트 import
import styled from 'styled-components';
import MonthBtn from './MonthBtn';
const MonthBtnContainer = styled.div`
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-template-rows: repeat(2, 1fr);
grid-gap: 10px;
width: 1200px;
height: 300px;
background-color: pink;
border-radius: 10px;
padding: 20px;
box-sizing: border-box;
justify-items: center;
align-items: center;
margin: 0 auto;
`;
// 거의 가운데 정렬을 위한 것. 그리드 형태로 가로 2열의 형태로 버튼을 정렬함.
function Calendar() {
return (
<MonthBtnContainer>
<MonthBtn />
</MonthBtnContainer>
{/* 반복되는 버튼을 별도의 컴포넌트로 분리해서 컨테이너 컴포넌트로 감쌈. CSS를 위해서. */}
);
}
export default Calendar;
// components/MonthBtn.jsx
import styled from 'styled-components';
const StyledMonthBtn = styled.button`
background-color: white;
border-radius: 10px;
font-size: 30px;
padding: 10px;
width: 100%;
height: 100%;
box-sizing: border-box;
`;
function MonthBtn() {
// StyledMonthBtn을 12개를 만들어야 하는 것을 배열로 미리 만들어 map 메서드를 돌려서 렌더링함.
const months = [
'1월', '2월', '3월', '4월', '5월', '6월',
'7월', '8월', '9월', '10월', '11월', '12월'
];
return (
<>
{months.map((month, index)=>
<StyledMonthBtn key={index}>{month}</StyledMonthBtn>
)}
{/* jsx에서는 map 메서드로 렌더링 시 고유의 key를 부여해야 하는데,
map메서드의 두번 째 인자로 index를 설정하고 이를 설정하는 것이 가장 쉬움. */}
</>
);
}
export default MonthBtn;
버튼을 감싸고 있는 MonthBtnContainer
를 화면에서 좌우 가운데로 위치시키기 위해 전체 컴포넌트를 감싸고 있는 Layout
컴포넌트의 CSS도 수정해주어야 한다.
import styled from 'styled-components';
const MainDiv = styled.div`
display: flex;
justify-content: center;
align-items: flex-start;
height: 100vh;
width: 100%;
`;
function Layout({ children }) {
return <MainDiv>{children}</MainDiv>;
}
export default Layout;
월별 버튼을 클릭한 것만 색상이 변하도록 조건부 렌더링 하기
사용자가 어떤 버튼을 눌렀는지 직관적으로 알 수 있도록 누른 버튼만 배경 색상이 변한 상태로 고정되도록 조건부 렌더링을 구현한다. 여기에서 props-drilling
이 발생하는데, 사용자가 어떤 색상을 눌렀는지에 대한 정보를 컴포넌트가 리렌더링 되더라도 값이 유지되게 하기 위해서 변수가 아닌 useState
훅을 통해 그 정보를 담고 처리하고자 한다.
그런데 그 상태는 MonthBtn
이라는 곳에서만 필요한데, 그렇다면 해당 컴포넌트에서 상태를 정의하고 바로 사용하면 되지만 프롭스 드릴링의 불편함을 경험해보는 과제 요구사항이 있으므로, 가장 메인 컴포넌트인 App
에서 상태를 정의하고, 이를 핸들링 하는 함수를 정의한 후 MonthBtn
까지 프롭스를 내려보겠다.
먼저 조건부 렌더링 기능을 구현하는 방법에 대해서부터 작성한다.
- 먼저 App 컴포넌트에서
clickedMonth
라는 상태를 만들어 사용자가 버튼을 눌렀는지 정보를 담는다. 초기 상태는 누르지 않은 상태일 것이므로, 초기값으로-1
을 할당한다. true, false를 넣어도 되겠지만, 이것을 핸들링 하는 함수에서 전달받는 매개변수가month
라는 배열의 인덱스를 받으므로, 숫자 형태로 불리언 값을 false로 반환하는-1
을 넣도록 한다.// App.jsx
// 상태를 정의하기 위해 useState 훅 import
import { useState } from 'react';
// 클릭 상태를 저장하는 state 정의. 초기값은 불리언값으로 false에 해당하는 -1 할당
const [clickedMonthBtn, setClickedMonthBtn] = useState(-1);
// 이 핸들 함수가 달린 버튼을 사용자가 클릭하면 몇 번째 버튼을 눌렀는지 인덱스를 매개변수로 전달한다.
// 그 매개변수로 버튼을 눌렀는지 검증하는 상태의 매개변수로 전달해서 초기값은 -1(false) 상태에서
// 값을 수정한다.
const handleClick = (index) => {
setClickedMonthBtn(index);
};
여기서 말하는 사용자가 클릭한 버튼의 인덱스란, MonthBtn 컴포넌트에서 아래와 같은 배열을 만들고 그 배열을 map 메서드를 통해 버튼 12개를 렌더링 했기 때문에 버튼마다 고유한 인덱스가 존재하고, 이를 key로 지정했기 때문에 각 버튼들은 고유한 key를 갖게 되어 어떤 버튼을 눌렀는지 식별이 가능해진 것이다.
그런데 key에는 고유한 값만 오면 되기 때문에 굳이 index가 아니더라도 month와 같이 설정해도 된다. 둘 다 장단이 있는데 배열에 1~12월이 아니라 다른 값이 더 추가 되거나 삭제되어도 인덱스는 무조건 중복되지 않으니, 배열이 수정되면 재 렌더링은 될 지언정 값이 바뀌진 않는다.
그런데 month를 key로 주게 되면 혹시라도 배열에 중복되는 값이 생기면 map 반복문이 작동하지 않는다.
그리고 month로 변경하는 경우 App 컴포넌트에서 정의한 매개변수 또한 index에서 month로 수정해야 한다.
#### 버튼 색상 조건부 렌더링 하기
```jsx
const StyledMonthBtn = styled.button`
background-color: ${(props) => (props.$active ? "blue" : "white")};
border-radius: 10px;
font-size: 30px;
padding: 10px;
width: 100%;
height: 100%;
box-sizing: border-box;
`;
props
가 active일 때는 blue 컬러로, false면 white컬러로 하라는 의미이다.
return (
<>
{months.map((month, index) => (
<StyledMonthBtn
key={index}
$active={index === clickedMonthBtn}
onClick={() => handleClick(index)}
>
{month}
</StyledMonthBtn>
))}
</>
);
props-drilling 경험하기
이번 과제에서는 props-drilling을 일부러 발생시켜 불편함을 느끼고 context API나 redux로 리팩토링 하는 과정을 또 과제로 제출해야 한다.
월별 버튼을 눌러서 클릭한 월을 고정시키기 위해 props-drilling을 사용하였다. App.jsx 메인 컴포넌트에서 버튼이 눌렸는지 안 눌렸는지 정보를 저장하기 위한 상태로 만든 clickedMonthBtn
을 정의하였고, 버튼을 눌렀을 때 onClick 이벤트 핸들러에 그 상태를 변경하는 함수에 map문을 돌려 만들어진 버튼의 index를 매개변수로 전달하기 위한 핸들 함수도 정의했다.
사실 이 상태와 함수는 전체 컴포넌트에서 필요한 것도 아니라 App.jsx 메인 컴포넌트에서 정의할 이유가 없다. 그리고 버튼들을 감싸고 있는 컨테이너 컴포넌트일 뿐인 Calendar.jsx 컴포넌트에서도 이 상태와 함수가 필요 없고 오로지 MonthBtn.jsx 버튼 컴포넌트에서만 필요하다. 따라서 이 버튼 컴포넌트에서 직접 정의하고 사용까지 하면 props를 위에서부터 내일 필요가 없는데, 일부러 props-drilling을 발생시키기 위해 가장 최상위 메인 컴포넌트에서 상태를 정의했다.
컴포넌트에 상태를 props로 전달할 때는 부모->자식 간에만 가능하기 때문에 만약 다른 컴포넌트들에서도 이 상태와 함수가 필요하다면 그 컴포넌트들의 공통 부모까지 올라가서 그곳에서 정의하면 자식들에게 내려 쓸 수 있다.
하지만 만약 부모->자식->자식->자식->자식 처럼 깊이가 점점 깊어진다면 너무 복잡해지고, 프롭스 드릴링이 발생하기 때문에 유지보수 측면에서도 깨끗하진 못하다.
// App.jsx
function App() {
const [clickedMonthBtn, setClickedMonthBtn] = useState(-1);
const handleClick = (index) => {
setClickedMonthBtn(index);
};
return (
<Routes>
<Route path="/" element={<Home clickedMonthBtn={clickedMonthBtn} handleClick={handleClick} />} />
<Route path="/detail" element={<Detail />} />
</Routes>
);
}
dummy data 추가하고 가계부 리스트 렌더링하기
연습을 위해 아래와 같은 dummy data를 FakeData.js
라는 파일명으로 src
폴더 내에 위치시켰다.
const data = [
{
"id": "25600f72-56b4-41a7-a9c2-47358580e2f8",
"date": "2024-01-05",
"item": "식비",
"amount": 100000,
"description": "세광양대창"
},
{
"id": "25600f72-53b4-4187-a9c2-47358580e2f8",
"date": "2024-01-10",
"item": "도서",
"amount": 40500,
"description": "모던 자바스크립트"
},
{
"id": "24310f72-56b4-41a7-a9c2-458580ef1f8",
"date": "2024-02-02",
"item": "식비",
"amount": 50000,
"description": "회식"
},
{
"id": "25600f72-99b4-41z7-e4h6-47312365e2f8",
"date": "2024-02-02",
"item": "간식",
"amount": 500,
"description": "아이스크림"
},
{
"id": "25143e72-16e2-22a7-a9c2-47358580e2f8",
"date": "2024-02-02",
"item": "여행",
"amount": 1055000,
"description": "일본여행"
},
{
"id": "25600f72-97p2-14a7-a9c2-47363950e2t8",
"date": "2024-02-02",
"item": "미용",
"amount": 155000,
"description": "미용실"
},
{
"id": "24312f70-97q2-14a7-a9c2-47132950e2t8",
"date": "2024-02-02",
"item": "도서",
"amount": 75000,
"description": "자율주행차량 운전주행모드 자동 전환용 인식률 90% 이상의 다중 센서 기반 운전자 상태 인식 및 상황 인식 원천 기술 개발"
}
];
export default data;
List 컴포넌트에서 이 파일을 import한다.
// components/List.jsx
import data from '../FakeData.js';
import하여 받아온 data 배열에 map 메서드를 사용하여 화면을 그린다.
// components/List.jsx
const List = () => {
return (
<ListArea>
{data.map(item => (
<ListItem key={item.id}>
<Date>{item.date}</Date>
<Category>{item.item} - {item.description}</Category>
<Amount>{item.amount.toLocaleString()} 원</Amount>
</ListItem>
))}
</ListArea>
);
}
그런데 위 방법 말고, List 컴포넌트에서 data를 import하지 않고 부모 컴포넌트에서 data를 import 한 후 props를 내려서 사용하는 방법이 더 유연하겠으나, 나의 프로젝트 폴더 구조를 보면 App.jsx가 메인이 아니라 App.jsx는 라우팅만 하고 있고 components 폴더 안에 있는 Home.jsx가 메인 컴포넌트로 사용되고 있다.
Home 컴포넌트와 List 컴포넌트는 부모자식 관계가 아니라 같은 자식 관계이므로 props를 내릴 수 없다. 뭔가 프로젝트 세팅을 잘못한 것 같다는 고민이 드는데 이는 완성 후 리팩토링 과정에서 다시 검토해볼 예정이다. 따라서 지금은 props를 내릴 수 없으므로 data를 List 컴포넌트에 직접 import 하는 방식으로 사용한다. 따라서 매개 변수에 props를 내릴 필요가 없다.
그리고 과제 요구사항 중 한 줄을 넘치는 텍스트에 대해서는 ...
처리 하게 되어있으므로 몇 가지 CSS를 추가한다.
// components/List.jsx
const Category = styled.span`
flex: 2;
white-space: nowrap; // 텍스트를 한줄로 표현
overflow: hidden; // 넘치는 텍스트를 숨김
text-overflow: ellipsis; // 숨겨진 텍스트를 ... 으로 표현
`;
버튼 클릭한 월만 필터링해서 렌더링 하기
const List = ({ month }) => {
// 원본 데이터 배열에서 yyyy, mm, dd 프로퍼티를 추가한 새로운 배열 생성
const processedData = data.map(item => {
const [yyyy, mm, dd] = item.date.split('-');
return {
...item,
yyyy: parseInt(yyyy),
mm: parseInt(mm),
dd: parseInt(dd)
};
});
const filteredData = processedData.filter(item => item.mm === (month + 1)); // month는 0부터 시작하므로 +1
return (
<ListArea>
{(month === -1 ? data : filteredData).map(item => (
<ListItem key={item.id}>
<Date>{item.date}</Date>
<Category>{item.item} - {item.description}</Category>
<Amount>{item.amount.toLocaleString()} 원</Amount>
</ListItem>
))}
</ListArea>
내가 선택한 방법은 원본 데이터가 date: “2024-01-01”
의 형태로 하나의 스트링으로 묶여있기 때문에 이것을 split
메서드로 구분자 -
를 기준으로 해체하는데, 각각 yyyy, mm, dd라는 새로운 프로퍼티를 만들어 정수로 변환하여 새로운 값을 할당하여 월만 골라내기 위한 processedData
라는 새로운 배열을 만들어 낸다.
불변성을 유지해야 하기 때문에 스프레드 연산자로 각 인덱스(객체)를 풀어서 뒤에다 새로 추가할 프로퍼티만 넣었다.
그리고 filteredData
배열을 새롭게 만들었는데, 이것은 아래와 같다.
const filteredData = processedData.filter(item => item.mm === (month + 1));
mm, yy, dd가 추가된 배열인 processedData를 한 번 더 필터를 돌리는데, 그 중 mm, 즉 월에 해당하는 것이 현재 month
배열(1월, 2월 …)과 일치하는 것만 필터링 해서 새롭게 filteredData
로 반환한 것이다. month + 1
을 한 이유는, month 배열을 보면 1월, 2월 … 이렇게 저장되어 있는데 실제로 index는 0부터 시작하기 때문에 month + 1을 해야 일치하는 것이다.
'Programing > TIL' 카테고리의 다른 글
2024-05-28 스탠다드 과제 - props-drilling을 context API 활용 리팩토링 하기 (0) | 2024.05.28 |
---|---|
2024-05-27 리액트 숙련과제 (2) (0) | 2024.05.28 |
2024-05-23 React의 상태관리 (0) | 2024.05.23 |
2024-05-22 [React] state를 이용하여 화면에 렌더링 되는 배열 필터링하기 (0) | 2024.05.21 |
2024-05-21 [React] state로 렌더링 되는 데이터 필터링하기 (0) | 2024.05.21 |
댓글