728x90

보통 여느 어플리케이션이든, 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  로 지정하여 스타일링을 줄 수 있다. 

[그림1] upload button 추가


Modal창 추가하기

보통 저런 업로드 버튼을 누르면 Modal창이 열리며 추가적 UI가 가운데 표시된다.

이 외에도 모달창은 각종 페이지에서 사용자 확인등의 이유로 자주 사용되기 때문에 이러한 컴포넌트를 만들어두면

유용하게 쓸 수 있다.

[그림2] modal창 예시

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 으로 연출을 해주면 된다. )

[영상1] modal 구현 확인

728x90
728x90

보통 유튜브 어느 페이지든 상관없이 항상 위쪽에 달려있는 것이 있다.

보통 그러한 유형의 UI를 Header라고 부른다. 명색의 스트리밍 사이트니까 우리도 추가해주자

[그림1] 유튜브 header


styled-components

이전 강좌에서 기본 styling 을 위한 디렉토리를 삭제했는데, 그 이유는 styled-components를 사용할 것이기 때문이다.

styled-copmonents는 javascript 라이브러리로 CSS-in-JS를 위한 라이브러리다. 별도의 css 파일을 작성하지 않고,

component 파일에 함께 작성 할 수 있기 때문에 파일이 많아졌을때 일일이 어떤 style인지 확인해야할 

수고를 덜어줄 수 있다.

대신 CSS때문에 파일 길이가 증가한다. 때문에 컴포넌트의 경계를 잘 설정하여 한 컴포넌트가

너무 비대해지지 않게 주의해야 한다.

설치는 간편하게 프로젝트 디렉토리에서 "yarn add styled-components"으로 모듈을 추가하면 된다.


컴포넌트 추가하기

react에서 컴포넌트 추가하는것과 동일하다 (애초에 react 위에서 돌아가니..) class 형보다는 짧게 작성 가능한

함수형 컴포넌트가 요새 트렌드 이기때문에 여기서도 그렇게 한다.

import styled from "styled-components";

const TopPanel = styled.div`
  display: flex;
  top: 0px;
  justify-content: space-between;
  width: 100%;
  height: 60px;
  background: #ffffff;
`;

const TopLogo = styled.div`
  height: 100%;
  width: 300px;
  background: url("/static/images/logo.png") no-repeat;
  background-size: 90% 70%;
  background-position: 10% 30%;
  box-sizing: border-box;
`;

const TopRightPanel = styled.div`
  height: 100%;
  margin: 10px;
`;

const Header = () => {
  return (
    <TopPanel>
      <TopLogo />
      <TopRightPanel></TopRightPanel>
    </TopPanel>
  );
};

export default Header;

애초에 전문 프론트엔드 개발자도 아니고 div로 퉁쳐서 만들었다.

styled-components의 사용법은 보다시피 간단하다!

1. 컴포넌트의 태그로 들어갈 이름을 변수명으로 한다.

2. styled.태그 뒤에 백틱( ` ) 으로 감싸고 그 안에 css style을 명시한다.

 

이렇게 하고 header 이기 때문에 _app.jsx 컴포넌트에다가 넣어준다.

import Header from "../components/Header";

function MyApp({ Component, pageProps }) {
  return (
    <div>
      <Header></Header>
      <Component {...pageProps} />
    </div>
  );
}

export default MyApp;

이제 모든 페이지는 header가 출력된다.

[그림2] index 페이지 모습


정적 라우팅

유튜브를 보면 로고 부분 클릭시 home( index ) 페이지로 이동한다. Next JS에서는 요러한 정적 페이지로 이동하는 것을

<Link> 태그를 이용하여 구현 할 수 있다.

import styled from "styled-components";
import Link from "next/link";

//...

const Header = () => {
  return (
    <TopPanel>
      <Link href="/">
        <TopLogo />
      </Link>
      <TopRightPanel></TopRightPanel>
    </TopPanel>
  );
};

export default Header;

위 코드처럼 Logo를 Link태그로 감싸면 자동으로 클릭을 바인딩하여 href="" 페이지로 이동시켜준다.

(그냥 / (index) 페이지로 해두면 이동 안하는 것처럼 보일 수 있으니 확인하고 싶으면 없는 페이지라도

써보면 확인 가능하다!)

로고 위로 올라오면 커서 모양도 바뀌게 css<a>태그로 적당히 묶어주자!

[영상1] 정적 라우팅 적용

 

728x90
728x90

NextJS? 

React 프레임워크로, 이미 웹 프레임워크인 React 위에 굳이 NextJS를 올리는 이유는

바로 SSR을 쉽게 지원하기 때문이다.


SSR?

SSR(Sever Side Rendering)은 CSR (Client Side Rendering)과는 대조적인 방식으로, 결국

브라우저가 해석하는 문서 (HTML)의 대부분이 이미 만들어져서 나온다.

원래 다 그런 것 아니야?? 할 수 있지만, 요새 대부분의 웹사이트에 접속하면 

데이터가 아직 없어서 아무것도 표시 못하다가 사용자별 데이터가 들어오면 그제서야 component를

만들어서 렌더링을 한다.

[그림1] 유튜브 접속했을때 아직 페이지 렌더링이 덜 된 모습

그래서 SSR을 사용하면 사용자 경험을 올릴 수 있는 장점이 있지만, 그만큼 서버의 연산 자원을 활용해야 하는

단점이 있다. 그래서 SSR과 CSR을 적절히 섞어서 써야 한다.


프로젝트 생성

[그림2-1] 프로젝트 생성

"npx create-next-app"을 하면 알아서 npm이 관련 dependencies를 함께 설치해준다.

그래서 react, react-dom, next가 함께 설치 된다.

[그림2-2] 설치 완료

설치된 디렉토리를 VSCode를 열어보면 다음과 같을 것이다.

[그림2-3] 기본 directory

pages : next js의 기본 라우팅을 해주는 디렉토리로 해당 디렉토리내 컴포넌트의 이름으로 자동으로 라우팅이 된다.

     ex) pages/board.js  =>  "xxx.myapp-domain.xx/board"  로 외부에서 접속 가능 

public : image나 영상등을 넣게 된다. 기본적으로 정적 리소스에 대한 참조( background-image 같은것들 )를

          이 디렉토리 내에서 한다.

styles : next js에서 제공하는 기본 styling 방법을 위한 디렉토리.

pages/api : 기본 api route를 지원받기 위한 디렉토리로, 이 녀석 덕분에 백엔드의 기능이 적은 

               규모가 작은 어플리케이션이라면, 별도 백엔드 없이 요거 하나로 충분하다.

_app.js : 좀 특별한 컴포넌트로 모든 페이지에 적용되는 컴포넌트다. 보통 공통의 레이아웃은 여기에 위치한다.

 

일단 컴포넌트는 _app.js, index.js를 제외하고 싹다 날리자. styles 디렉토리도 안쓸거니 날리자.

index.jsx의 내용들도 다 날리고 심플하게 "hello, nextjs"만 남긴 뒤 app을 구동시켜서 브라우저로 확인해보자.

"npm run dev" 혹은 "yarn run dev"로 실행 할 수 있다.

// index.jsx
export default function Home() {
  return <div>hello, next js</div>;
}

[그림2-4] 실행에 성공하여 브라우저에서 접속 한 모슴

이제 기본 프로젝트 설정은 다 끝났다.

728x90

+ Recent posts