일정을 관리해주는 플랫폼이다.
Trello와 거의 유사한 기능과 UI를 가지고 있다.
로그인을 통해 계정을 생성 후, 일정을 관리하는 대시보드를 만들 수 있다.
프로젝트 별로 대시보드를 만들고, 그 안에 일정 카드들을 생성하여
이번 프로젝트도 마찬가지로 디자인과 백엔드 API는 코드잇 측에서 준비해주었고,
우리 팀은 프론트엔드 개발에만 온전히 집중할 수 있는 환경이었다.
프로젝트 Github 링크
위 링크로 접속하면 프로젝트 소개 및 소스코드를 확인할 수 있다.
이번 프로젝트에서는 Github organization을 생성하여 팀원들끼리의 그룹을 만들었다.
또한 Git Flow 전략은 기능별로 브랜치를 만들어 활용하였다.
main, dev로 나누어 main은 배포용, dev는 개발용으로 두었으며,
기능을 개발할 때는 각자 기능_topic 이라는 브랜치를 파서 작업을 진행하였다.
이 때 멘토님께서 이야기 하셨던 브랜치 전략을 반영해보려고 노력하였다.
기능-UI, 기능-로직 과 같이 기능 브랜치에서 한 번 더 나누어 관리를 하는 것이다.
이렇게 하면 충돌 가능성을 확실히 낮출 수 있고,
자기가 어떤 일을 하고 있는지도 직관적으로 파악할 수 있어
현업에서도 이쁨(?)을 받을 수 있다는 멘토님의 말씀이셨다. :)
사용한 기술의 경우 기초 프로젝트 이후부터 학습하였던 TypeScript와 Next.js를 도입하였다.
실무에서도 많이 쓰이고 있는 기술인 만큼, 중급 프로젝트를 통해 확실히 익혀보자는 의견이었다.
스타일의 경우 동일하게 Styled Component를 사용하였다.
이번 프로젝트에서는 ESLint(Airbnb)와 Prettier를 활용하여 기본적인 코드 스타일을 맞추었다.
다만 ESLint의 경우 불필요한 몇가지 옵션은 꺼두고 작업하였다. (수많은 에러 방지..)
이 세팅은 일부 팀원 분들이 맡아서 세팅을 해주셨다.
네이밍, commit, PR과 같은 자잘한 컨벤션들은 사전에 회의를 통해 정해놓고, 프로젝트를 시작하였다.
기본적으로 chip, card와 같은 컴포넌트에 대한 폴더를 만들고,
그 안에 해당 컴포넌트에 관한 메인 컴포넌트 파일, 스타일 파일을 넣어서 작업을 진행하였다.
스타일 파일 안에는 styled component들을 만들어서 export 해두었고, 외부에서 사용할 수 있도록 하였다.
이런 식으로 하니 컴포넌트 파일이 훨씬 깔끔해보인다.
- components
- cards
- Card.tsx
- Card.style.ts
- chips
- Chip.tsx
- ColorPalette.tsx
- Chip.style.ts
- Chip.type.ts
Next.js의 경우 pages 폴더 아래 컴포넌트를 생성하면 바로 라우팅이 가능하므로 pages 아래 페이지 컴포넌트를 배치하였다.
Chip 컴포넌트는 모서리가 각진버전, 둥근버전 두가지가 있다.
멘토링 시간에 Button 컴포넌트를 재사용성 있게 만드는 요령(?)을 들은 후,
이번 Chip 컴포넌트에도 적용해보기로 하였다.
가장 퓨어한 Chip 컴포넌트를 만든 후, 이것을 상속 받아 border-radius만 바꾸어서
RoundChip, SquareChip 컴포넌트를 만들어주었다.
처음에는 각각의 파일에 따로 만들어주었는데, 팀원 분이 훨씬 간결하게 코드를 작성하신 것을 보고
살짝쿵 참고하여 아래와 같이 구현해주었다.
import React from 'react';
import * as S from './Chip.style';
import { TChipProps } from '@components/chips/Chip.type';
function Chip({ children, size, color }: TChipProps) {
return (
<S.BasicChip $size={size} $color={color}>
{children}
</S.BasicChip>
);
}
function RoundChip({ children, size, color }: TChipProps) {
return (
<S.RoundChip $size={size} $color={color}>
{children}
</S.RoundChip>
);
}
function SquareChip({ children, size, color }: TChipProps) {
return (
<S.SquareChip $size={size} $color={color}>
{children}
</S.SquareChip>
);
}
Chip.Round = RoundChip;
Chip.Square = SquareChip;
export default Chip;
이렇게 구현해주면 파일의 갯수도 적어지고, 외부에서 컴포넌트를 호출할 때 훨씬 직관적으로 코드를 작성할 수 있었다.
<Chip.Square size={'small'} color={'orange'}> 프로젝트 </Chip.Square>
이 때 사용되는 props들이 모두 children, size, color 정도로 동일해서
따로 type 파일에 타입들을 지정해주었다.
또한 size와 color에 넣을 값들도 각각 도메인을 지정하여 새로운 타입으로 지정하였다.
export type TColorKey = 'green' | 'purple' | 'pink' | 'orange' | 'blue' | 'gray';
export type TChipSize = 'large' | 'small' | 'tiny';
export type TChipProps = {
children: ReactNode;
size: TChipSize;
color: TColorKey;
};
타입을 활용해주니 이후에 props를 넣을 때 자동완성이 되어 편리했다.
이 때 props로 받아온 size와 color는 아래와 같이 Styled Component에 다이나믹 스타일링으로 활용해주었는데,
const chipColorList = {
green: { background: '#E7F7DB', color: '#86D549' },
purple: { background: 'var(--violetlight)', color: 'var(--violet)' },
...,
};
...
export const BasicChip = styled.span<ChipStyleProps>`
...
background-color: ${({ $color }) => chipColorList[$color]['background']};
color: ${({ $color }) => chipColorList[$color]['color']};
`;
각 색상 별로 배경, 글자 색을 객체 배열 형태로 지정해준 후 color 값에 맞게 갖다 쓰기만 하면 되도록 구현하였다.
chip 안에 있는 text가 수직 정렬 되지 않아 어려움을 겪었는데,
display 속성을 span과 flex의 속성을 모두 가질 수 있는 inline-flex로 준 후
align-items:center
로 설정해주니 수직 정렬이 가능했다.
그냥 flex로만 줄 경우에는 Round Chip을 사용할 때 화면에 chip이 꽉차는 문제가 발생했다.
랜덤한 색깔의 칩(태그) 생성 기능
요구사항 중 하나는, 할 일 카드를 생성할 때 사용되는 칩의 컬러를 랜덤으로 생성되게 하라는 것이었다.
칩의 컬러 자체를 랜덤으로 생성하는 것은 어렵지 않았지만,
문제는 이렇게 랜덤하게 만들어진 칩의 컬러를 기억하는 것이었다.
주어진 api에는 따로 칩의 색상을 저장하는 부분은 없었기 때문에,
새로운 방안을 떠올려야했다.
내가 떠올린 아이디어는 칩의이름:칩의색상
의 형태로 칩의 정보를 저장하는 것이었다.
칩의 정보는 string 형식으로 들어가기 때문에, 위의 형태로 정보를 저장하는 것이 가능했다.
...
const randColor = makeRandomChipColor();
const tagArr = [...tags, `${inputValue}:${randColor}`];
setTags(tagArr);
...
랜덤으로 칩의 색상을 만들어 칩의 정보를 저장하면,
나중에 해당 칩을 불러올 때 :
을 기준으로 split 하여 칩의 이름과 색상을 따로 사용할 수 있었다.
{tags.map((tagString, index) => {
const [text, color] = tagString.split(':');
return (
<Chip.Square key={`${index} ${text}`} size={'small'} color={color}>
{text}
</Chip.Square>
);
})}
이 후 카드를 생성할 때 chip의 색상을 결정할 수 있는 말그대로 팔레트이다.
간단히 style 파일에 ColorTile이라는 styled component를 만들어 사용하였다.
마찬가지로 색상별, 크기별 객체 배열을 만들어 활용하였다.
const colorPaletteList = {
green: 'var(--green)',
purple: 'var(--purple)',
pink: 'var(--pink)',
orange: 'var(--orange)',
blue: 'var(--blue)',
gray: 'none',
};
const colorTileSizeList = {
large: 30,
small: 28,
tiny: 6,
};
export const ColorTile = styled.div<ChipStyleProps>`
display: inline-block;
position: relative;
width: ${({ $size }) => colorTileSizeList[$size]}px;
height: ${({ $size }) => colorTileSizeList[$size]}px;
border-radius: 50%;
background-color: ${({ $color }) => colorPaletteList[$color]};
`;
size의 경우 팔레트 크기별 large, small을 사용할 수 있고,
tiny는 아래 그림과 같이 상태 표시와 같이 조그만 동그라미 형태로 사용할 때 활용할 수 있도록 구현하였다.
이 때 display를 inline-block으로 준 이유는,
span 처럼 자기 자신이 있는 영역만 차지하되, 화면에 보일 수 있도록 div의 속성도 가져야 하기 때문이다.
ColorPalette 컴포넌트에서는 색상 키를 배열로 만들고, map을 돌며 ColorTile을 배치해주었다.
const colorList: TColorKey[] = ['green', 'purple', 'pink', 'orange', 'blue'];
{colorList.map((color) => {
return ( <S.ColorTile $color={color} $size={size} /> );
})}
간단히 useState를 통해 구현할 수 있었다.
ColorPalette 컴포넌트에서 map을 돌릴 때 idx 값을 추가하여,
각 타일에 onClick props로 클릭 시 해당 색상의 idx를 selectedColor로 set해주고,
해당 색상이 현재 선택된 색상이면 체크 아이콘이 나타날 수 있도록 구현하였다.
const [selectedColor, setSelectedColor] = useState(0);
{colorList.map((color, idx) => {
return ( <>
<S.ColorTile $color={color} $size={size} onClick={() => setSelectedColor(idx)}/>
{selectedColor === idx ? ( <S.ColorCheckIcon>✓</S.ColorCheckIcon> ) : null}
</>);
})}
Card UI의 경우 크게 어려운 점은 없었다.
까다로웠던 것은 반응형 디자인을 적용하는 부분이었다.
이 화면이 태블릿용 화면이고,
이 화면이 PC와 모바일용 화면이었다.
태블릿 사이즈가 중간에 끼어있는(?) 만큼 이전에 하던 작업보다는 까다로웠던 것 같다.
(PC에 미적용 - 태블릿에 적용 - 모바일에서 다시 미적용...)
중간중간 container div를 끼워넣고, 각각에 flex, block, margin.. 등등 속성을 달리하며
미디어 쿼리를 꾸역꾸역 넣어서 완성하였다. 더 깔끔하게 짤 수 없을까..? 하였지만 한계였다..
Card 컴포넌트 전체를 감싸는 cardContainer 의 경우 max-width, min-width, width 세가지를 적절히 활용하여 기기별로 이미지가 있는 카드와 없는 카드의 너비를 동일하게 적용해주었다.
export const CardContainer = styled.div`
display: inline-flex;
flex-direction: column;
width: 314px;
...
@media ${device.tablet} {
flex-direction: row;
width: 544px;
}
@media ${device.mobile} {
flex-direction: column;
padding: 10px;
max-width: 284px;
}
`;
이 styled component만 보더라도 PC, 태블릿, 모바일 기기별 반응형 디자인을
어떤 식으로 구현했길래 까다로웠다 한 것인지 이해가 될 것이다.. (flex-direction 등등)
일정 작업자의 프로필 (김, 배, 모) 아이콘은 카드 내 같은 곳에 위치 시키기 위해
태블릿 사이즈에서는 positiona absolute로 조정해주었다.
Modal 컴포넌트의 경우 Modal portal을 사용하여 구현하였다.
내가 맡아서 개발한 페이지는 대시보드를 수정하는 페이지이다.
대시보드의 이름과 색상을 변경할 수 있고,
해당 대시보드를 함께 공유할 수 있는 멤버들을 초대하고 삭제할 수 있는 페이지이다.
기본적으로 화면을 그리는 것은 그리 어렵지 않았다.
미리 만들어져 있는 컴포넌트를 가져다 쓰기만 하면 되었기 때문이다.
하지만 반응형 디자인을 적용하면서 몇가지 문제가 생겼었다.
기본적인 화면의 구조는 사이드 메뉴가 있는 Left Section,
헤더와 컨텐츠가 있는 Right Section으로 나누었는데,
Right Section의 너비가 문제가 되었다.
Right Section에 아무런 width도 주지 않을 경우 기본 PC화면에서 오른쪽에 여백이 생기고,
width를 100%로 줄 경우 기본 PC화면에 꽉 차게 되지만 기기의 너비가 좁아질 경우 나머지 요소들의 스타일이 깨져버리는 문제가 발생했다.
그래서 둘 중 어떤 방식을 취해야 할지 고민 끝에,, 선택한 방법은 미디어 쿼리를 활용하여
PC보다 기기 너비가 넓어질 때는 width를 100%로 주고, 그 이하 너비에서는 width를 auto로 주는 방법을 선택했다.
이렇게 해주니 반응형에서도 올바르게 너비가 적용되었다.
다음은 대시보드의 색상을 변경할 수 있는 컬러 팔레트 부분이었다.
기존 PC 화면에서는 컬러 팔레트 5색상이 전부 나올 수 있게 하지만,
모바일 화면에서는 현재 선택되어 있는 대시보드의 색상 하나만 타일로 나타나게 해주어야 했다.
이 부분은 edit 페이지 컴포넌트에서 state를 만들어 관리함으로써 해결하였다.
모바일 사이즈인지 아닌지 알려주는 state를 만들고, 현재 기기의 너비에 따라 state 값을 변경해주었다.
이 때 창의 너비를 받아오는 함수가 길어지고 중복되기 때문에, 커스텀 훅으로 만들어 활용했다.
window 객체의 resize 이벤트를 활용하여 너비를 받아오는 훅을 만들고,
import { useEffect, useState } from 'react';
type windowSizeType = {
windowWidth: number;
windowHeight: number;
};
const useWindowSize = () => {
const [windowSize, setWindowSize] = useState<windowSizeType>({
windowWidth: 1920,
windowHeight: 1080,
});
useEffect(() => {
const resizeHandler = () => {
setTimeout(() => {
setWindowSize({ windowWidth: window.innerWidth, windowHeight: window.innerHeight });
}, 500);
};
resizeHandler();
window.addEventListener('resize', resizeHandler);
return () => {
window.removeEventListener('resize', resizeHandler);
};
}, []);
return windowSize;
};
export default useWindowSize;
이후 페이지에서 아래와 같이 사용하였다,
const { windowWidth } = useWindowSize();
// 컬러 팔레트를 사용하는 컴포넌트
<EditName
isMobile={windowWidth <= size.tablet}
...
/>
EditName 컴포넌트 안에서는 isMobile prop 값에 따라 true면 현재 대시보드의 색을 가진 ColorTile을,
false면 ColorPalette를 렌더링 하도록 해주었다.
edit 페이지는 간단해 보이지만 은근히 핵심적인 기능들이 들어가 있다.
멤버를 초대하는 것 처럼 공유기능이나, 페이지네이션 기능의 경우는 이번에 처음 구현 해보는 파트라 설렜다..
정보 변경, 멤버 삭제, 멤버 초대 기능 자체는 api 요청을 통해 이루어지기 때문에,
이전 프로젝트와 비교하여 특별한 점은 없다.
이번 프로젝트에서 달라진 점은 Next를 사용한다는 점,
모든 api 요청에 accessToken을 넣어 권한을 명시해주어야 한다는 점이다.
정보 가져오기
Next.js를 사용하는 만큼 SSR을 적용해보기로 했다.
이전에 배웠던 프리렌더링을 활용해보며 성능의 차이를 보고 싶기도 했고,
수정 페이지의 특성 상 데이터가 자주 바뀌기 때문에 정적 생성 보다는 SSR이 적합하다고 판단했다.
또 이번 프로젝트에서는 이전에 사용했던 fetch 대신 axios를 사용해보았다.
개인적으로 axios에는 get, post 등 메소드를 간편하게 사용할 수 있어 좋았다.
기본적으로 api 통신 함수는 아래와 같은 형태로 작성해주었다.
export const getDashboard = async (id: string): Promise<DashBoardNameData> => {
return await axios
.get(`dashboards/${id}`, {
headers: {
Authorization: `Bearer ${ACCESS_TOKEN}`,
},
})
.then((res) => res.data)
.catch((error: Error) => {
// 이후 예외 처리
});
};
페이지 컴포넌트에서는 여기서 받아온 데이터를 getServerSideProps으로 내려주었다.
export async function getServerSideProps(context: any) {
const dashboardId = context.query['dashboardId'];
const dashboardData = await getDashboard(dashboardId);
return {
props: {
dashboardData,
},
};
}
정보 업데이트하기
이전 프로젝트에서 하나의 큰 이슈였던 재렌더링 없이 바뀐 정보를 업데이트 하기..
이번 프로젝트는 큰 원리는 똑같으나, 살짝 다른 방식으로 구현해보았다.
우선 위에서 ServerSideProps로 받아온 정보를 페이지 컴포넌트 내의 state에 initialState로 받아왔다.
function Edit({ dashboardData: initialDashboardData}: EditPageProps) {
const [dashboardData, setDashboardData] = useState(initialDashboardData);
이렇게 해주면 처음 접속했을 때의 데이터는 기존에 서버에 저장되어 있는 데이터이다. (서버 데이터)
그리고 update 동작이 이루어지면, dashboardData state를 업데이트하여 화면에 보여주는 것이다. (클라이언트 데이터)
이런 식으로 구현했을 때 멘토님께서는 SSR이 제대로 작용하지 않을 것이다.. 라고 하셨지만,
이미 서버에 데이터는 보내놓고, 화면만 싱크를 맞추어 주는거라 문제가 없을 것 같다고 판단했다.
그래서 대시보드의 색상과 이름을 바꾸고, 변경 버튼을 눌러주면 아래 함수가 동작한다.
const router = useRouter();
const dashboardId = router.query['dashboardId']?.toString();
const handleUpdateClick = async () => {
if (dashboardId) {
await updateDashboard(dashboardId, dashboardName, selectedColor);
const newDashboardList = await getDashboardList();
const newDashboardData = await getDashboard(dashboardId);
setDashboardData(newDashboardData);
setDashboardList(newDashboardList);
}
};
updateDashboard는 바뀐 정보들을 가지고 put 요청을 보내주어 서버에 바뀐 데이터를 반영하고,
newDashboardList, newDashboardData에 바로 get 요청을 보내 받아온 데이터들을 저장 후
화면에 보여줄 state들을 업데이트 하였다.
그 결과 아래와 같이 매끈하게 업데이트 된 정보가 화면에 반영되었다.
데이터 관리 리팩토링
데이터들을 각자 페이지에서 관리해주다 보니, 몇몇 데이터의 경우 prop drilling이 발생하였다.
내가 맡은 페이지에서는 사이드 메뉴에 들어가는 대시보드 리스트와, 헤더에 띄워 줄 내 정보 데이터가 그러했다.
이를 해결하기 위해 Context API를 사용하였고,
두 가지 방식으로 구현해보았다. 어떤 방법이 더 나은지는 추후 팀원들과 논의하여 결정할 예정이다.
1. 필요한 데이터만 데이터 별로, 데이터만 관리
나의 정보가 되는 myData 데이터를 예시로 들어보겠다.
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react';
import { getMyData } from '@utils/editDashboard/api';
import { DashBoardMember } from '@utils/editDashboard/edit.type';
type MyDataContextType = {
myData: DashBoardMember;
};
const MyDataContext = createContext<MyDataContextType | undefined>(undefined);
export const MyDataProvider = ({ children }: { children: ReactNode }) => {
const [myData, setMyData] = useState<DashBoardMember>({
id: 0,
userId: 0,
nickname: '',
email: '',
profileImageUrl: null,
isOwner: false,
});
const fetchData = async () => {
const myNewData = await getMyData();
setMyData(myNewData);
};
useEffect(() => {
fetchData();
}, []);
return <MyDataContext.Provider value={{ myData }}>{children}</MyDataContext.Provider>;
};
export const useMyData = () => {
const context = useContext(MyDataContext);
if (!context) {
throw new Error('Context 안에서만 사용할 수 있습니다.');
}
return context;
};
나의 경우 위와 같이 context에서는 데이터만 내려주었고,
Provider 컴포넌트 안에서 api 함수를 불러서 state에 저장해준 뒤 해당 값을 value로 리턴해주었다.
이후 myData를 써야하는 부분에서
const {myData} = useMyData();
와 같이 간단히 사용해주었다.
2. 모든 데이터를 하나의 컨텍스트에, api 함수까지 함께 관리
이 방식은 다른 팀원 분이 구현하신 방식이다.
import { TColumns, TDashInfo, TDashboards } from '@pages/dashboard/Dashboard.type';
import { getColumns, getDashboardInfo, getDashboards } from '@pages/dashboard/api';
import { useRouter } from 'next/router';
import React, { ReactNode, createContext, useContext, useEffect, useState } from 'react';
type ProviderProps = {
children: ReactNode;
dashboardId: number;
};
const initialContext = {
dashboardId: 0,
dashInfo: {} as TDashInfo,
dashboards: [] as TDashboards,
fetchDashboardInfo: () => {},
fetchDashboards: () => {},
};
const DashContext = createContext(initialContext);
export const useDashContext = () => useContext(DashContext);
export function DashProvider({ children, dashboardId }: ProviderProps) {
const [dashInfo, setDashInfo] = useState<TDashInfo>({
color: '',
createdAt: '',
createdByMe: false,
id: 0,
title: '',
updatedAt: '',
userId: 0,
});
const [dashboards, setDashboards] = useState<TDashboards>([]);
const fetchDashboardInfo = async () => {
const res = await getDashboardInfo(dashboardId);
setDashInfo(res);
};
const fetchDashboards = async () => {
const res = await getDashboards();
const result = res.dashboards;
setDashboards(result);
};
useEffect(() => {
if (!dashboardId) return;
fetchDashboardInfo();
fetchDashboards();
}, [dashboardId]);
const values = {
dashboardId,
dashInfo,
dashboards,
fetchDashboardInfo,
fetchDashboards,
};
return <DashContext.Provider value={values}>{children}</DashContext.Provider>;
}
이렇게 데이터 뿐 아니라 fetch~~ 와 같이 api 통신 함수도 함께 내려주는 방식이다.
이 방식을 적용한다면 데이터를 굳이 ServerSideProp으로 내려 받지 않아도
기본적으로 세팅이 되어 있는 상태이고, 해당 데이터를 간단히 사용할 수도 있다.
또한 위에서 정보를 업데이트를 할 때 get 요청을 한 번 더 보내는 작업을
이 context의 fetch~~함수를 불러다 사용하면 된다.
이것을 적용하여 기존의 코드를 수정해보면 아래와 같다.
const handleUpdateClick = async () => {
if (dashboardId) {
await updateDashboard(dashboardId, dashboardName, selectedColor);
fetchDashboardInfo();
fetchDashboards();
}
};
이렇게 해주면 context의 데이터들이 자동으로(?) 업데이트 되므로 사이드 메뉴나, 헤더 컴포넌트 안에서는
따로 prop을 줄 필요 없이 해당 컴포넌트 안에서 context의 데이터들을 받아서 사용하면 된다.
기존 : edit -> sidemenu -> dashList 로 dashboardList 데이터를 넘겨줌
수정 후 : dashList 안에서 const { dashboards } = useDashContext();
로 사용
확실히 간편하고 코드가 깔끔해지긴 하나, 조금의 props 드릴링도 허용하지 않고 모두 context로 사용하게 되는게 좋기만 한건지는 아직 잘 모르겠따.. 이전 파트 멘토님께서는 다른 방향(이전 포스트)으로 조언을 해주셨기 때문에..
멤버 삭제나 초대하는 기능도 api 요청 메소드만 다를 뿐, 위와 동일한 방식으로 구현해주었다.
이번 프로젝트에서 멤버 목록을 표시할 때 페이지네이션 기능이 필요했고, 페이지네이션이 가능한 api를 처음 사용해보았다.
members?page=${page}&size=${PAGE_SIZE}&dashboardId=${id}
위와 같은 형태의 주소로 리퀘스트를 보내주는데,
크게 어려운 점 없이 이동할 페이지의 숫자를 api 함수의 {page}아규먼트로 넘겨준 후,
쿼리스트링으로 get 요청을 하면 되었다.
우선 팀원들과의 논의 후에 한 페이지(PAGE_SIZE)에는 5명씩 보여주는 것이 UI적으로 깔끔하다고 판단하였다.
전체 페이지 수
api get 요청 시 전체 멤버의 수가 응답으로 돌아왔다.
해당 값을 5로 나누어 올림 처리 해준 값을 전체 페이지 수로 사용하였다.
멤버가 하나도 없을 경우 '0페이지'와 같이 뜨면 안되기 때문에, 기본 값을 1로 설정해주었다.
현재 페이지
state로 관리하였다.
페이지를 넘기는 버튼을 클릭할 때마다 이전 값에 1을 더하거나 빼서 현재 페이지 숫자를 관리해주었다.
전체 페이지 수보다 많아지거나 1보다 작아지면 변화를 주지 않았다.
이후 useEffect를 활용하여 페이지 숫자가 바뀔 때마다
해당 숫자를 api 함수의 아규먼트로 넘겨주어 get 요청을 실행하였다.
대표적으로 api 요청을 할 때 발생하는 에러 타입에 따라 예외 처리를 해주었다.
401 에러의 경우 권한 없음, 400 에러의 경우 올바르지 않은 입력 등등..
에러의 메시지를 통해 에러를 판단하였고, 상황에 따라 맞는 문구를 alert로 띄울 수 있도록 하였다.
...
.catch((error: Error) => {
if (error.message === ERROR_403_MESSAGE) alert(NO_AUTHORITY_MESSAGE);
else if (error.message === ERROR_404_MESSAGE) alert(NO_DASHBOARD_MESSAGE);
}
나중에 좀 더 디테일하게 리팩토링을 할 수 있게 된다면, toas 혹은 Modal 같은 장치를 이용해서
좀 더 예쁘게(?) 에러 처리를 할 수도 있을 것 같다.
이번에 TypeScript를 프로젝트에 도입하면서, 정말 수많은 타입 오류를 만났다.
그 중에서도 이벤트 핸들러로 쓸 함수들을 prop으로 넘겨줄 때 함수들의 타입을 어떻게 정할지 고민이 많이 되었다.
기존에 이벤트 핸들러 함수의 타입을
(e: MouseEvent) => void; // MouseEventHandler<HTMLButtonElement> 도 가능
와 같이 정해주라고 해서 적용해보았더니 오류가 났다.
그래서 처음에는 모두 any 처리를 해주고 진행했고,
이 후 리팩토링 과정에서 하나하나 타입을 지정해주었다.
onChange 같은 경우 기본적으로 input 안에 있는 값을 가지고 setState를 하기 때문에
(value: string) => void 와 같은 형식으로 타입을 지정해주었다.
onClick의 경우 일반적으로 별다른 파라미터를 받지 않기 때문에 () => void 와 같이 해주었다.
아직 타입 지정에 관해 아주 익숙하지는 않아서 발생한 오류인지도 모르겠다.
내가 만들어서 사용하고자 했던 modal 안의 Input에 글씨가 써지지 않는 문제가 발생했다.
멤버를 초대하는 기능의 모달이었고, 해당 모달의 Input에 초대할 아이디를 적으면 초대가 발송되어야 했다.
onChange를 주어 input value를 초대 아이디 state로 설정하고자 했는데,
onChange 옵션을 주기만 하면 문제가 발생했다.
모달이 아닌 바깥의 input에서는 해당 로직이 잘 동작하고 있어 더욱 의문이었다..
수많은 시도를 하다가, 모달을 호출하는 바로 그 시점, 즉 해당 모달 컴포넌트 안에서 setInviteEmail(초대할 아이디 state) 함수를 바로 내려주니 잘 동작이 되었다.
원래는 edit 페이지 > TableHeader 컴포넌트 > 모달 순으로 setInviteEmail 함수를 넘겨주고 있었다...
function InviteModal({ close, onInviteClick }: InviteModalProps) {
...
const [inviteEmail, setInviteEmail] = useState<string>();
return (
<S.ModalContainer>
<S.ModalTitle>초대하기</S.ModalTitle>
<ModalInput id="email" type="text" placeholder="이메일을 입력해주세요." onChange={setInviteEmail}>
이메일
</ModalInput>
<S.ModalButtons>
<Button.ModalReject onClick={trigger}>취소</Button.ModalReject>
<Button.ModalConfirm
onClick={() => {
if (onInviteClick && inviteEmail) {
onInviteClick(inviteEmail);
}
trigger();
}}
>
초대
</Button.ModalConfirm>
</S.ModalButtons>
</S.ModalContainer>
);
}
문제의 원인을 짐작해봤을 때, input이 사용되는 시점에 onChange를 준게 아니라 쓸데 없이 저~위에 부터 setState prop을 내려줬기 때문이었던 것 같다.
대시보드 수정 페이지에서는 dashboardId라는 params를 받아와 api 통신을 해주어야 한다.
평소처럼 페이지 컴포넌트 안에서 router.query를 써주고 있었지만,
새로 고침 시에는 query가 제대로 불러와지지 않는 이슈가 발생했다.
이후 Next.js의 경우 기본적으로 프리렌더링이 되기 때문이라는 이슈의 원인을 발견하였고,
따라서 getServerSideProps(SSR)을 통해 먼저 dashboardId 라는 쿼리(context.query)를 받아와
페이지 컴포넌트에 내려주는 방식으로 해결하였다.
export const getServerSideProps = async (context: any) => {
const { dashboardId } = context.query;
return {
props: {
dashboardId,
},
};
};
function Edit({ dashboardId }: { dashboardId: string }) {
...
토큰을 사용하고자 컨텍스트를 분리하는 작업 이후,
대시보드 정보 수정 시 sidemenu, header에 수정된 사항이 바로 반영되지 않는 이슈가 발생했다.
edit 페이지 내에서 DashProvider를 사용하고 있었고,
그렇기 때문에 컨텍스트 내의 함수를 페이지 자체에서 사용할 수는 없었다.
이 때 Provider의 영향을 받고 있는 페이지 내 하위 컴포넌트 안에서는 컨텍스트의 함수가 올바르게 동작하고 있다는 것을 발견하였다. 그래서 변경 버튼 클릭 시 isEdited라는 state를 조정하여 하위 컴포넌트인 Header, SideMenu 컴포넌트에 prop으로 내려주고, 해당 컴포넌트 안에서 isEdited state에 따라 컨텍스트 내의 fetch 함수를 사용하여 수정된 정보가 바로 반영될 수 있게 하였다.
edit 페이지 컴포넌트
const [isEdited, setIsEdited] = useState(false);
<Sidemenu isDashboardEdited={isEdited} setIsDashboardEdited={setIsEdited} />
SideMenu 컴포넌트
useEffect(() => {
if (isDashboardEdited) {
fetchMyDashboardsAll(); // 현재 대시보드 리스트를 fetch
if (setIsDashboardEdited) setIsDashboardEdited(false);
}
}, [isDashboardEdited]);
이번에 Next.js나 TypeScript 처럼 처음 프로젝트에 적용해보는 기술이 있어서,
확실히 기초프로젝트 때 보다는 난이도가 있다고 느껴졌다.
프로젝트가 거의 마무리 될 때 오류 하나를 고치면서 때려 치고 싶다는 생각이 들 정도,,,
심화 프로젝트는 아마 더 어렵겠지..?
그래도 단 한번의 고비를 이기고 오류를 해결해냈을 때의 짜릿함때문에 프로젝트가 재밌는 것 같다.
그리고 기획 과정에서 우리가 구현할 기능이나 컴포넌트에 대해 정말 촘촘히 정해놔야겠구나 하고 몸소 느꼈다.
우리 팀의 경우 각자 공통 컴포넌트를 만들고, 각자 맡은 페이지에서 다른 팀원이 만들어놓은 컴포넌트를 사용하는 과정에서 타입을 고치고, 추가하는 과정에서 코드가 꼬이는 경우가 많이 발생했다.
이런 문제점을 팀원 모두가 회고하며 깊이 공감하였다...
이러한 사항을 다음 심화 프로젝트 때 잘 반영해서 마지막 프로젝트가 정말 알찬 프로젝트가 될 수 있도록 해야겠다.
아니 뭐야 너무 꼼꼼하게 적었잖아요 저도 다시 적으러 갑니다.. 총총.. 잘 보고 갑니다🐾