국비 학원 파이널 프로젝트가 얼마 남지 않아 조바심이 나서인지, 시간이 나면 코드를 붙잡고 있느라 블로그에 정리하는 것을 미루곤 한다. 무려 열흘 동안 밀린 코드 업데이트 사항을 작성하면서, 그 사이에 공부한 것들을 복습해야겠다.
1. Main Page
const StyledPage = styled.div`
position: absolute;
width: 100%;
`;
const Main = () => {
const videoData = VideoData.videos;
return (
<StyledPage>
<Header />
<Navigator />
<ContentsBox key={videoData.videoNo} videoData={videoData} />
</StyledPage>
);
};
먼저 메인 페이지에서, position을 absolute로 설정하여 StyledPage를 만들고 이것으로 컴포넌트들을 감쌌다. Header 부분의 position이 absolute로 설정되어 있어서, 스크롤 바를덮어 버리기 때문에, body 아래에 요소를 추가하여 스크롤 바의 영역을 침범하지 않도록 한 것이다.
2. Main page - Header Components
const HeaderStyle = styled.header`
width: 100%;
height: 56px;
padding-left: 16px;
display: flex;
justify-content: space-between;
position: fixed;
z-index: 10;
top: 0px;
left: 0px;
background-color: white;
`;
const Header = () => {
return (
<HeaderStyle>
<HeaderLogo />
<HeaderSearch />
<HeaderBtns />
</HeaderStyle>
);
};
헤더의 구조는 위와 같다. 포지션은 원래 sticky로 설정했으나, sticky는 기본적으로는 relative 속성을 가지고 있다가 특정 지점에 도달하면 fixed 속성을 갖도록 바뀐다는 점을 알게 되어, 항상 최상단에 위치하도록 fixed로 수정했다.
3. HeaderComponents - HeaderLogo
const HeaderLogoWrap = styled.div`
width: 169px;
display: flex;
justify-content: flex-start;
align-items: center;
.visible {
width: 240px;
visibility: visible;
}
.invisible {
width: 0;
visibility: hidden;
}
`;
const HeaderLogo = () => {
const [willBeShown, setWillBeShown] = useState(false);
return (
<HeaderLogoWrap>
<HeaderLogoOpenMenu
willBeShown={willBeShown}
setWillBeShown={setWillBeShown}
/>
<HeaderLogoMainLogo />
<HeaderLogoHiddenMenu
willBeShown={willBeShown}
setWillBeShown={setWillBeShown}
/>
</HeaderLogoWrap>
);
};
HeaderLogo는 위와 같이 구성했다. 햄버거 모양의 HeaderLogoOpenMenu, 유튜브 아이콘 모양의 HeaderLogoMainLogo, 숨겨진 메뉴인 HeaderLogoHiddenMenu가 차례로 렌더링된다. 여기서 HeaderLogoHiddenMenu는 width가 0인 invisible 클래스를 가진 상태인데, state를 이용하여 HeaderLogoOpenMenu를 클릭하면 HeaderLogoHiddenMenu가 나타나도록 했다. 구체적으로는 다음과 같다.
먼저, willBeShown, setWillBeShown은 기본 false 상태로 HeaderLogoOpenMenu, HeaderLogoHiddenMenu로 전달된다.
const HeaderLogoHiddenMenu = ({ willBeShown, setWillBeShown }) => {
const [isSelected, setIsSelected] = useState(false);
const onClickLogo = () => {
setWillBeShown(!willBeShown);
};
return (
<>
<HiddenMenuWrap className={willBeShown ? "visible" : "invisible"}>
<HiddenMenuLogoWrap className={willBeShown ? "visible" : "invisible"}>
<HeaderLogoOpenMenu
willBeShown={willBeShown}
setWillBeShown={setWillBeShown}
/>
<HeaderLogoMainLogo />
</HiddenMenuLogoWrap>
<HiddenMenuGuideWrap>
<HiddenMenuGuideHome
isSelected={isSelected}
setIsSelected={setIsSelected}
/>
</HiddenMenuGuideWrap>
</HiddenMenuWrap>
<BlackArea
onClick={onClickLogo}
className={willBeShown ? "display__on" : "display__off"}
/>
</>
);
};
이 때, HeaderLogoHiddenMenu은, 위와 같이 willBeShown이 visible일 때만 화면에 width 값을 가지므로, willBeShown이 false인 상태에서는 렌더링되지 않는다.
const HeaderLogoOpenMenu = ({
willBeShown = false,
setWillBeShown = () => {},
}) => {
const onClickLogo = () => {
setWillBeShown(!willBeShown);
};
return (
<div className="globalIconBtn" onClick={onClickLogo}>
<i className="fas fa-bars"></i>
</div>
);
};
자식인 HeaderLogoOpenMenu는, 클릭 이벤트가 발생하면 전달된 프롭스인 willBeShown의 상태를 반대로(false -> true) 바꾸고, 이렇게 state가 바뀌면 부모 컴포넌트가 리렌더링된다. (디폴트 값은 false, {}로 설정하였다.)
부모 컴포넌트가 리렌더링될 때는, 이번에는 willBeShown이 true인 상태이므로, HeaderLogoHiddenMenu의 width가 240px인 visible 클래스를 갖게 된다. 여기에 transition-duration을 0.2초로 적용하면 커튼이 열리듯이 왼쪽의 히든 메뉴가 나타나게 된다.
HeaderLogoHiddenMenu에서도 HeaderLogoOpenMenu를 재활용하는데, 여기서도 마찬가지 원리로 클릭하면 willBeShown의 상태를 반대로(false -> true) 바꾸어 HeaderLogoHiddenMenu의 width를 0으로 바꾸고, 커튼이 닫히듣이 히든 메뉴를 화면에서 보이지 않게 만들 수 있다.
또한, 위와 같이 히든 메뉴가 나타나면 메인 화면은 선택되지 않도록 했다.
3. HeaderLogo - HeaderLogoHiddenMenu
const HiddenMenuWrap = styled.div`
position: fixed;
top: 0;
left: 0;
height: 100vh;
display: flex;
justify-content: flex-start;
align-items: center;
background-color: white;
transition-duration: 0.2s;
flex-direction: column;
z-index: 3;
.sup {
display: none;
}
`;
const BlackArea = styled.div`
position: absolute;
background-color: black;
left: 240px;
opacity: 0.3;
height: calc(100vh + 56px);
width: calc(100vw - 240px);
transition-duration: 0.2s;
z-index: 1;
`;
const HiddenMenuLogoWrap = styled.div`
height: 55.5px;
display: flex;
justify-content: flex-start;
align-items: center;
padding-left: 16px;
border-bottom: solid 1px hsl(0, 0%, 80%);
`;
const HiddenMenuGuideWrap = styled.div`
width: 100%;
height: calc(100% - 56px);
background-color: white;
`;
const HeaderLogoHiddenMenu = ({ willBeShown, setWillBeShown }) => {
const onClickLogo = () => {
setWillBeShown(!willBeShown);
};
return (
<HiddenMenuWrap className={willBeShown ? "visible" : "invisible"}>
<HiddenMenuLogoWrap className={willBeShown ? "visible" : "invisible"}>
<HeaderLogoOpenMenu
willBeShown={willBeShown}
setWillBeShown={setWillBeShown}
/>
<HeaderLogoMainLogo />
</HiddenMenuLogoWrap>
<HiddenMenuGuideWrap>
<HiddenMenuGuideHome />
</HiddenMenuGuideWrap>
<BlackArea
onClick={onClickLogo}
className={willBeShown ? "display__on" : "display__off"}
/>
</HiddenMenuWrap>
);
};
HeaderLogoHiddenMenu의 나머지 부분은 위와 같다. BlackArea는 히든 메뉴가 열리면 해당 메뉴를 제외한 나머지 화면을 채우기 위해 전체 화면 너비에서 메뉴의 너비인 240px을 빼고, 포지션을 absolute로 하여 부모인 히든 메뉴를 기준으로 left 240px를 주었다. 마찬가지로 클릭 시에는 willBeShown의 상태를 바꾸어 히든 메뉴가 닫히고, BlackArea 자신도 사라지도록 하였다. 그 외에는 순서대로 로고 부분과 가이드 부분을 렌더링하도록 했다.
const HiddenMenuGuideHomeWrap = styled.div`
width: 100%;
height: 100%;
overflow-y: scroll;
`;
const HiddenMenuGuideHome = () => {
const [isSelected, setIsSelected] = useState("Home");
return (
<HiddenMenuGuideHomeWrap>
<HiddenMenuHome isSelected={isSelected} setIsSelected={setIsSelected} />
<HiddenMenuPreference
isSelected={isSelected}
setIsSelected={setIsSelected}
/>
<HiddenMenuSubscription
isSelected={isSelected}
setIsSelected={setIsSelected}
/>
<HiddenMenuAboutGyutube
isSelected={isSelected}
setIsSelected={setIsSelected}
/>
<HiddenMenuSettings
isSelected={isSelected}
setIsSelected={setIsSelected}
/>
<HiddenMenuCopyRight />
</HiddenMenuGuideHomeWrap>
);
};
가이드 부분은 Home, Preference, Subscription, About, Settings, CopyRight 부분으로 다시 나누어진다. 이는 모두 비슷한 스타일을 가지고 있으므로, 글로벌 스타일을 적용하였다. 스타일드 컴포넌트로 더 간단하게 할 수 있을 것 같은데 아직 이해도가 부족하여 구현하지 못했다. 일단은 완성시키는 것이 우선이라고 생각하여 할 수 있는 레벨에서 클래스를 만들어 글로벌 스타일을 적용하였다. 또, 각 자식 컴포넌트가 클릭되면 isSelected의 값이 바뀌고, 이 값을 가진 컴포넌트가 붉은색으로 표시되도록 만들기 위해, isSelected State를 설정하여 각각의 자식 컴포넌트에게 프롭스로 전달하였다.
const HiddenMenuHomeWrap = styled.div`
border-bottom: solid 1px hsl(0, 0%, 80%);
padding: 12px 0;
`;
const HiddenMenuHome = ({ isSelected = "Home", setIsSelected = () => {} }) => {
const onClickHome = () => {
setIsSelected("Home");
};
const onClickSearch = () => {
setIsSelected("Search");
};
const onClickMySubscription = () => {
setIsSelected("MySubscription");
};
return (
<HiddenMenuHomeWrap>
<div
className={`${isSelected === "Home" ? "selected" : null} globalItemBar`}
onClick={onClickHome}
>
<div className="globalIconBtnRect globalIconWrap">
<i className="fas fa-home"></i>
</div>
<div className="globalTextBar">홈</div>
</div>
<div
className={`${
isSelected === "Search" ? "selected" : null
} globalItemBar`}
onClick={onClickSearch}
>
<div className="globalIconBtnRect globalIconWrap">
<i className="fas fa-compass"></i>
</div>
<div className="globalTextBar">탐색</div>
</div>
<div
className={`${
isSelected === "MySubscription" ? "selected" : null
} globalItemBar`}
onClick={onClickMySubscription}
>
<div className="globalIconBtnRect globalIconWrap">
<i className="fas fa-layer-group"></i>
</div>
<div className="globalTextBar">구독</div>
</div>
</HiddenMenuHomeWrap>
);
};
최하위 자식 컴포넌트인 Home, Preference, Subscription, About, Settings은 대체로 위와 같은 구조를 가지고 있다. 상술한 바와 같이, 클릭에 따라 부모로부터 전달받은 isSelected의 값을 변화시키고 이에 따라 아이콘과 글자 색을 빨갛게 바꾸는 selected 클래스를 가지게 될 것이다.
4. 현재 진행 상황
아직 정리는 다 못했지만, 실제 코드 작성은 상당히 진전이 있었다.
메인 페이지는 동영상 리스트의 반응형 레이아웃 및 각 버튼의 기능이 구현되지 않았다. 검색 등의 기능을 제대로 구현해 보려면, DB를 조금 더 채워 넣을 필요가 있을 것 같다.
한동안 빈 채로 있었던 동영상 오른쪽의 비디오 리스트는 오늘 채워 넣었다. 비디오 타이틀 등을 입력할 때 고생할 줄 알았는데 컴포넌트를 그대로 불러오니 금방 완성되었다.
꾸준히 기능도 추가해서 완성하는것도 중요하지만 그때그때 알게 된 점을 정리해야 할 필요도 느껴진다. 기록하는 습관을 잊지 말아야겠다.
[Clone YouTube]Global Style (0) | 2021.08.19 |
---|---|
[Clone YouTube] HiddenMenu(infinite recursion) (0) | 2021.08.16 |
[Clone YouTube] React Router (0) | 2021.08.15 |
[Clone YouTube] Contents (0) | 2021.08.13 |
[Clone YouTube] Header & Navigator (0) | 2021.08.07 |
댓글 영역