ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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>
      );
    }

     

     

     

     

    참고 사이트

     

    [react] useEffect 훅에서 async await 함수 사용하기

    평소에 useEffect 안에서 async await 함수를 쓰고 싶었던 적이 많았는데 매번 에러가 나길래 안되는건줄 알았다. 근데 요즘 공부하고 있는 저자 이재승 님의 실전 리액트 프로그래밍 개정판 (리액트

    velog.io

     

     

    [React] 부수효과를 처리하는 UseEffect

    UseEffect를 잘 이해해보자.

    velog.io

     

Designed by Tistory.