

드디어 Econovation Recruit가 끝이 났어요. 실제로 사람들이 사용하는 첫번째 프로젝트였어요. 처음하는 것만큼 안전성에 목적을 두었고, 문제없이 잘 마무리가 되었어요.
이 프로젝트는 신입모집 동아리 사람들의 불편으로부터 시작되었어요. 총 인원이 30명 가량 있는 Econovation(동아리)는 서류와 면접을 통해서 신입 기수를 모집해요. 사람을 뽑는데 고려를 많이 하려고 하니, 다양한 툴을 사용하게 되었다. 나열을 하자면 피그마, 노션, 구글 form, 트렐로, 메일, 문자, 엑셀, 쉐어포인트 등 진행 상황이나 데이터를 공유하는 사이트를 사용했어요. 그러다보니 데이터의 분산이 이루어지며 서로 관계가 복잡해져 신입모집을 하는데 불편했어요.
이 프로젝트는 신입모집을 하는데 전반적인 모든 편의 기능을 넣는 프로젝트예요. Designer가 1명, Backend 1명, 그리고 Frontend 2명에서 진행했으며 다음과 같은 기능이 주를 이루었어요.
저는 이 프로젝트에서 1~3, backborn을 담당하여 개발을 진행했었어요.
첫번째 사용자로서 Recruit TF팀이 사용했어요. 이들은 지원자들의 지원서와 면접 자료들을 쉽게 보기 위해서 사용했어요. 두번째 사용자로서 에코노베이션에 들어가려는 사람들이 사용했어요. 지원서를 제출하며, 자신이 썼던 지원서 내용을 볼 수 있어요.
이 웹은 각 페이지마다 로그인을 하는지 안하는지 검사해야 했어요. 각 사람들의 개인 정보에 대해서 물어보고 수집하기 때문에 면접 이외의 목적에는 사용하지 않기 위해, 그리고 유출을 금하기 위해서 로그인 시스템을 도입해야 했어요.
사용자의 로그인 여부와 관리를 쉽게 하기 위해서 axios instance를 나누어서 개발하였고, 또한 Component를 만들어 선언적하면 적용할 수 있게 했어요. 403, 401 같은 경우에는 로그인이 되어 있더라도 실패로 간주하고 로그인 페이지로 보냈어요. 이를 통해서 로그인으로 가는 버튼을 제거했어요.(member only이기 때문이에요)
"use client";
import { localStorage } from "@/src/functions/localstorage";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
const Validate = () => {
const router = useRouter();
useEffect(() => {
if (
localStorage.get("accessToken", "") === "" ||
localStorage.get("refreshToken", "") === ""
) {
alert("로그인이 필요합니다.");
router.replace("/signin");
return;
}
}, []);
return null;
};
export default Validate;위와 같은 코드를 보면 토큰이 없다면 모든 history path를 날리고 로그인 페이지로 이동하게 했어요.
if (error.response.status === 401 || error.response.status === 403) {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
alert("로그인이 필요합니다.");
window.location.href = "/signin";
}또한 에러의 코드가 403, 401인 경우 토큰을 삭제했어요.
도입된지 하루만에 급하게 연락이 와 메일, 문자에 대해 자동화를 해달라고 연락이 왔어요. 이들의 기능은 돈이 들어가거나 구현하기 복잡한 문제를 가진 것뿐이었어요.
이 문자 문제는 기존에 node로 짜여져 있는 코드가 있어 nextjs를 사용하는 이 프로젝트에서 쓰기 적절했어요. 주어진 문구와 parameter로 데이터를 받아 문자를 보냈어요.
메일에서 보여지는 web spec은 우리가 브라우저에서 쓰는 스펙과 많이 달라요. table로 디자인을 하는 경우도 있고, flex, grid는 안되는 곳이 허다했어요. 메일전용으로 만드는 페이지를 만들고 외부로 가는 링크를 만들어 지원자가 보낸 지원서를 확인할 수 있게 했어요. 지원서에 해당하는 아이디가 UUIDv4로 작성되어 있기 때문에 주입을 해주어야 하는데, 백엔드와 협업하여 %APPLICANT_ID%를 replace하여 바꾸는 형태로 해결했어요.
우리의 사이트는 어떠한 일이 있더라도 사용자가 제출한 데이터가 삭제되면 안 됐어요. 그렇기 때문에 여러 가지 안전장치를 만들었어요.
이 프로젝트는 위 2가지를 이용하여 백업을 처리했는데(물론 백엔드에서도 DB 이중화를 했다) 할 수 있었던 이유는, 모든 질문에 대한 응답은 JSON 객체로 변환이 가능했기 때문이고, 단순 백업 용도이기 때문에 간단한 database 구조를 가졌기 때문이에요. 실제로 1건에 대해서 이 데이터베이스에서 복구하여 사용했어요.
프로젝트를 만들고 있는 와중에도 질문이 변경되었었어요. 매번 질문이 변경될 때마다 디자이너에게 물어보거나, 그에 맞추어서 개발을 진행하기에 비용이 너무 많이 들 것 같아 모듈화를 했어요. 형태는 Flex를 본 따서 만들었고, 각 type마다 다르게 보이거나 데이터를 쓸 수 있도록 했어요.
import { ApplicantReq } from "@/src/apis/applicant/applicant";
import dynamic from "next/dynamic";
import { FC } from "react";
const ApplicantCustomField = dynamic(
() => import("./applicantNode/CustomField.component")
);
// ...다양한 dynamic import
interface JunctionApplicantProps {
applicantNodeData: ApplicantNode;
data: ApplicantReq[];
}
export const JunctionApplicant: FC<JunctionApplicantProps> = ({
applicantNodeData,
data: applicantData,
}) => {
const jsxNode = {
customField: (
<ApplicantCustomField nodeData={applicantNodeData} data={applicantData} />
),
customHuman: (
<ApplicantCustomHuman nodeData={applicantNodeData} data={applicantData} />
),
// ..다양한 구분자들
};
return jsxNode[applicantNodeData.type] ?? <></>;
};위 코드를 보면 applicationNodeData의 타입에 따라서 object에 접근하는 형태로 썼었어요. 이런 식으로 지원서 내 질문의 형태를 나누었어요. 지원서의 Layout 또한 이런 식으로 나누어 다양한 component와 layout을 만들어 쉽게 조합하게 만들었어요.
지금 생각해보면 typescript를 이용하기 때문에 상표를 붙여 활용하는 것도 나쁘지 않겠다라는 생각을 해봐요.
칸반보드는 trello에서 maintainer로 사용하고 있는 https://github.com/atlassian/react-beautiful-dnd를 기반으로 작성하였다. 한 곳에서만 칸반보드를 옮면 사실 쉽게 코드를 작성할 수 있었다. 하지만 우리의 기능은 trello처럼 칸반을 자유롭게 생성하고 움직이는 것을 목표로 하였다. 이 또한 백엔드와 많은 데이터 형태를 고려하여 다음과 같은 스펙을 정하였다.
이 경우를 가지고 복잡하지만 다음과 같은 형태로 코드가 이루어져요.
export const getFromToIndexDefault = (
kanbanData: KanbanColumnData[],
result: DropResult
): { boardId: number; targetBoardId: number } => {
if (!result.destination) return { boardId: 0, targetBoardId: 0 };
const from = result.source;
const to = result.destination;
const boardId = kanbanData[+from.droppableId].card[from.index]?.boardId ?? 0;
const targetBoardId =
to.droppableId !== from.droppableId && to.index < from.index
? kanbanData[+to.droppableId].card[to.index - 1]?.boardId ??
kanbanData[+to.droppableId].card[0]?.boardId ??
0
: kanbanData[+to.droppableId].card[to.index]?.boardId ?? 0;
return { boardId, targetBoardId };
};설명하자면 column이 움직일 때가 있고, default(카드 한 개)가 옮겨질 때가 있는데, 이는 default가 옮겨질 때예요. targetBoardId가 가장 복잡하게 설정되는데 해석하자면 다음과 같아요.
이 이유는 같은 곳에서 옮길 때 자신이 있는 곳에서는 미리 더해진 상태로 움직인다는 계산이 있기 때문이에요.(이미 그 자리를 차지하고 있음)
하지만 -1이 없는 경우 undefined일 수 있어요(0개인 경우). 그럴 때는 미리 사전에 말해둔 상태인 맨 위의 것은 0으로 이동해요.
이와 같이 움직이면 column간 데이터의 이동이 자유롭게 설정돼요.
사실상 첫번째로 사용자에 대한 피드백을 받을 수 있었던 프로젝트였어요. 다행히 backend와 frontend 간의 소통이 매우 잘 되어서(예를 들면 어디에서 데이터를 처리할 것인가, 정책) 사람에 대한 스트레스나 프로젝트에 대한 스트레스는 없었어요. 하지만 의도치 않는 피드백이 요청되어서 놀랐어요. 저도 이 리크루트 프로세스의 불편한 점을 개선하기 위해서 개발을 시작했고 대부분의 불편한 점을 개선했다고 생각했지만, 사용자들은 자신의 일을 줄이는 자동화에 대해서 더욱 관심을 가졌었어요.(당연한 일인 건가요?)
사실 요청사항이나 개발 요구사항에는 모두 완성된 것이 아니었어요. 실제로 실험적 기능을 가진 사이트였고 다행히 성공적으로 마무리가 될 수 있었어요. 그렇기 때문에 개선해야 할 점이 많아 보여요.
사용자들에게 피드백을 받는 것이 정말 좋은 경험임이 틀림없다는 것을 배웠어요. 항상 생각보다 사용자들의 생각은 제 생각과 다르고 또 맞는 것도 있다는 것을 배웠어요. 새로운 프로젝트를 시작할 때 더 나은 고민을 하고자 해요.