넷플릭스 클론 1. 정답 with CSS in JS

February 15, 2021


클론. 프론트엔드 개발의 구조를 배우고 싶다. 타인의 정답으로 나의 정답을 찾는다. 기록보다 실행.

styled-components부터 시작한다. 사용해보니 위에서 아래로 흐르는 React의 데이터 흐름과 연관이 깊다.


package.json

"dependencies": {
  "@testing-library/jest-dom": "^5.11.4",
  "@testing-library/react": "^11.1.0",
  "@testing-library/user-event": "^12.1.10",
  "firebase": "^8.2.6",
  "fuse.js": "^6.4.6",
  "normalize.css": "^8.0.1",
  "react": "^17.0.1",
  "react-dom": "^17.0.1",
  "react-router-dom": "^5.2.0",
  "react-scripts": "4.0.2",
  "styled-components": "^5.2.1",
  "web-vitals": "^1.0.1"
}

normalize.css는 브라우저 스타일 간 차이점을 정규화한다. 리셋과는 다르게 유용한 기본값은 유지하고 최신 표준에 맞는 일관성을 제공한다. 크로스 브라우징에 유용할 것 같다.

styled-components는 자바스크립트를 이용하여 리액트 컴포넌트를 스타일링(CSS in JS) 한다. function, props 등으로 동적인 스타일링이 가능하다.

CSS

위에 두 개의 패키지로 전역 스타일을 핸들링한다. normalize.css는 그냥 import 해주면 되고 styled-components는 자식 속성을 허용하지 않는 헬퍼 함수 createGlobalStyle을 이용한다. 둘 다 적용하면 헬퍼 함수의 속성이 normalize.css의 속성을 덮어쓴다.

둘 다 하나의 HTML 페이지의 고유한 스타일을 추가하는 Internal CSS로써 <head> 요소의 자식으로 <style> 태그 내부에 정의된다.

index.js

import React from 'react'
import { render } from 'react-dom'
import 'normalize.css'
import App from './app'
import { GlobalStyles } from './global-styles'
import { FirebaseContext } from './context/firebase'
import { firebase } from './lib/firebase.prod'

render(
  <>
    <FirebaseContext.Provider value={{ firebase }}>
      <GlobalStyles />
      <App />
    </FirebaseContext.Provider>
  </>,
  document.getElementById('root')
)

global-styles.js

import { createGlobalStyle } from 'styled-components';

export const GlobalStyles = createGlobalStyle`
  html, body {
    font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -mos-osx-font-smoothing: grayscale;
    ...
  }
`

Components

conponents 폴더를 만들고 새로운 컴포넌트가 추가될 때마다 하위 폴더를 만든다. 그리고 각 컴포넌트마다 JS와 CSS 코드를 index.jsstyles로 분리해서 작성한다. 다음 흐름을 따르는데 코드 작성은 순서를 거꾸로 하는 것도 괜찮은 것 같다.

  1. app.js에서 라우팅한다.
  2. containers/<conponent_name>.js에서 컴포넌트를 배치한다.
  3. components/<conponent_name>/index.js에서 컴포넌트를 정의한다.
  4. components/<conponent_name>/styles/<conponent_name>.js에서 컴포넌트를 스타일링한다.
  5. components/index.js에서 default 함수를 export 한다.

Jumbotron

점보트론은 대형 스크린을 말한다.

jumbotron

  • 가장 외부 Container
  • 각 내부 Item, Inner마다 2개의 Pane(left, right)
  • Pane마다 TitleSubTitle, Image

CSS in JS을 이용해서 만드는 첫 컴포넌트이고 오랜만에 다루는 리액트의 흐름도 낯설기 때문에 데이터의 흐름대로 코드를 충분히 기록한다.

app.js

import React from 'react'
import { JumbotronContainer } from './containers/jumbotron'

export default function App() {
  return (
    <JumbotronContainer />
  )
}

containers/jumbotron.js

jumbotron 컨테이너부터 마지막 스타일링 단계까지 props를 이용해서 direction 값을 전달한다.

import React from 'react'
import jumboData from '../fixtures/jumbo.json'
import { Jumbotron } from '../components'

export function JumbotronContainer() {
  return (
    <Jumbotron.Container>
      {jumboData.map(item => (
        <Jumbotron key={item.id} direction={item.direction}>
          <Jumbotron.Pane>
            <Jumbotron.Title>{item.title}</Jumbotron.Title>
            <Jumbotron.SubTitle>{item.subTitle}</Jumbotron.SubTitle>
          </Jumbotron.Pane>
          <Jumbotron.Pane>
            <Jumbotron.Image src={item.image} alt={item.alt} />
          </Jumbotron.Pane>
        </Jumbotron>
      ))}
    </Jumbotron.Container >
  )
}

components/jumbotron/index.js

Jumbotron을 구성하는 작은 요소들을 메소드로 작성했다. 때문에 Jumbotron 함수만 export default 해주면 된다. 여기에 보이는 props들은 CSS를 향한다.

import React from 'react'
import { Item, Inner, Container, Pane, Title, SubTitle, Image } from './styles/jumbotron'

export default function Jumbotron({ children, direction = 'row', ...restProps }) {
  return (
    <Item>
      <Inner direction={direction}>
        {children}
      </Inner>
    </Item>
  )
}

Jumbotron.Container = function JumbotronContainer({ children, ...restProps }) {
  return <Container {...restProps}>{children}</Container>
}

Jumbotron.Pane = function JumbotronPane({ children, ...restProps }) {
  return <Pane {...restProps}>{children}</Pane>
}

Jumbotron.Title = function JumbotronTitle({ children, ...restProps }) {
  return <Title {...restProps}>{children}</Title>
}

Jumbotron.SubTitle = function JumbotronSubTitle({ children, ...restProps }) {
  return <SubTitle {...restProps}>{children}</SubTitle>
}

Jumbotron.Image = function JumbotronImage({ ...restProps }) {
  return <Image {...restProps}></Image>
}

components/jumbotron/styles/jumbotron.js

styled-components/macroimport 해주면 ClassName이 더 직관적이다. styled-components가 아니었다면 flex-direction 프로퍼티 하나 때문에 코드가 더 복잡해졌을 거다. props는 템플릿 리터럴의 표현식(${expression})을 이용해서 접근한다.

import styled from 'styled-components/macro'

export const Item = styled.div`...
`

export const Inner = styled.div`
  display: flex;
  align-items: center;
  flex-direction: ${({ direction }) => direction};
  justify-content: space-between;
  max-width: 1100px;
  margin: auto;
  width: 100%;

  @media (max-width: 1000px) {
    flex-direction: column;
  }
`
export const Container = styled.div`
  @media (max-width: 1000px) {
    ${Item}: last-of-type h2 {
      margin-bottom: 50px
    }
  }
`

export const Pane = styled.div`...
`

export const Title = styled.h1`...
`

export const SubTitle = styled.h2`...
`

export const Image = styled.img`...
`

components/index.js

export { default as Jumbotron } from './jumbotron'

Written by Soomin 호기심이라는 우주선을 타고 떠나는 여행. 이 곳은 모험을 기록하는 우주정거장. Soomin Space Station