보통 여느 어플리케이션이든, header의 우측 상단에 config나 계정 관련 기능이 들어가기 마련이다.
그래서 동영상의 업로드 기능을 header의 우측 상단에 추가해보자.
(한번 찍어 먹어보는 것이기 때문에 거창한 기능은 필요없다.)
Upload Button 추가하기
const UploadButton = styled.div`
width: 50px;
height: 25px;
background: url("/static/images/upload-icon.png") center / contain no-repeat;
cursor: pointer;
&:hover {
background-color: ${(props) => props.hoverColorStyle || "#ddd"};
}
`;
const Header = () => {
return (
<TopPanel>
<Link href="/">
<TopLogo />
</Link>
<TopRightPanel>
<UploadButton onClick={() => {}}></UploadButton>
</TopRightPanel>
</TopPanel>
);
};
UploadButton을 styled-components로 꾸며서 넣어준다. 이때 button이 hover가 되었을때 색상을 바꿔주던가 하는
동작들이 필요할 수 있는데, styled-components에서 pseudo class는
&:PseudoClass 로 지정하여 스타일링을 줄 수 있다.
Modal창 추가하기
보통 저런 업로드 버튼을 누르면 Modal창이 열리며 추가적 UI가 가운데 표시된다.
이 외에도 모달창은 각종 페이지에서 사용자 확인등의 이유로 자주 사용되기 때문에 이러한 컴포넌트를 만들어두면
유용하게 쓸 수 있다.
Modal창을 만드는 방법에는 여러 방법이 있겠지만, 여기서는 React Context를 통하여 "전역" Modal 창을 만들어보자.
이전에 단순 컴포넌트로 만들고 띄우는 페이지에서 state를 관리하게 하였더니 매우 불편해던 경험이 있어서,
Modal 컴포넌트가 알아서 state도 관리하게끔 React Context를 통해 제공하는 인터페이스가 더 좋다 판단하였다.
React Context?
컴포넌트간 prop drilling를 없애고 데이터를 공유할 수 있는 메커니즘이다.
이전에는 Redux를 통해 store에 저장하고 공유했지만 왠만한 것은 Context를 통해 해결할 수 있다.
( 사실 Redux를 안 써봐서 정확한 비교는 어렵다. )
React 특성상 페이지의 기능이 방대하고 거대해질 수록 Component가 많아지고 그 깊이가 길어질 것이다.
예를 들어 다음과 같은 구조라 했을때)
const nameBox = (props) => {
return (
<div>{props.userName}</div>
)
}
const login = (props) => {
return (
<nameBox userName={userName}></nameBox>
)
}
const userInfo = (props) => {
return (
<login userName={userName}></login>
)
}
const index = () => {
const userName = getUserNameFromServer();
return (
<div>
<div>{userName}</div>
<userInfo userName={userName}></userInfo>
</div>
);
};
index에서 서버로부터 가져온 userName 데이터가 nameBox component가 사용할때, 두번 요청하여 가져올
이유는 없기때문에 이미 가져온 데이터를 넘길 것이다. 근데 userName까지 이미 userInfo, login component가 있을때
사용도 안하는 component들이 불필요하게 props를 받았다가 넘겨야 한다. 이러한 것을 props를 뚫어서 넘긴다 하여
"props drilling"이라 하는데, React Context를 사용하면 불필요한 props drilling을 막을 수 있다.
Context를 사용하여 개선하면 다음과 같다.
// context 생성
const UserInfoContext = createContext({});
const nameBox = () => {
const userContext = useContext(UserInfoContext);
return (
//방법1) useContext() hook으로 UserInfoContext사용을 명시하고 사용
<div>{userContext.userName}</div>
//방법2) consumer 컴포넌트로 세팅된 'userName'을 소비한다
<UserInfoContext.Consumer>
<div>{userName}</div>
</UserInfoContext.Consumer>
);
};
const login = () => {
return <nameBox></nameBox>;
};
const userInfo = () => {
return <login></login>;
};
const index = () => {
const userName = getUserNameFromServer();
return (
// provider 컴포넌트로 하위 컴포넌트에 'userName'을 제공한다
<div>
<div>{userName}</div>
<UserInfoContext.Provider value={userName}>
<userInfo></userInfo>
</UserInfoContext.Provider>
</div>
);
};
이제 login, userInfo component는 property를 넘기지 않는다. React.createContext()를 통해 생성된 context에는
두 컴포넌트가 있는데, Provider와 Consumer이다. 하위 컴포넌트에 넘기기 원하는 값은 Provider의 "value"를 통해
제공한다. 제공되는 value를 사용하는 nameBox component는 2가지 방법이 있다.
[방법1] 은 useContext() 훅을 통해 넘겨받은 value로 사용하는 것이다.
[방법2] 는 Consumer를 통해 값을 받겠다 명시하고 사용하면 된다.
다시 모달 창으로 돌아와서, 먼저 Modal component를 만들자.
const ModalContainer = styled.div`
display: ${(props) => (props.modalState ? "block" : "none")};
position: fixed;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
z-index: 99;
background-color: rgba(0, 0, 0, 0.6);
`;
const ModalPopupContainer = styled.div`
position: fixed;
left: 50%;
top: 50%;
background-color: #ff8;
transform: translateX(-50%) translateY(-50%);
`;
const Modal = ({ state, content }) => {
return (
<ModalContainer modalState={state}>
<ModalPopupContainer>{content}</ModalPopupContainer>
</ModalContainer>
);
};
modal창은 팝업되면 항상 화면 전체를 덮어야 하기 때문에 fixed로 만들었다. state에 display를 보이게 할 지,
안보일지 결정하게 한다. 또한 내용물은 외부에서 주입하도록 content를 받았다가 가운데 content영역에 넣어준다.
다음은 이 modal을 전역적으로 사용하게할 context이다.
const ModalContext = createContext({});
const ModalContextProvider = ({ children }) => {
const [modalState, setModalState] = useState(false);
const [modalContent, setModalContent] = useState("");
return (
<ModalContext.Provider
value={{
open: () => {
setModalState(true);
},
close: () => {
setModalState(false);
},
setContent: setModalContent,
}}
>
{children}
<Modal state={modalState} content={modalContent}></Modal>
</ModalContext.Provider>
);
};
export const useModal = () => {
const context = useContext(ModalContext);
return context;
};
export default ModalContextProvider;
별도의 provider component를 만들어서 Context Provider로 넘겨진 children을 감싸는 형태로 만들었다.
그 아래 Modal component를 위치 시켜 항상 팝업 가능하게 한다.
Context Provider의 value로는 open, close, setContent로 Modal을 열리거나 닫히도록 state를 변경하거나,
Modal의 내용을 주입시키는 메소드 이다.
아래 외부 노출 (export) 용 Custom Hook이 있다. ( useModal ) 단순히 userContext()로 Context를 사용하게 하고
반환한다.
이로써, Modal을 전역으로 제공하기 위한 준비는 마쳤다.
Custom Hook?
React 진영에서 캡슐화된 함수를 "Hook"이라 부르는데 기본 제공 Hook( useState, useContext 등 )이 아닌
사용자 정의 Hook들은 모두 Custom Hook이라 부른다. 사실 function이라 부르지 않는데에는 많은 철학과
마케팅(?)이 담겨있는데, 공통적으로 "use" 라는 prefix로 시작하며, 내부에서 순수 Logic이 아닌, React 진영의
UI logic즉, component를 제어하고, react hooks를 사용하는 것을 알려 줄 수 있기 때문에 구분해서 부르는 것 같다.
(심지어 그 철학은 담아 고수시키는 Lint도 있다!!)
전역적으로 제공할 것이므로, _app.jsx파일에 방금 작성한 ModalContextProvider component로 감싸주자.
import Header from "../components/Header";
import ModalContextProvider from "../components/Modal";
function MyApp({ Component, pageProps }) {
return (
<div>
<ModalContextProvider>
<Header></Header>
<Component {...pageProps} />
</ModalContextProvider>
</div>
);
}
export default MyApp;
위에서 Header내 Upload Button에 대한 click 동작이 비어있는데, 이제 Modal을 띄우게 변경하자
//...
const Header = () => {
const modal = useModal();
const uploadVideoContent = <Modal_UploadVideo />;
return (
<TopPanel>
<Link href="/">
<TopLogo />
</Link>
<TopRightPanel>
<UploadButton
onClick={() => {
modal.setContent(uploadVideoContent);
modal.open();
}}
></UploadButton>
</TopRightPanel>
</TopPanel>
);
};
//...
useModal Hook으로 context를 가져온 뒤 onClick시 content를 세팅하고 open한다.
Modal의 upload content를 담당하는 component는 다음과 같이 간단하게 구성했다.
import React, { useRef, useState } from "react";
import styled from "styled-components";
import { useModal } from "./Modal";
//.. style component 생략
const Modal_UploadVideo = () => {
const modal = useModal();
const [videoName, setVideoName] = useState("");
const [videoDescription, setVideoDesc] = useState("");
return (
<ModalContentContainer>
<ContentHeader>비디오 업로드</ContentHeader>
<ColoredLine />
<ContentCenter>
<ContentCenterElement>
<StyledTextBox
type="text"
placeholder="파일 이름"
disabled />
<StyledLabel>
<StyledInputFile
accept=".mp4"
type="file" />
파일 선택
</StyledLabel>
</ContentCenterElement>
<ContentCenterElement>
<StyledTextBox
type="text"
placeholder="제목"
onChange={(event) => setVideoName(event.target.value)} />
</ContentCenterElement>
<ContentCenterElement>
<StyledTextBox
id="upload-description"
type="text"
placeholder="설명"
onChange={(event) => setVideoDesc(event.target.value)} />
</ContentCenterElement>
</ContentCenter>
<ColoredLine />
<ContentFooter>
<FooterRight>
<InlineMargin>
<ModalButton >업로드</ModalButton>
</InlineMargin>
<InlineMargin>
<ModalButton
backgroundColor="#cfcfcf"
hoverColor="#e0e0e0"
textColor="000"
onClick={() => {
modal.close();
}}
>
닫기
</ModalButton>
</InlineMargin>
</FooterRight>
</ContentFooter>
</ModalContentContainer>
);
};
export default Modal_UploadVideo;
좀 길어지기는 했지만, 중요한 것은 modal.close()이다. 닫기 버튼을 눌렀을때 modal.close()를 통해 간단하게
Modal창이 닫히도록 했다. ( 좀 더 예쁘게 하고 싶다면 keyframe 으로 연출을 해주면 된다. )
'프로그래밍 > Javascript' 카테고리의 다른 글
[NextJS찍먹] 스트리밍 사이트 만들기 - 2. header 추가 (Component 생서, styled-components, 정적 라우팅) (0) | 2022.01.06 |
---|---|
[NextJS찍먹] 스트리밍 사이트 만들기 - 1. 프로젝트 생성 (0) | 2021.12.06 |
2d skyline bin packing algorithm (0) | 2021.11.20 |
File System Access API로 browser에서 local file 수정하기 (0) | 2021.09.27 |