🎧 글 진행 순서
1. 프로젝트 결과물 & 구현내용
2. API 명세서 제공 이전의 FE 구현 : MSW
3. CRUD 구현
4. 회원 권한에 따른 예외처리 구현
5. apis 기능 분리 : 서버 통신 및 데이터 맵핑을 담당하는 코드의 분리
6. AWS S3 & CloudFront & Route53을 활용한 정적 웹 배포
다사다난했던 프로젝트가 마무리되었다.
이번 프로젝트에서는 배운게 너무 많아서 정리하기도 쉽지 않고, 특별히 팀원분들과 멘토분들의 도움을 많이 받아서 감사한 시간이었다.
그래서, 프로젝트의 회고를 협업회고 / 기술회고로 나눠서 정리해보고자 한다.
기술회고에 대해 짧은 요약을 하자면,
"세상은 넓고, 방법은 많고, 라이브러리는 더 많다.. 근데, JS/네트워크/개발이론을 모르면 내 이야기 아님.."
프로젝트 결과물 & 구현내용
프로젝트 명 : 스터디 통합 플랫폼 EduSync
프로젝트 기간 : 2023.05.01 ~ 2023.05.25
인원 : FE 3명, BE 3명
배포링크 : https://www.edusync.com
Github : https://github.com/codestates-seb/seb43_main_016
구현담당 : 스터디 그룹 관리 / 캘린더 기능
API 명세서 및 서버 구현 이전의 FE 구현 : MSW
추후 총평에 추가로 이야기하게 될 내용이지만, 일단 MSW를 사용하지 않아도 무방할만큼 최소사양을 최소화하고, 기능들이 동작할 수 있는 "프로토타입(prototype)" 페이지 및 API 명세에서 어떤 데이터를 주고받을지 확실하게 정해놓는 것이 좋다!
보통 서버에 어떤 데이터를 저장하고 요청할지를 정한 API 명세서와 서버 구현이 나오기 전까지는 프론트에서 구현 & 테스트를 할 수 있는 부분은 많지 않다. 하지만 실제 서버를 연결한 뒤에 동일하게 동작할 수 있도록 브라우저에서 보낸 요청을 가로채서, 가상의 응답을 제공하는 Mocking Service Worker를 사용하면 해당 부분을 테스트할 수 있다.
아래는 게시글 관리 페이지 기능에 진입하기 전 로그인 상태 / 회원가입 상태 / 가상의 accessToken을 발급받아 다양한 기능을 구현하고 테스트하는 과정에서 작성한 msw 코드이다. 이와 같이, 서버가 구현되어서 직접 테스트를 하기 어려운 상황이라도 FE 구현은 테스팅을 통해 가능하다.
import { RestRequest, rest } from "msw";
import { MemberPasswordCheckDto } from "../apis/MemberApi";
import { getStudyGroupInfo } from "../apis/StudyGroupApi";
import { useRecoilValue } from "recoil";
import { LogInState } from "../recoil/atoms/LogInState";
const isLoggedIn = useRecoilValue(LogInState);
interface LoginRequestBody {
email: string;
password: string;
}
interface LoginResponse {
data: string;
}
interface UserInfo {
uuid: string;
email: string;
profileImage: string;
nickName: string;
aboutMe: string;
withMe: string;
memberStatus: string;
roles: string[];
}
interface Data {
nickName: string;
password: string;
}
interface ProfileDetailData {
aboutMe: string;
withMe: string;
}
interface PasswordData {
password: string;
}
export const handlers = [
rest.post(`${import.meta.env.VITE_APP_API_URL}/refresh`, (req, res, ctx) => {
const refreshToken = req.headers.get('refresh');
if (refreshToken === '유효한_리프레시_토큰') {
const fakeAccessToken = '가짜_액세스_토큰';
return res(
ctx.set('authorization', fakeAccessToken),
ctx.status(200)
);
}
return res(
ctx.status(401),
ctx.json({ message: 'Unauthorized' })
);
}),
rest.post<LoginRequestBody, LoginResponse>(
`${import.meta.env.VITE_APP_API_URL}/members/login`,
(req, res, ctx) => {
const { email, password } = req.body;
if (email === "user1@gmail.com" && password === "user1") {
const accessToken = "your-access-token";
return res(
ctx.status(200),
ctx.set("Authorization", `Bearer ${accessToken}`),
ctx.json({ message: "로그인 성공", data: accessToken })
);
} else {
return res(ctx.status(401), ctx.json({ error: "인증 실패" }));
}
}
),
rest.get(
`${import.meta.env.VITE_APP_API_URL}/members`,
(req: RestRequest, res, ctx) => {
const accessToken = req.headers.get("Authorization")?.replace("Bearer ", "");
if (accessToken === "Bearer your-access-token") {
const userInfo: UserInfo = {
uuid: "b1c452bf-4b2f-4ea5-89f3-a241795495ba",
email: "test5555@gmail.com",
profileImage: "https://avatars.githubusercontent.com/u/120456261?v=4",
nickName: "테스트5555",
aboutMe: "test5555_aboutMe",
withMe: "test5555_aboutMe",
memberStatus: "MEMBER_ACTIVE",
roles: ["LEADER"],
};
return res(ctx.status(200), ctx.json(userInfo));
} else {
return res(ctx.status(401), ctx.json({ error: "Unauthorized" }));
}
}
),
rest.patch(
`${import.meta.env.VITE_APP_API_URL}/members`,
(req: RestRequest, res, ctx) => {
const accessToken = req.headers.get("Authorization")?.replace("Bearer ", "");
if (accessToken === "Bearer your-access-token") {
const data: Data = {
nickName: "테스트5550",
password: "user5550",
};
return res(ctx.status(200), ctx.json(data));
} else {
return res(ctx.status(401), ctx.json({ error: "Unauthorized" }));
}
}
),
// 후략 ...
];
CRUD 구현 : 마이페이지 & 스터디 대기목록 페이지 & 스터디 관리 페이지 & 캘린더 이벤트 CRUD
이번 프로젝트에서 내가 맡은 구현 분야는 스터디 관리 페이지 및 캘린더 이벤트의 CRUD이다.
CRUD 구현은 전적으로 API 명세서에 의존한다고 봐도 무방하다. 그렇기에 스터디 관리 페이지 및 캘린더 이벤트를 구현하면서 하염없이 API 명세서를 기다렸던 시간들이 있다. (이불킥각..)
다만 첫 2주간의 삽질을 어느정도 마무리하고 새로운 마음으로 개발에 집중했을 때, 아직 서버에서는 구현되지 않았지만 프론트에서는 구현할 수 있는 지점들이 있어서 해당 기능을 조금은 다르게(?) 구현을 해봤다.
이번 프로젝트에서 캘린더는 서드파티 기능에 가까웠지만, 실제 UX 경험으로 봤을 때는 가장 핵심적인 기능에 속한다.
User가 스터디를 아무리 구조적으로 구성한다해도, 자신의 일정과 스터디의 일정을 일일이 계산해서 입력해야 한다면 아마 스터디 자체를 관리하기가 상당히 어려울 것이다.
즉, 캘린더는 유저가 굳이 신경쓰지 않아도 스터디의 정보를 OverView할 수 있는 형태로 구현되어야 하며, 해당 스터디의 정보에서 가장 핵심적인 내용을 가져와 유저에게 제공해야만 한다. 이는 단순히 스터디 가입 외에도 스터디 탈퇴 등의 이벤트에도 동일한 자동화가 이뤄져야 한다.
이를 API 명세서 없이 구현하기 위해서는 이미 API 문서에 제공되어 있는 2가지 데이터를 기반으로 구현했다.
이 두 데이터를 기반한다면 다음과 같은 로직을 구현할 수 있다.
1. 내가 속한 스터디 정보를 조회한다.
2. 스터디 정보에서 스터디의 id를 추출한다.
3. 스터디 id로 내가 속한 스터디의 상세정보를 조회한다.
4. 날짜 / 시간 / 요일을 fullCalendar 라이브러리의 이벤트 형식에 맞게 arr.map()메서드로 맵핑한다.
5. 캘린더에 나와있는 이벤트를 클릭했을 때, 해당 모달로 스터디 id를 Props로 전달하여 스터디 상세 정보를 로드한다.
지금은 새로운 API 명세가 나와서, 위의 로직이 필요없어졌음으로 github 로그에 남아있는 기록을 가져왔다.
import {
StudyInfoDto,
getStudyGroupInfo,
getStudyGroupList,
} from "./StudyGroupApi";
// ====================== 개인이 속한 스터디의 스케줄을 가져오는 로직 ===========================
// 1. 개인이 속한 스터디 조회
// 2. 조회 데이터의 id 추출
// 3. id를 인자로 전달하여 각 스터디의 상세정보를 추출하고, 변수에 담기
// 4. 변수에 담은 스터디 정보를 fullCalendar 라이브러리에 맞게 맵핑
// 5. fullCalendar 라이브러리에 전달하여 이벤트 생성
export interface Event {
id: string;
title: string;
daysOfWeek?: string[];
startTime: string;
endTime: string;
startRecur: string;
endRecur: string;
description: string;
overlap: boolean;
}
export const generateStudyEvents = async (
isLoggedIn: boolean
): Promise<Event[]> => {
// 1. 개인이 속한 스터디 조회
const myStudyGroups = await getStudyGroupList();
console.log(myStudyGroups);
// 2. 조회 데이터의 id 추출
const studyGroupIds: number[] = [];
// members 배열에서 스터디 그룹의 ID 추출
for (const member of myStudyGroups.data.members) {
studyGroupIds.push(member.id);
}
// 3. id를 인자로 전달하여 각 스터디의 상세정보를 추출하고, 변수에 담기
const studyGroupInfos: StudyInfoDto[] = [];
for (const id of studyGroupIds) {
const studyGroupInfo = await getStudyGroupInfo(id, isLoggedIn);
studyGroupInfos.push(studyGroupInfo);
}
// 4. 변수에 담은 스터디 정보를 fullCalendar 라이브러리에 맞게 맵핑
const events: Event[] = studyGroupInfos.map(
(studyGroupInfo: StudyInfoDto) => {
const mappedDaysOfWeek: string[] = studyGroupInfo.daysOfWeek.map(
(day: string) => {
switch (day) {
case "월":
return "1"; // "월" -> 1
case "화":
return "2"; // "화" -> 2
case "수":
return "3"; // "수" -> 3
case "목":
return "4"; // "목" -> 4
case "금":
return "5"; // "금" -> 5
case "토":
return "6"; // "토" -> 6
case "일":
return "0"; // "일" -> 0
default:
return ""; // handle any other cases if necessary
}
}
);
const event: Event = {
id: studyGroupInfo.id.toString(),
title: studyGroupInfo.studyName,
daysOfWeek: mappedDaysOfWeek,
startTime: `${studyGroupInfo.studyTimeStart}:00`,
endTime: `${studyGroupInfo.studyTimeEnd}:00`,
startRecur: studyGroupInfo.studyPeriodStart,
endRecur: studyGroupInfo.studyPeriodEnd,
description: studyGroupInfo.introduction,
overlap: true,
};
console.log(event);
return event;
}
);
// 5. fullCalendar 이벤트 배열 반환
return events;
};
회원 권한에 따른 예외처리 구현
실제로 백엔드 분들과 가장 많은 소통이 이뤄지고, 가장 많은 에러로 고생했던 과정이 회원 권한에 따른 예외처리를 포함한 각종 예외처리였다. 구현할 때는 분명 문제없이 잘 되던게 배포 서버에 올리자마자 에러가 나고, 예상하지 못한 유저의 동작에 적절한 reaction을 주지 못해서 에러를 더욱 세분화하여 메세지를 제공하는게 어려웠다.
이번 구현 단계에서 진행한 예외처리 목록은 다음과 같다.
1. 스터디 등록 : 날짜 & 시간 미입력시 서버에 ISO8201 형식으로 전달되지 않아 해당 내용을 맵핑
2. 스터디 등록 : 날짜 & 시간 미입력시 유저에게 입력요청
3. 스터디 관리 - 스터디 장일 경우만 가능 : 스터디 삭제, 스터디 수정, 스터디 대기 인원 승인, 스터디 대기 인원 거절, 스터디원 강퇴, 스터디장 권한 위임
4. 스터디 관리 - Introduction의 Textarea에 대한 xss 공격 예외 처리
5. 스터디 관리 - 스터디 장일 경우 불가능 : 스터디 탈퇴, 회원탈퇴
6. 스터디 관리 - 회원일 경우 가능 : 스터디 탈퇴, 회원탈퇴
apis 기능 분리
기능 중심의 구현을 생각하고 처음으로 시도한 것이 별도의 apis 폴더를 만들어 서버와 통신하는 모든 기능을 apis로 관리하고, 클라이언트의 컴포넌트에서는 클라이언트 단에서만 이뤄지는 동작들을 관리하는 것이다. 이에 따라 폴더구조는 다음과 같이 변경되었다. (2주차...)
좀 더 요약하자면 다음과 같이 기능들을 세분화하여 프로젝트를 진행했다.
├── components : 클라이언트 단에서 이뤄지는 기능 및 동작을 제어
│ ├── calendar : 별도의 라이브러리를 사용하는 경우 별도의 폴더로 관리
│ ├── modal
├── apis : 서버와 통신이 이뤄지는 내용 관리
├── mock : 서버 통신 이전에 api 통신을 mocking
├── pages : 컴포넌트들이 최종적으로 조립되어 유저에게 보여지는 페이지 관리
├── assets : 미디어 파일 관리
이 내용들을 구현하면서 문제를 해결할만한 방법들을 차근차근 시도했고 여러 도구들을 만날 수 있었다.
하지만, 나의 프로그래밍적 지식이 부족해서 결국 적용을 못한 내용들이 많았다.
대표적인 것이 React-Query이다.
React-Query는 axios를 대신해서 서버 통신이 이뤄지는 네트워크 상태를 관리하는 라이브러리이다.
이를 활용해서 caching, mutation 등의 상태를 관리할 수 있다. 이를 활용해서 custom hook을 만든다면, spinner / user-validation 등의 반복된 작업을 단숨에 줄일 수 있을 것이다.
하지만, 공식문서를 봐도 잘 이해가 되지 않고 심지어 내가 구현하고 있는 내용조차 버겁게 느껴졌기 때문에 이번 프로젝트에서는 적용을 포기하고 axios를 활용해서 서버와 통신했다.
이와 같이 세상을 정말 넓고, 방법은 더 많고, 도구는 더더더욱 많은데.. 내 지식이 부족해서 활용하지 못한 것을 절감한 프로젝트였다.
'문제해결 > 프로젝트&협업' 카테고리의 다른 글
[프로젝트 코드 다시 뜯어보기] axios 인스턴스 까보기(class와 promise 문법을 중심으로) (0) | 2023.07.02 |
---|---|
[리팩토링] 이미지 업로드 : json-data => form-data (0) | 2023.07.02 |
[리팩토링] url 난독화 (utf-8 => base64 encode) (0) | 2023.07.01 |
프로젝트 리팩토링 관련 의견 공유 공간 (0) | 2023.06.26 |
서버로부터 받은 데이터는 함부로 훼손하지 않는다. (0) | 2023.05.15 |