프로젝트를 진행할 당시에는 코드가 지저분한지 어떤지 신경쓰지 않고 오로지 구현에만 신경을 썼다. 당시에는 일정이 빠듯하다 느꼈지만... 그냥 실력이 부족했었지 싶다.
그리고 그 결과 관심사의 분리, 리렌더링 신경 쓸 새 없이 엉망진창으로 작성했고, 하드한 유지보수성 + 필요없는 리렌더링이 엄청나게 발생하는 코드가 탄생했다.
한 파일에 컴포넌트를 두개 작성하고 각 컴포넌트가 무지하게 길다. 내부는 뭐...복잡해서 말할 것도 없고. 스타일 분리도 안해놔서 이 파일은 420줄의 코드를 가지고 있다. 유지보수를 전혀 고려하지 않음ㅋㅋ.
코드를 생각없이 작성하니 쓸데없는 리렌더링도 발생한다.
메뉴에 호버링하면 사이드 바 전체가 리렌더링 된다. 근데 이런 리렌더링이 다른 컴포넌트에도 한무더기 있다.
코드 상태가 심각하다는 점을 본능적으로 알고 있었다. 근데 당시 더 심각한 것은 내 실력이었고.
부트캠프를 끝내니 리액트를 "대충" 사용할 수는 있었는데, 파편적인 지식을 가지고 있으니 문제가 있어도 어디서부터 시작해야 할지, 어떻게 해야할지 감을 못잡겠더라. 때문에 추가적인 공부가 필요하다고 생각했다. 자바스크립트 독서 스터디부터 시작해 리액트 공식문서 번역과 강의를 듣고 나니 드디어 "하면서 모르겠다 싶은 부분들은 레퍼런스를 찾아보자" 라는 생각이 들었다. 물론 기간이 지나치게 길었던거 아닌가? 싶은데 중간에 힘 빠져서 설렁설렁 하기도 하고, 생계를 위해 일도 하다보니 이제서야 시작하게 되었다. 헤. 근데 일단 시작했잖아? 그 부분에 의의를 두기로 했다. 지나간걸 뭐 어떻게 해. 반복하지 않으면 되는거지.
페이지의 왼쪽에 있는 네비게이션을 담당하는 컴포넌트였다.
function LeftNav() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [hoverTarget, setHoverTarget] = useState('');
const icons = [
<HiOutlineEye key="1-icons" />,
<BiBone key="2-icons" />,
<Intestine key="3-icons" />,
<Liver key="4-icons" />,
<Brain key="5-icons" />,
<Skin key="6-icons" />,
<GrPowerCycle className="small bold-stroke" key="7-icons" />,
<AiOutlineThunderbolt key="8-icons" className="bolt" />,
<RiHeartAddLine className="heart-add" key="9-icons" />,
<AiOutlinePlusCircle className="small bold-stroke" key="10-icons" />,
];
const handleMenuOpen: React.MouseEventHandler<HTMLDivElement> =
useCallback(() => {
setIsMenuOpen(!isMenuOpen);
}, [isMenuOpen]);
// 호버 시 아이콘이 나오도록
const handleHover: React.MouseEventHandler<HTMLLIElement> = useCallback(
(e) => {
const { innerText } = e.target as HTMLLIElement;
setHoverTarget(innerText);
},
[],
);
// 마우스가 카테고리를 떠났을 때 hoverTarget 초기화
const handleLeave: React.MouseEventHandler<HTMLLIElement> =
useCallback(() => {
setHoverTarget('');
}, []);
return (
<Container>
<Nav>
<Link to="/">
<Logo />
</Link>
<Hamburger onClick={handleMenuOpen} isMenuOpen={isMenuOpen}>
<div />
<div />
<div />
</Hamburger>
{isMenuOpen && (
<CategoryContainer>
{CATEGORIES.map((el, i) => (
<Link
to={`/list?categoryName=${CATEGORIES[i]
.replaceAll(' ', '_')
.replaceAll('/', '_')}`}
key={`${i.toString()}-${el}`}
>
<ListContainer
onMouseEnter={handleHover}
onMouseLeave={handleLeave}
>
{hoverTarget === el && icons[i]}
<Category>{el}</Category>
</ListContainer>
</Link>
))}
</CategoryContainer>
)}
</Nav>
</Container>
);
}
이때 상태 isMenuOpen
과 hoverTarget
이 분리되지 않아 불필요한 렌더링이 계속해서 발생했다. 호버되지 않은 메뉴들과, x버튼, 로고가 렌더링이 되더라. 그게 이거였다.
다행히 코드가 그리 긴 편은 아니어서, 컴포넌트와 상태를 분리하는 것만으로도 해결할 수 있었다. 컴포넌트를 리팩터링 하면서, 하나의 컴포넌트는 하나의 일을 수행하는게 이상적이라는 원칙을 머릿속에 기억하고자 했다. 기존엔 LeftNav
가 메뉴를 렌더링하면서 호버링하면 메뉴 그림이 나오고 이동까지 시켜주는, LeftNav
"해줘" 식 컴포넌트였다.
이건 간단하게... 로고 밑에 있던 다른 컴포넌트들과 관련 로직들을 HamburgerMenu
라는 컴포넌트로 따로 분리했다.
// LeftNav.js
import { Link } from 'react-router-dom';
import { Logo } from '../../../assets/Icons';
import HamburgerMenu from './HamburgerMenu';
import { Container, Nav } from './style';
function LeftNav() {
return (
<Container>
<Nav>
<Link to="/">
<Logo />
</Link>
<HamburgerMenu />
</Nav>
</Container>
);
}
export default LeftNav;
밑에가 HamburgerMenu
// HamburgerMenu.js
export default function HamburgerMenu() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [hoverTarget, setHoverTarget] = useState('');
const icons = [
<HiOutlineEye key="1-icons" />,
<BiBone key="2-icons" />,
<Intestine key="3-icons" />,
<Liver key="4-icons" />,
<Brain key="5-icons" />,
<Skin key="6-icons" />,
<GrPowerCycle className="small bold-stroke" key="7-icons" />,
<AiOutlineThunderbolt key="8-icons" className="bolt" />,
<RiHeartAddLine className="heart-add" key="9-icons" />,
<AiOutlinePlusCircle className="small bold-stroke" key="10-icons" />,
];
const handleHover: React.MouseEventHandler<HTMLLIElement> = useCallback(
(e) => {
const { innerText } = e.target as HTMLLIElement;
setHoverTarget(innerText);
},
[],
);
const handleMenuOpen: React.MouseEventHandler<HTMLDivElement> =
useCallback(() => {
setIsMenuOpen(!isMenuOpen);
}, [isMenuOpen]);
const handleLeave: React.MouseEventHandler<HTMLLIElement> =
useCallback(() => {
setHoverTarget('');
}, []);
return (
<>
<Hamburger onClick={handleMenuOpen} isMenuOpen={isMenuOpen}>
<div />
<div />
<div />
</Hamburger>
{isMenuOpen && (
<CategoryContainer>
{CATEGORIES.map((el, i) => (
<Link
to={`/list?categoryName=${CATEGORIES[i]
.replaceAll(' ', '_')
.replaceAll('/', '_')}`}
key={`${i.toString()}-${el}`}
>
<ListContainer
onMouseEnter={handleHover}
onMouseLeave={handleLeave}
>
{hoverTarget === el && icons[i]}
<Category>{el}</Category>
</ListContainer>
</Link>
))}
</CategoryContainer>
)}
</>
);
}
초반이라 HamburgerMenu
도 복잡함. 그래도 이렇게 하니 로고 리렌더링을 막을 수 있더라.
휴 다행
이건 hoverTarget
이 분리가 안되어 있어서 그랬다. 그래서 hoverTarget
이 필요한 컴포넌트를 따로 분리해줬다. 우선 메뉴 리스트를 렌더링하는 일만 하는 컴포넌트를 구현했다.
// MenuCategory.js
export default function MenuCategory() {
return (
<CategoryContainer>
{CATEGORIES.map((el, i) => (
<Link
to={`/list?categoryName=${CATEGORIES[i]
.replaceAll(' ', '_')
.replaceAll('/', '_')}`}
key={`${i.toString()}-${el}`}
>
<LeftNavMenu el={el} i={i} />
</Link>
))}
</CategoryContainer>
);
}
<LeftNavMenu el={el} i={i} />
가 각 메뉴가 호버링 되었을 때의 로직을 담당한다.여기에서 hoverTraget을 관리하도록 했다. LeftNavMenu
는 el
, i
를 props로 받으며, hoverTarget
이 전달받은 el
과 같다면 LeftNavIcons
는 i
를 props
로 전달받아 렌더링한다. LeftNavIcons
는 props
로 받은 아이콘들의 배열 icons
의 i
번째 인덱스의 아이콘을 반환해 렌더링하는 컴포넌트이다.
export default function LeftNavMenu({ el, i }: { el: string; i: number }) {
const [hoverTarget, setHoverTarget] = useState('');
const handleHover: React.MouseEventHandler<HTMLLIElement> = useCallback(
(e) => {
const { innerText } = e.target as HTMLLIElement;
setHoverTarget(innerText);
},
[],
);
const handleLeave: React.MouseEventHandler<HTMLLIElement> =
useCallback(() => {
setHoverTarget('');
}, []);
return (
<ListContainer onMouseEnter={handleHover} onMouseLeave={handleLeave}>
{hoverTarget === el && <LeftNavIcons i={i} />}
<Category>{el}</Category>
</ListContainer>
);
}
import { AiOutlineThunderbolt, AiOutlinePlusCircle } from 'react-icons/ai';
import { BiBone } from 'react-icons/bi';
import { GrPowerCycle } from 'react-icons/gr';
import { HiOutlineEye } from 'react-icons/hi';
import { RiHeartAddLine } from 'react-icons/ri';
import { Intestine, Liver, Brain, Skin } from '../../../assets/Icons';
export default function LeftNavIcons({ i }: { i: number }) {
const icons = [
<HiOutlineEye key="1-icons" />,
<BiBone key="2-icons" />,
<Intestine key="3-icons" />,
<Liver key="4-icons" />,
<Brain key="5-icons" />,
<Skin key="6-icons" />,
<GrPowerCycle className="small bold-stroke" key="7-icons" />,
<AiOutlineThunderbolt key="8-icons" className="bolt" />,
<RiHeartAddLine className="heart-add" key="9-icons" />,
<AiOutlinePlusCircle className="small bold-stroke" key="10-icons" />,
];
return icons[i];
}
이렇게 해서 LeftNav
는 비교적 심플하게 마무리 지을 수 있었다.
그 전까지는 파일 안에 스타일이 들어있었다. 당시에는 이렇게 하면 나중에 해당 컴포넌트 스타일을 찾아서 수정하기 쉽겠지? 라고 생각했는데, 코드가 너무 길어지니까 오히려 가독성이 넘 떨어지더라... 그래서 이번 리팩터링을 진행하면서, 스타일 역시 분리하기로 했다.
다음은 기존 구조
바꾸고 난 후
이렇게 하니 오히려 가독성이 좋아지더라.. 파일 찾는 것도 쉽고. 이 중 style.ts
에 스타일을 저장했다.
import styled, { css, keyframes } from 'styled-components';
// HamburgerMenu.tsx
export const Hamburger = styled.div<{ isMenuOpen: boolean }>`
margin: 45px 0 20px 0;
cursor: pointer;
div {
width: 18px;
height: 3px;
margin: 3.5px 0;
background-color: var(--purple-200);
border-radius: 4px;
cursor: pointer;
transition: 0.5s;
}
${({ isMenuOpen }) =>
isMenuOpen &&
css`
> :first-child {
-webkit-transform: translateY(6.5px) rotate(-315deg);
transform: translateY(6.5px) rotate(-315deg);
}
> :nth-child(2) {
opacity: 0;
}
> :last-child {
-webkit-transform: translateY(-6.5px) rotate(315deg);
transform: translateY(-6.5px) rotate(315deg);
}
`}
`;
// LeftNav.tsx
export const Container = styled.div`
position: relative;
`;
이런 식으로 주석을 활용해서 해당 스타일이 필요한 컴포넌트를 적어놨는데, 전보다는 훨씬 나아졌다는 느낌을 받았다. 이렇게 정리하다보며 느낀건데, atomic 패턴 같은 것들도 찾아서 공부해보면 좋겠다 싶었다. 근데 지금 이 프로젝트 리팩터링 하면서 적용하기엔... 아예 프로젝트를 하나 더 시작하는 느낌이라, 그냥 다른 프로젝트 할 때 공부해서 적용하기로 ㅇㅇ
이번 컴포넌트는 간단해서 다행이었다. memo
를 쓸 필요 없이 컴포넌트를 분리하기만 하면 됐으니까. 좀 더 복잡한 컴포넌트를 건들 때는 물론 여러 내장 훅들을 사용하겠지만. 리액트 공식문서를 혼자 번역하면서 어느 정도 감이 잡힌 듯 해서 어어어어어엄청나게 걱정되지는 않는다. 물론 막상 닥치면 또 모르겠고 ㅋㅋ. 하면서 느낀건데, 내가 프로젝트 진행 당시 리액트에 관한 지식이 얼마나 없었는가~ 다시한번 생각하게 된다. 리팩터링을 진행하면서 좀 더 리액트에 능숙해지고, 프로그래밍적 사고력이 늘기를 바란다.
참고
https://react.dev/learn/thinking-in-react (컴포넌트 분리를 어떻게 해야하지? 할 때 참고했음)