내가 리액트를 해~,,, 결국은 잡게된 프론트엔드. 내 담당은 아니지만, 다들 어려워하고 힘들어하고 있다보니 팀원으로서 같이 해결에 나섰다.
프로젝트를 통해서 처음 접해보는 리액트... 그러다보니 수정에 수정을 거치고, gpt에게 (잘못된) 조언을 들으며 프로젝트 구조가 너무나도 복잡해져 버린 것이다. 뭐 하나를 수정하고 싶어도 이상한 코드 양식에 복잡한 구조 탓에 레이아웃이 잘 안 생기는 것... 정도가 문제였음. 캘린더 하나를 좀 띄우고 싶을 뿐인데...!
gaenchwis/
├── client/
│ └── src/
│ └── component/
│ └── calendar1/
│ ├── index.js
│ ├── holidays.js
│ ├── Firebase.js
│ ├── EditSchedule.js
│ ├── Day.js
│ ├── Datepicker.js
│ ├── Calendar.js
│ ├── AddSchedule.js
│ ├── styles/
│ │ └── CalendarStyles.js
│ └── redux/
│ ├── configStore.js
│ └── modules/
│ └── schedule.js
기본적으로는 이런 식으로 구성이 되어 있었다. 하지만 어떤 건 스타일 관련 코드가 Styles 폴더의 CalendarStyles.js에 있고, 어떤 건 한데 합쳐져 있고... 너무 난리통이었음. 하지만 우리는 아직 너무 초보임 ㅠ... 그래서 코드 보는 것도 어려웠다. 그래서 우선은 초보자들도 보기 쉽게 폴더 구조를 정리하기로 맘을 먹었다.
gaenchwis/
├── client/
│ └── src/
│ └── component/
│ └── calendar1/
│ ├── index.js # 메인 진입점
│ ├── Calendar.js # 메인 캘린더 컴포넌트
│ ├── Calendar.css # 캘린더 스타일
│ ├── Day.js # 일자 컴포넌트
│ ├── Day.css # 일자 스타일
│ ├── Datepicker.js # 날짜 선택 컴포넌트
│ ├── Datepicker.css # 날짜 선택 스타일
│ ├── AddSchedule.js # 일정 추가 컴포넌트
│ ├── AddSchedule.css # 일정 추가 스타일
│ ├── EditSchedule.js # 일정 수정 컴포넌트
│ ├── EditSchedule.css # 일정 수정 스타일
│ ├── Firebase.js # Firebase 설정
│ ├── holidays.js # 공휴일 데이터
│ └── redux/ # Redux 관련 파일들
│ ├── configStore.js # 스토어 설정
│ └── modules/
│ └── schedule.js # 스케줄 리듀서
주요 수정사항은 스타일링 방식을 변경한 것이다.
Body
, CalendarWrapper
등)을 일반 div
+ className으로 변경일단 기존 코드가 내가 작성한 것이 아니고 인터넷에 있던 것이기 때문에 리액트에 대한 지식이 거의 전무하다 싶은 내가 건들기가 너무 어려웠다. 구조를 파악하기도 어렵고, 어디를 고쳐야할지도 쉽사리 감이 잡히지 않았다. 그래서 최대한 초심자도 이해하기 쉽게 구조를 바꾸는 것이 필요했다.
styled-component
제거Calendar.js
코드import React, { useState, useEffect } from 'react';
import moment from 'moment';
import 'moment/locale/ko';
import {
MdChevronLeft,
MdChevronRight,
MdDehaze,
MdCheck,
MdDoneAll,
MdEdit
} from 'react-icons/md';
import { useDispatch, useSelector } from 'react-redux';
import {
readSchedule,
setIsFilter,
openEditPopup
} from './redux/modules/schedule';
import Day from './Day';
import EditSchedule from './EditSchedule';
import AddSchedule from './AddSchedule';
import {
Body,
ButtonWrapper,
CalendarWrapper,
Header,
YearDisplay,
HeaderContent,
DateContainer,
Weekend,
Dow
} from './styles/CalendarStyles';
const Calendar = () => {
const { thisMonth, isOpenEditPopup, isFilter } = useSelector(
(state) => state.schedule
);
const [isAddPopupOpen, setIsAddPopupOpen] = useState(false);
const [current, setCurrent] = useState(moment());
const dispatch = useDispatch();
useEffect(() => {
const startDay = current.clone().startOf('month').format('YYYYMMDD');
const endDay = current.clone().endOf('month').format('YYYYMMDD');
dispatch(readSchedule({ startDay, endDay }));
}, [current, dispatch, isOpenEditPopup]);
const movePrevMonth = () => {
setCurrent(current.clone().subtract(1, 'month'));
};
const moveNextMonth = () => {
setCurrent(current.clone().add(1, 'month'));
};
const generate = () => {
const startWeek = current.clone().startOf('month').week();
const endWeek =
current.clone().endOf('month').week() === 1
? 53
: current.clone().endOf('month').week();
let calendar = [];
for (let w = startWeek; w <= endWeek; w++) {
calendar.push(
<Weekend key={w}>
{Array(7)
.fill(0)
.map((n, idx) => {
const noFormatDate = current
.clone()
.startOf('year')
.week(w)
.startOf('week')
.add(idx, 'day');
const day = noFormatDate.format('D');
const fullDate = noFormatDate.format('l').replaceAll('.', '');
const isToday =
noFormatDate.format('YYYYMMDD') === moment().format('YYYYMMDD')
? 'today'
: '';
const isGrayed =
noFormatDate.format('MM') === current.format('MM')
? ''
: 'grayed';
const currentSch = thisMonth.filter((s) => {
return s.date === fullDate;
});
const dateInfo = { day, fullDate, dow: idx, currentSch };
return (
<Day
key={n + idx}
dateInfo={dateInfo}
className={`${isGrayed} ${isToday}`}
/>
);
})}
</Weekend>
);
}
return calendar;
};
const onFilter = (isFilter) => {
dispatch(setIsFilter(isFilter));
};
return (
<Body>
<CalendarWrapper>
{isOpenEditPopup && <EditSchedule />}
{isAddPopupOpen && (
<AddSchedule onClose={() => setIsAddPopupOpen(false)} />
)}
<Header>
<YearDisplay>{current.format('YYYY')}</YearDisplay>
<HeaderContent>
<MdChevronLeft className="dir" onClick={movePrevMonth} />
<span>{current.format('MMMM')}</span>
<MdChevronRight className="dir" onClick={moveNextMonth} />
</HeaderContent>
</Header>
<DateContainer>
<Weekend className="row">
<Dow color="#ff4b4b">
<span>S</span>
</Dow>
<Dow>
<span>M</span>
</Dow>
<Dow>
<span>T</span>
</Dow>
<Dow>
<span>W</span>
</Dow>
<Dow>
<span>T</span>
</Dow>
<Dow>
<span>F</span>
</Dow>
<Dow color="#4b87ff">
<span>S</span>
</Dow>
</Weekend>
{generate()}
</DateContainer>
</CalendarWrapper>
<ButtonWrapper onClick={() => dispatch(openEditPopup(false))}>
{isFilter ? (
<MdCheck
onClick={(e) => {
e.stopPropagation();
onFilter(false);
}}
className={'filterBtn subBtn'}
/>
) : (
<MdDoneAll
onClick={(e) => {
e.stopPropagation();
onFilter(true);
}}
className={'filterBtn subBtn'}
/>
)}
<MdEdit
onClick={(e) => {
e.stopPropagation();
setIsAddPopupOpen(true);
}}
className={'writeBtn subBtn'}
/>
<MdDehaze className={'menuBtn'} />
</ButtonWrapper>
</Body>
);
};
export default Calendar;
Redux로 상태 관리를 하는 것은 아직도 잘 모른다... 그건 나중에 공부할래 ㅠㅠ 일단 달력 생성 로직이 많이 복잡해보였고, 지금은 useState도 어려워서 뭔 소린지 모르겠는데 styled-components요...? 그냥 날 죽여줘 ㅠ
import React, { useState, useEffect } from 'react';
import moment from 'moment';
import 'moment/locale/ko';
import {
MdChevronLeft,
MdChevronRight,
MdDehaze,
MdCheck,
MdDoneAll,
MdEdit,
} from 'react-icons/md';
import { useDispatch, useSelector } from 'react-redux';
import {
readSchedule,
setIsFilter,
openEditPopup,
} from './redux/modules/schedule';
import Day from './Day';
import EditSchedule from './EditSchedule';
import AddSchedule from './AddSchedule';
import './Calendar.css';
const Calendar = () => {
const { thisMonth, isOpenEditPopup, isFilter } = useSelector(
(state) => state.schedule,
);
const [isAddPopupOpen, setIsAddPopupOpen] = useState(false);
const [current, setCurrent] = useState(moment());
const dispatch = useDispatch();
// 달력 데이터 가져오기
useEffect(() => {
const startDay = current.clone().startOf('month').format('YYYYMMDD');
const endDay = current.clone().endOf('month').format('YYYYMMDD');
dispatch(readSchedule({ startDay, endDay }));
}, [current, dispatch, isOpenEditPopup]);
// 이전 달로 이동
const movePrevMonth = () => {
setCurrent(current.clone().subtract(1, 'month'));
};
// 다음 달로 이동
const moveNextMonth = () => {
setCurrent(current.clone().add(1, 'month'));
};
// 달력 생성
const generate = () => {
const startWeek = current.clone().startOf('month').week();
const endWeek =
current.clone().endOf('month').week() === 1
? 53
: current.clone().endOf('month').week();
let calendar = [];
for (let week = startWeek; week <= endWeek; week++) {
let days = Array(7)
.fill(0)
.map((n, i) => {
const current_day = current
.clone()
.week(week)
.startOf('week')
.add(i, 'day');
const isGrayed = current_day.format('MM') !== current.format('MM');
const isToday =
current_day.format('YYYYMMDD') === moment().format('YYYYMMDD');
const fullDate = current_day.format('YYYYMMDD');
const currentSch = thisMonth.filter(
(schedule) => schedule.date === fullDate,
);
return (
<Day
key={fullDate}
dateInfo={{
day: current_day.format('D'),
currentSch,
}}
className={`calendar-day ${isGrayed ? 'grayed' : ''} ${
isToday ? 'today' : ''
} ${i === 0 ? 'sunday' : ''} ${i === 6 ? 'saturday' : ''}`}
/>
);
});
calendar.push(
<div key={week} className="calendar-week">
{days}
</div>,
);
}
return calendar;
};
// 필터 토글
const onFilter = (filterValue) => {
dispatch(setIsFilter(filterValue));
};
return (
<div className="calendar-body">
<div className="calendar-wrapper">
{isOpenEditPopup && <EditSchedule />}
{isAddPopupOpen && (
<AddSchedule onClose={() => setIsAddPopupOpen(false)} />
)}
<div className="calendar-header">
<div className="year-display">{current.format('YYYY')}</div>
<div className="header-content">
<MdChevronLeft className="dir-button" onClick={movePrevMonth} />
<span>{current.format('MMMM')}</span>
<MdChevronRight className="dir-button" onClick={moveNextMonth} />
</div>
</div>
<div className="calendar-container">
<div className="calendar-week">
<div className="dow sunday">S</div>
<div className="dow">M</div>
<div className="dow">T</div>
<div className="dow">W</div>
<div className="dow">T</div>
<div className="dow">F</div>
<div className="dow saturday">S</div>
</div>
<div className="calendar-dates">{generate()}</div>
</div>
</div>
<div
className="button-wrapper"
onClick={() => dispatch(openEditPopup(false))}
>
{isFilter ? (
<MdCheck
onClick={(e) => {
e.stopPropagation();
onFilter(false);
}}
className="filterBtn subBtn"
/>
) : (
<MdDoneAll
onClick={(e) => {
e.stopPropagation();
onFilter(true);
}}
className="filterBtn subBtn"
/>
)}
<MdEdit
onClick={(e) => {
e.stopPropagation();
setIsAddPopupOpen(true);
}}
className="writeBtn subBtn"
/>
<MdDehaze className="menuBtn" />
</div>
</div>
);
};
export default Calendar;
여전히 redux는 어렵고... 달력 생성 로직 자체가 복잡하지만 그래도 css 파일을 사용해서 그나마 진입장벽을 조금 낮추기 위해 노력했다. 주석도 많이 달아놨다.
어찌어찌 달력을 띄우는 덴 성공했으나...? calender-header
요소와 calendar-container
요소의 영역이 서로 같아야 하는데 맞지 않아서 캘린더 본문이 길이를 초과하고 잘리는 상황 발생ㅠ 하...
요약하자면
CSS가 먹히지 않는다!
이런 상황이었음.
/* 1. 달력 전체 컨테이너 */
.calendar-body {
width: 50%; /* 전체 화면의 절반 크기 */
position: relative; /* 내부 요소들의 기준점 */
}
/* 2. 달력 래퍼 */
.calendar-wrapper {
position: relative; /* 팝업 위치의 기준점 */
}
/* 3. 달력 헤더 영역 (년도, 월 표시) */
.calendar-header {
height: 7vh; /* 뷰포트 높이의 7% */
display: flex;
flex-direction: column; /* 년도와 월을 세로로 배치 */
align-items: flex-start;
padding: 10px;
font-size: 1.5em;
}
/* 3-1. 년도 표시 */
.year-display {
color: #666;
font-size: 0.8em;
margin-bottom: 5px;
margin-left: 10px;
}
/* 3-2. 월 표시 및 이동 버튼 컨테이너 */
.header-content {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
/* 3-3. 월 이름 간격 */
.header-content > span {
margin: 0 100px; /* 좌우 화살표와의 간격 */
}
/* 3-4. 이전/다음 월 이동 버튼 */
.dir-button {
color: #cccccc;
transition: all 0.3s ease;
width: 35px;
height: 35px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
cursor: pointer;
}
/* 4. 달력 본문 영역 */
.calendar-container {
display: flex;
flex-direction: column;
}
/* 4-1. 요일 및 날짜 행 */
.calendar-week {
display: flex;
width: 100%;
}
/* 4-2. 날짜 그리드 컨테이너 */
.calendar-dates {
display: flex;
flex-direction: column;
width: 100%;
}
/* 4-3. 요일 헤더 스타일 */
.dow {
flex: 1; /* 균등 분할 */
border-bottom: 1px solid gray;
height: 35px;
text-align: center;
padding: 5px;
}
/* 4-4. 일요일, 토요일 색상 */
.sunday {
color: #ff4b4b;
}
.saturday {
color: #4b87ff;
}
/* 5. 플로팅 버튼 영역 */
.button-wrapper {
position: fixed;
right: 350px;
bottom: 50px;
text-align: center;
padding-bottom: 3px;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
height: 150px;
z-index: 1000;
}
/* 5-1. 서브 버튼 호버 효과 */
.button-wrapper:hover .subBtn {
opacity: 1;
visibility: visible;
top: 0;
}
/* 5-2. 버튼 공통 스타일 */
.button-wrapper svg {
cursor: pointer;
border-radius: 50%;
color: white;
width: 25px;
height: 25px;
padding: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
/* 5-3. 각 버튼별 스타일 */
.filterBtn {
/* 필터 버튼 */
background-color: pink;
z-index: 1;
transition: all 0.4s ease;
}
.writeBtn {
/* 작성 버튼 */
background-color: skyblue;
z-index: 2;
transition: all 0.5s ease;
}
.menuBtn {
/* 메뉴 버튼 */
background-color: #ffdb0d;
z-index: 3;
}
/* 5-4. 숨겨진 서브 버튼 스타일 */
.subBtn {
opacity: 0;
visibility: hidden;
top: 60px;
position: relative;
}
정말 하기 싫었지만... 이제는 하나하나 뜯어보며 코드를 고쳐가며 적용해보는 수밖에. 그래서 먼저 한 일은 GPT에게 코드 주석을 달고 정리를 해달라는 거였다. 여전히 어지러운 코드들이었기 때문에 ㅎㅎ
하지만 정말 아무리 수정에 수정을 거쳐도 캘린더는 꿈쩍도 하지 않는 것이었다. 이건 더이상 내 잘못일 수가 없다.
<div class="calendar-container"> <!-- 첫 번째 calendar-container -->
<div class="sc-blHHSb iMSBqf">
<div class="sc-egkSDF bIQRnc">CALENDAR</div>
<div class="sc-gtLWhw cNMOSx">
<div class="calendar-body">
<div class="calendar-wrapper">
<div class="calendar-header">...</div>
<div class="calendar-container"> <!-- 두 번째 calendar-container -->
<div class="calendar-week">...</div>
<div class="calendar-dates">...</div>
</div>
</div>
</div>
</div>
</div>
</div>
문제는 HTML 구조를 확인해보니 답이 나왔다.
styled-components로 추가된 것으로 보이는 클래스들(sc-blHHSb, sc-egkSDF 등)이 있는데, 이것들이 레이아웃에 영향을 미치고 있는 것이었다.
아니 나는 이 친구를 지우고 싶은 거라고...! 이미 삭제도 했는데?! sc-
로 시작하는 클래스들은 styled-components가 생성하는 고유한 클래스 네이밍 패턴이라고 한다.
당연히 기존의 styled-components 관련 폴더는 삭제했고, import가 되고 있지 않다는 것도 체크했다.
npm uninstall styled-components
rm -rf node_modules
rm package-lock.json # 있다면
npm install
npm start
위 명령어를 차례로 입력하여 styled-components를 완전히 지워버리고 프로젝트를 다시 시작했다.
그 결과 ...
ERROR in ./src/calendar.js 8:0-39
Module not found: Error: Can't resolve 'styled-components' in 'C:\workbench\calendar\gaenchwis\client\src'
ERROR in ./src/firstpage.js 6:0-39
Module not found: Error: Can't resolve 'styled-components' in 'C:\workbench\calendar\gaenchwis\client\src'
webpack compiled with 2 errors and 1 warning
에러 발생... 내 캘린더에서는 지웠으나, 다른 팀원의 페이지에서 사용하고 있었던 것 ㅠ 그래서 styled-components를 완전 지울 수 없다는 것을 깨닫고... 다른 방법을 찾아야 했다.
styled-components
의 클래스들을 덮어쓰자!/* styled-components 클래스들에 대한 스타일 재정의 */
[class^="sc-"] {
width: 100% !important;
display: flex !important;
flex-direction: column !important;
}
/* calendar-container 스타일 강화 */
.calendar-container {
width: 100% !important;
max-width: 100% !important;
display: flex !important;
flex-direction: column !important;
overflow: hidden !important;
box-sizing: border-box !important;
}
/* calendar-wrapper 스타일 강화 */
.calendar-wrapper {
width: 100% !important;
position: relative !important;
display: flex !important;
flex-direction: column !important;
box-sizing: border-box !important;
}
/* calendar-body 스타일 강화 */
.calendar-body {
width: 50% !important;
position: relative !important;
display: flex !important;
flex-direction: column !important;
margin: 0 auto !important;
box-sizing: border-box !important;
}
[class^="sc-"]
선택자는 sc-
로 시작하는 모든 클래스에 스타일을 적용하며, !important
규칙을 사용해 styled-components의 스타일을 덮어쓰게 했다.
나의 문제 상황은 이러했다.
styled-components
가 전역 스타일을 생성하면서 자동으로 생성된 클래스명(sc- 접두어)이 남아있어서? 자꾸 좀비처럼 다시 기어나옴.import
문을 제거해도 이미 빌드된 컴포넌트에styled-components
의 스타일이 적용되어 있었음- 이 스타일들이 일반 CSS보다 우선순위가 높아 레이아웃에 영향을 미침
그렇게 해서... 어제 한나절을 붙잡고 있던 캘린더 띄우기 기어이 성공 ㅠㅠㅠ
결국 !important
규칙을 사용해서 기존 styled-components의 스타일을 강제로 덮어쓰게 했다. 후... 진짜 힘들었다. 다른 styled-components 스타일도 일반 css로 마이그레이션을 해야 할까... 그건 또 얼마나 대공사일까...! 일단은 이렇게 냅둬본다. 빨리 완성해야해...!
일단 오늘은 내가 맡은 파트는 아니었지만 문제 해결을 위해서 뛰어들 수밖에 없는 상황이었다. 하지만 나도 리액트를 잘 모른다는 점이 문제였다. 다행히 강사님께 한 시간 정도 리액트 구조에 대한 강의를 듣고, 어느정도 코드를 보는 감을 익힌 후, ai나 내 뇌의 도움을 받아서 구조를 변경하고, 이후에 코드 수정이나 변경이 조금 쉬울 수 있도록 대대적인 수정을 했다. 리액트 진짜 어렵당... 그래도 이게 하루만에 되어서 좀 다행이고 뿌듯했다. 이런저런 시도들을 하면서 하나씩 고쳐질 때 정말 행복했다... (빨리 끝내고 싶었음) 얼른 크롤러와 db 관련된 부분 마무리 짓고, 내가 맡은 파트에 대해서도 글을 쓰고 싶다. 그건 또 언제정리하냐 ㅋㅋㅋ 깔깔... 아니 정리가 문제가 아니라 기능 개발을 다 마쳐야 하는데 ㅠㅠㅠ 파이팅...
본 포스팅은 글로벌소프트웨어캠퍼스와 교보DTS가 함께 진행하는 챌린지입니다.