프론트 개발자 취업 회고록 2
2022/09/18
n°14
category : Recap
☼
2022/09/18
n°14
category : Recap

앞서 말했듯이 인력이 부족한 상황이었고, 온보딩 코스가 따로 마련되지 않았기 때문에 첫날부터 바로 업무에 투입되었다. 입사하고 한달이 지나도 개발을 못한 다른 친구들도 있었기 때문에, 현업에서 구르며 빠르게 배울 수 있어 오히려 만족스러웠다.
프론트 리드님이 업무 중요도가 낮고, 구현 난이도가 어렵지 않은 업무부터 천천히 일을 던져주셨다. 하나씩 하나씩 일을 처리해가면서 점점 커가는 느낌이 들었다. 1인분의 몫을 하면서 점차 팀에 도움이 되는 것 같아 만족스러웠다.
- 통합 지갑
회사에서는 두가지의 암호화폐를 발행하고 있었다. 메인 서비스인 포탈에서 활동하면 얻을 수 있는 코인과 회사의 nft 관련된 교육 컨텐츠를 소비하면 얻을 수 있는 코인이었다. 또한 회사에서 발행하는 nft도 있었다. 이전에도 물론 이를 관리할 지갑이 있었지만, nft 코인은 별도의 도메인으로 운영 중이었기에 별도의 지갑으로 관리되었다. 하지만 메인 서비스 페이지에 nft 거래소를 추가하는 프로젝트가 기획되면서, 이 3가지 가상 자산을 통합적으로 관리할 지갑이 필요했다. 본격적으로 투입되는 프로젝트는 처음이었기에 기대반 걱정반이었다. 이 프로젝트에서 나는 다음 3가지 역할을 맡아야했다.
리뉴얼은 어느정도 도가 튼 상황이었기에, 큰 무리없이 빠르게 진행할 수 있었다. 물론 이것이 가능했던 이유는 기존의 코드가 새로 개발하기 쉽도록 짜여졌던 점도 있었고, 내가 회사 코드에 어느 정도 적응했기 때문이기도 했다. 처음에 방대한 프로젝트의 폴더구조와 모듈을 보고 압도당한 기분이었는데, 다 이럴 때를 위해서였다.
특히 마이페이지를 리뉴얼하면서 유저의 개인 정보를 불러오는 여러 훅에 적응할 수 있었다. 리뉴얼 과정에서는 총 5개의 모듈 컴포넌트를 새로 개발하였고, 16개의 모듈 컴포넌트와 2개의 템플릿 컴포넌트, 1개의 커스텀 훅을 수정하였다. 모듈 컴포넌트는 마이페이지의 하위 메뉴 하나에 해당하는 컴포넌트였으며, 템플릿 컴포넌트는 마이페이지 전체 레이아웃과 prop으로 내려줄 state, fetch 데이터를 관리하는 페이지였다.
이 과정에서 업무에서 전역 상태 관리와 SSR을 경험했기에 공유한다.
*회사 코드를 그대로 가져온 것은 아닌, 설명을 하기 위한 snippet입니다.
마이페이지의 index.tsx
const Menu = (props) => <MenuTemplate {...props} />;
export const getServerSideProps: GetServerSideProps = async (context) => {
axios.defaults.headers.common.Authorization = "";
const { tokenState } = await initializeProps(context);
return {
props: {
tokenState ,
},
};
};
아토믹 패턴을 적용하여 SSR이 일어나는 모든 페이지의 index.tsx는 이처럼 직관적으로 구성된다. util에 있는 initializeProps 함수를 이용해 로그인 이후 쿠키에 저장된 토큰을 반환받아 SSR의 템플릿에 Props로 내려준다. 기본적으로 인증이 필요한 SSR로 렌더링되는 모든 페이지는 이와 같은 방식으로 활용하였다. Node.js로 MVC 패턴의 api를 개발할 때를 떠올리게 하는 code splitting과 컴포넌트 구조였다.
마이페이지 / 지갑 아이템 공용 컴포넌트
interface IProps {
backgroundColor: string;
customHeaderRight?: ReactNode;
children: ReactNode;
title: string;
titleIcon?: string;
}
const WalletItem = (props: IProps) => {
const { backgroundColor, customHeaderRight, children, title, titleIcon } = props;
return (
<__Wrapper backgroundColor={backgroundColor}>
<__HeaderWrapper alignItems="center" justifyContent="space-between">
<__TitleWrapper alignItems="center">
{titleIcon && <__TitleIcon src={titleIcon} alt={title} />}
<span>{title}</span>
</__TitleWrapper>
<__CustomHeaderRightWrapper>{customHeaderRight}</__CustomHeaderRightWrapper>
</__HeaderWrapper>
<__ContentWrapper>{children}</__ContentWrapper>
</__Wrapper>
);
};
마이페이지의 지갑은 코인 지갑, nft 코인, nft 지갑 3 종류의 지갑이 한 페이지에 들어갔다. 세 컴포넌트가 동일한 UI를 공유하는 부분이 있었고, 지갑 안의 링크를 클릭하면 해당 코인/nft의 상세 정보 페이지로 이동했다. 세 지갑의 Wrapper가 동일했기에, 재사용할 수 있는 지갑 공용 컴포넌트를 만들었고, styled-components의 prop을 활용하여 지갑마다 다른 스타일링을 할 수 있도록 하였다.
<WalletItem
backgroundColor="linear-gradient(286.91deg, #7949F2 -0.72%, #8650C9 101.75%)"
title="Nft Coin Wallet"
titleIcon="https://s3.coinghost.com/cogo_mobile/image/wallet/wallet-pop-icon.svg"
customHeaderRight={
<__NftCoinGuide onClick={() => router.push("/guide/pop")}>
<Flex alignItems="center">
<p>Nft Coin 사용가이드</p>
<Image
src="https://s3.coinghost.com/cogo_mobile/image/arrows/arrow-white.svg"
width="0.9rem"
height="1.4rem"
/>
</Flex>
</__NftCoinGuide>
}
>
<__MyPopWrapper>
<p className="field">보유수량</p>
<__MyWalletCount>
<__MyPopBalance>
<NumberFormat
value={userWalletData?.lollipopCount}
decimalScale={2}
displayType="text"
suffix=" POP"
thousandSeparator
/>
</__MyPopBalance>
<p className="notice">(1POP = 1KRW)</p>
</__MyWalletCount>
</__MyPopWrapper>
<__FooterWrapper alignItems="center" justifyContent="center" background="#9067EB">
<__WalletButton width="220px" onClick={() => router.replace("/wallet/pop")}>
__Nft Coin 내역
</__WalletButton>
<__FooterDiv footerBorderColor="#B693E8" />
<__WalletButton width="187px" onClick={() => router.replace("/wallet/pop")}>
충전하기
</__WalletButton>
<__FooterDiv footerBorderColor="#B693E8" />
<__WalletButton width="255px" onClick={() => router.replace("/")}>
Coin으로 스왑하기
</__WalletButton>
</__FooterWrapper>
</WalletItem>
이처럼 재사용되었다.
- BMS 사이트 운영 관리자 페이지
메인 서비스와 nft 서비스를 운영팀이 관리할 수 있는 백오피스 페이지를 개발하였다. 총 24개의 페이지를 개발하였으며, 수많은 수정이 있었다. (내가 작성한 수정 관련 커밋이 50개 있었다.) 개발을 끝마치면 바로 운영팀에서 피드백이 돌아왔기에 수정할 일이 많았다. 다행히 사내에서만 사용되는 페이지이기에 사용자 경험에 대한 이해를 쌓아가며 빠르게 수정할 수 있었다. 만일 상용 페이지였다면 사용자 이탈이 많이 일어났을 것이다. 운영팀으로부터 요청받은 수정 사항들은 다음과 같았다.
사수분이 개발해주신 공용 모듈 컴포넌트를 사용하여 빠른 속도로 개발할 수 있었다. 중간에 이 모듈 컴포넌트를 수정할 일이 있었는데 까다로운 작업이었다. 모듈로 작성된 다른 페이지들이 영향받지 않도록 해야했다. 공용 컴포넌트를 개발할 때 훗날 어떤 기능이 추가될지 알 수 없기에 확장성을 고려하게 된 계기였다.
운영자 게시판 Create 페이지의 공용 모듈 컴포넌트
/* nft이미지 업로드를 위한 변수 */
const filterCategory = createFormData?.category;
const mintingScheduleFields = JSON.stringify(['닉네임', '토큰 심볼', '이름', '설명', '발행인 수수료 주소', '발행인 수수료', '플랫폼 수수료', '롤리팝 가격', '코인 가격', '수량', '참조 주소']);
const formFields = JSON.stringify(filterCategory.map((item) => item = item.input?.label).filter((item) => item !== undefined));
...
const handleNftImage = () => {
const contractName = getValues('contractName');
if (contractName) {
return contractName;
}
alert('체인을 입력해주세요');
return false;
};
const handleImageChange = (value: string) => async (e) => {
let nftUrl = '';
if (formFields === mintingScheduleFields) {
const validateNftUrl = handleNftImage();
if (!validateNftUrl) {
return;
}
nftUrl = validateNftUrl;
}
const image = e.target.files[0];
const formData = new FormData();
formData.append('image', image, image.name);
Axios.post(!nftUrl ? `${apis.base}/images` : `${apis.base}/images?serviceName=nft&collection=${nftUrl}`, formData).then((res) => {
setImage((prev) => ({...prev, [value]: res.data.url}));
setValue(value, res.data.id);
});
};
문제상황 : 기존 create 페이지 모듈 컴포넌트에선 게시물에 이미지를 업로드 하기 위해서 백엔드 서버에 이미지를 post 요청으로 먼저 저장한 뒤에 이미지 url을 리턴받았다. 하지만 nft 이미지를 업로드할 때는 백엔드에 다른 주소로 요청을 보내주어야 했다. 그러나 내가 개발한 모든 관리자 create 페이지는 이 공용 컴포넌트로 되어 있어기 때문에, 기존 이미지 업로드 url은 유지하면서 nft 이미지의 요청 url만 변경해주어야 했다. 또한 nft의 체인에 따라 다른 경로로 보내주어야 했기에, url은 동적으로 변경돼야했다.
해결 : props로 넘겨받는 테이블의 필드들이 nft create 페이지의 특정한 필드와 동일한지 비교하여 만일 동일하다면 업로드된 nft 이미지의 체인 이름을 반환하여 url에 넣어주는 것으로 해결했다. 이 방식을 통해 기존 이미지 업로드도 유지하면서, nft 체인에 따라 동적으로 이미지를 요청할 수 있게 해주었다. 또한 nft 체인을 먼저 입력해서 올바른 url로 이미지를 업로드할 수 있도록 alert를 이용했다.
1)
const swr = useSWR(`/otc/mintings/schedules/${params.id}`);
const {data: detailData, mutate} = swr;
mutate({...d...