-
[Portfolly] 코드 Depth를 최대한 줄여보자Studying/Proj 과정 2023. 7. 27. 05:30
문제 상황
Portfolly 홈페이지의 '포트폴리오 상세보기 페이지' 컴포넌트인 PortfolioDetail.tsx 파일의 Depth가 너무 깊고 복잡해 보였습니다.
코치님께 말씀 드리니 일단 무조건 리팩터링이 필요해 보인다고..!! 당연한 말입니다.
팁을 하나 주셨는데, 아예 1 Depth 그 이상은 안 된다! 라는 극한의 조건을 스스로 걸고 코드를 작성하는 게 도움이 된다고 하셨습니다.
막상 코드를 작성할 땐 몰랐는데, 한 발짝 뒤로 물러나서 보니까 고쳐야 할 부분들이 눈에 들어옵니다. 그 과정을 기록해보려 합니다.
해결 과정
창피하지만 처음 작성한 코드입니다.
export default function PortfolioDetail() { const [isModalOpen, setIsModalOpen] = useState<boolean>(false); const [portfolio, setPortfolio] = useState<Portfolio>(); const [createdAt, setCreatedAt] = useState<string>(''); const [member, setMember] = useState<Member>(); const [sanitize, setElementInlineStyle] = useChangeHtmlContent(); const { portfolio_id: portfolioId } = useParams(); const navigate = useNavigate(); const deletePortfolio = () => call(`/portfolios/${portfolioId}`, 'DELETE'); const getPortfolio = () => call(`/portfolios/${portfolioId}`, 'GET'); const onEditButtonClick = () => window.location.href = `/portfolio/edit?portfolioId=${portfolioId}`; const openDeleteModal = () => setIsModalOpen(!isModalOpen); const deletePortfolioHandler = () => { deletePortfolio(); navigate('/main'); }; useEffect(() => { getPortfolio().then((res) => { console.log(res.data); console.log(res.data.content); setPortfolio(res.data); setMember(res.data.member); setCreatedAt(changeDateFormat(res.data.createdAt)); }); }, []); return ( <FlexColumnContainer bg="rgba(16, 16, 21, 1)"> <ButtonHeader> <BsArrowReturnLeft size={30} color="white" className="cursor-pointer" onClick={() => navigate(-1)} /> </ButtonHeader> <ContentContainer> <PortfolioContainer> {portfolio && ( <div dangerouslySetInnerHTML={{ __html: sanitize(setElementInlineStyle(portfolio.content)), }} ></div> )} </PortfolioContainer> <UserContainer> <UserCard> <FlexBetweenWrapper> {portfolio && ( <> <LikeButton portfolioId={portfolio.id} currentLikes={portfolio.countLikes} isToggled={portfolio.liked} /> <FlexWrapper gap={20}> <SmallText color="white">views · {portfolio.view}</SmallText> <Bookmark portfolioId={portfolio.id} isToggled={portfolio.marked} /> </FlexWrapper> </> )} </FlexBetweenWrapper> {member && <MemberProfile type="portfolio" member={member} />} <Center> <AskCommisionBtn>의뢰 요청</AskCommisionBtn> </Center> {portfolio && ( <> <HeadingText color="white">{portfolio.title}</HeadingText> <SmallText color="white">{createdAt}</SmallText> <BodyText color="white">{portfolio.explains}</BodyText> </> )} {portfolio?.writer && ( <FlexEndWrapper> <EditButton onClick={onEditButtonClick} /> <DeleteButton onClick={openDeleteModal} /> </FlexEndWrapper> )} </UserCard> <UserCard> <LabelText color="white">Tags</LabelText> <FlexWrapper gap={8}> {portfolio && portfolio.portfolioTags.map((tag: Tag) => <PortfolioTag tag={tag} key={tag.id} readOnly={true} />)} </FlexWrapper> </UserCard> </UserContainer> </ContentContainer> {isModalOpen && <DeleteModal onConfirm={deletePortfolioHandler} onCancel={openDeleteModal} />} </FlexColumnContainer> ); }
Early Return Pattern
가장 먼저 수정하고싶은 부분은 비동기 데이터 portfolio를 가져오지 못했을 때를 대비해 조건부 렌더링한 부분입니다.
매번 portfolio && 연산자를 감싸니 depth가 깊어질 수밖에 없었습니다.
export default function PortfolioDetail() { const [isModalOpen, setIsModalOpen] = useState<boolean>(false); const [portfolio, setPortfolio] = useState<Portfolio>(); const [createdAt, setCreatedAt] = useState<string>(''); const [member, setMember] = useState<Member>(); const [sanitize, setElementInlineStyle] = useChangeHtmlContent(); const { portfolio_id: portfolioId } = useParams(); const navigate = useNavigate(); const deletePortfolio = () => call(`/portfolios/${portfolioId}`, 'DELETE'); const getPortfolio = () => call(`/portfolios/${portfolioId}`, 'GET'); const onEditButtonClick = () => window.location.href = `/portfolio/edit?portfolioId=${portfolioId}`; const openDeleteModal = () => setIsModalOpen(!isModalOpen); const deletePortfolioHandler = () => { deletePortfolio(); navigate('/main'); }; useEffect(() => { getPortfolio().then((res) => { console.log(res.data); console.log(res.data.content); setPortfolio(res.data); setMember(res.data.member); setCreatedAt(changeDateFormat(res.data.createdAt)); }); }, []); if(!portfolio || !member) return null; return ( <FlexColumnContainer bg="rgba(16, 16, 21, 1)"> <ButtonHeader> <BsArrowReturnLeft size={30} color="white" className="cursor-pointer" onClick={() => navigate(-1)} /> </ButtonHeader> <ContentContainer> <PortfolioContainer> <div dangerouslySetInnerHTML={{ __html: sanitize(setElementInlineStyle(portfolio.content)), }} ></div> </PortfolioContainer> <UserContainer> <UserCard> <FlexBetweenWrapper> <LikeButton portfolioId={portfolio.id} currentLikes={portfolio.countLikes} isToggled={portfolio.liked} /> <FlexWrapper gap={20}> <SmallText color="white"> views · {portfolio.view} </SmallText> <Bookmark portfolioId={portfolio.id} isToggled={portfolio.marked} /> </FlexWrapper> </FlexBetweenWrapper> <MemberProfile type="portfolio" member={member} /> <Center> <AskCommisionBtn>의뢰 요청</AskCommisionBtn> </Center> <HeadingText color="white">{portfolio.title}</HeadingText> <SmallText color="white">{createdAt}</SmallText> <BodyText color="white">{portfolio.explains}</BodyText> {portfolio.writer && <FlexEndWrapper> <EditButton onClick={onEditButtonClick} /> <DeleteButton onClick={openDeleteModal} /> </FlexEndWrapper> } </UserCard> <UserCard> <LabelText color="white">Tags</LabelText> <FlexWrapper gap={8}> {portfolio.portfolioTags.map((tag: Tag) => { return <PortfolioTag tag={tag} key={tag.id} readOnly={true} /> )} </FlexWrapper> </UserCard> </UserContainer> </ContentContainer> {isModalOpen && <DeleteModal onConfirm={deletePortfolioHandler} onCancel={openDeleteModal} /> } </FlexColumnContainer> ); }
벌써 뭔가 깔끔해진 것 같은 느낌이 듭니다.
then 대신 async await 쓰기
getPortfolio() 함수는 비동기 API 요청을 하는 Promise 반환 함수입니다.
.then() 보다는 가급적 async await을 사용하는 게 훨씬 가독성이 좋은데 저때 왜 then을 사용했는지 모르겠습니다.
일단 수정 전 모습입니다.
// 수정 전 useEffect(() => { getPortfolio().then((res) => { console.log(res.data); console.log(res.data.content); setPortfolio(res.data); setMember(res.data.member); setCreatedAt(changeDateFormat(res.data.createdAt)); }); }, []);
그리고 async await 으로 수정 후 모습입니다.
// 수정 후 useEffect(() => { async function fetchPortfolio() { const portfolio = await getPortfolio(); setPortfolio(portfolio); setMember(portfolio.member); setCreatedAt(changeDateFormat(portfolio.createdAt)); } fetchPortfolio(); }, []);
그런데 주의할 점이 있다면 useEffect 훅의 첫번째 인자에 들어가는 콜백 함수는 async await을 적용할 수 없습니다.
왜냐하면 useEffect란 사이드 이팩트(Side Effect: 부수 효과)를 다루는 훅이고,
그래서 첫 번째 인자로 부수 효과 함수를 받습니다.
사이드 이팩트 함수는 오직 함수를 반환해야 하는데, async 함수는 프로미스 객체를 반환하므로 더이상 사이드 이팩트 함수라고 할 수 없는 겁니다.
그래서 useEffect 내부에서 async await을 사용하려면 useEffect 내부에 async 함수를 선언하고 호출하는 방식으로 사용해야 합니다.
이제와 기억나는 게, 처음에 then()을 사용한 이유가 아마 async 함수를 첫 번째 인자로 전달했다가 에러가 나는 걸 보고 왜 에러가 나는지 확인도 안 한 채 아 안되는구나 하고 then()을 쓴 것 같습니다. 한심...
그나마 개선된 코드
아직 완벽하다곤 할 수 없지만... 처음에 비하면 많이 나아진 것 같습니다.
여기서 더 깔끔하게 고칠 수 있으면 좋겠지만 지금 혼자 힘으로 하기엔 까막눈이라, 다른 코드를 많이 살펴보고 깨달음을 얻은 후에 차차 리팩터링 할 수 있으면 좋겠습니다.
export default function PortfolioDetail() { const [isModalOpen, setIsModalOpen] = useState<boolean>(false); const [portfolio, setPortfolio] = useState<Portfolio>(); const [createdAt, setCreatedAt] = useState<string>(''); const [member, setMember] = useState<Member>(); const [sanitize, setElementInlineStyle] = useChangeHtmlContent(); const { portfolio_id: portfolioId } = useParams(); const navigate = useNavigate(); const deletePortfolio = () => call(`/portfolios/${portfolioId}`, 'DELETE'); const getPortfolio = () => call(`/portfolios/${portfolioId}`, 'GET'); const onEditButtonClick = () => { navigate(`/portfolio/edit?portfolioId=${portfolioId}`); }; const openDeleteModal = () => { setIsModalOpen(!isModalOpen); } const deletePortfolioHandler = () => { deletePortfolio(); navigate('/main'); }; useEffect(() => { async function fetchPortfolio() { const portfolio = await getPortfolio(); setPortfolio(portfolio); setMember(portfolio.member); setCreatedAt(changeDateFormat(portfolio.createdAt)); } fetchPortfolio(); }, []); if(!portfolio || !member) return null; return ( <FlexColumnContainer bg="rgba(16, 16, 21, 1)"> <ButtonHeader> <BsArrowReturnLeft size={30} color="white" className="cursor-pointer" onClick={() => navigate(-1)} /> </ButtonHeader> <ContentContainer> <PortfolioContainer> <div dangerouslySetInnerHTML={{ __html: sanitize(setElementInlineStyle(portfolio.content)), }} ></div> </PortfolioContainer> <UserContainer> <UserCard> <FlexBetweenWrapper> <LikeButton portfolioId={portfolio.id} currentLikes={portfolio.countLikes} isToggled={portfolio.liked} /> <FlexWrapper gap={20}> <SmallText color="white"> views · {portfolio.view} </SmallText> <Bookmark portfolioId={portfolio.id} isToggled={portfolio.marked} /> </FlexWrapper> </FlexBetweenWrapper> <MemberProfile type="portfolio" member={member} /> <Center> <AskCommisionBtn>의뢰 요청</AskCommisionBtn> </Center> <HeadingText color="white">{portfolio.title}</HeadingText> <SmallText color="white">{createdAt}</SmallText> <BodyText color="white">{portfolio.explains}</BodyText> {portfolio.writer && <FlexEndWrapper> <EditButton onClick={onEditButtonClick} /> <DeleteButton onClick={openDeleteModal} /> </FlexEndWrapper> } </UserCard> <UserCard> <LabelText color="white">Tags</LabelText> <FlexWrapper gap={8}> {portfolio.portfolioTags.map((tag: Tag) => { return <PortfolioTag tag={tag} key={tag.id} readOnly={true} /> )} </FlexWrapper> </UserCard> </UserContainer> </ContentContainer> {isModalOpen && <DeleteModal onConfirm={deletePortfolioHandler} onCancel={openDeleteModal} /> } </FlexColumnContainer> ); }
참고 사이트
'Studying > Proj 과정' 카테고리의 다른 글
[Portfolly] 메인 페이지 Lighthouse 분석 (0) 2023.08.01 [Portfolly] 프로젝트를 마치며 (0) 2023.07.27 [Portfolly] KISS 원칙으로 코드를 바꿔보자 (0) 2023.07.27 [Portfolly] Five Lines of Code 규칙을 (최대한) 지켜보자 (0) 2023.07.27 [Portfolly] 스프린트 3주차 회고 (0) 2023.07.23