- 이글은 Karl hadwen React Netflix 를 분석한 글입니다.
.
├── LICENSE.txt
├── README.md
├── build
│ ├── asset-manifest.json
│ ├── favicon.ico
│ ├── images
│ ├── index.html
│ ├── precache-manifest.d31c015ae155da94bbd347a607830636.js
│ ├── service-worker.js
│ ├── static
│ └── videos
├── netflix-preview.png
├── node_moduls
├── package.json
├── public
│ ├── favicon.ico
│ ├── images
│ ├── index.html
│ └── videos
├── src
│ ├── __tests__
│ ├── app.js
│ ├── components
│ ├── constants
│ ├── containers
│ ├── context
│ ├── fixtures
│ ├── global-styles.js
│ ├── helpers
│ ├── hooks
│ ├── index.js
│ ├── lib
│ ├── logo.svg
│ ├── pages
│ ├── seed.js
│ └── utils
├── tree.txt
└── yarn.lock
1103 directories, 18 files
node_modules 를 포함한 폴더 구조는 이렇습니다.
index.js 파일 구성
import React from 'react';
import { render } from 'react-dom';
import 'normalize.css';
import { GlobalStyles } from './global-styles';
import { App } from './app';
import { firebase } from './lib/firebase.prod';
import { FirebaseContext } from './context/firebase';
render(
<React.StrictMode>
<FirebaseContext.Provider value={{ firebase }}>
<GlobalStyles />
<App />
</FirebaseContext.Provider>
</React.StrictMode>,
document.getElementById('root')
);
render 로 전달하고 개발시 코드가 많아지기 떄문에 문재를 catch 하기위해 StrictMode를 사용했습니다.
로그인 구현을 위해 Firebase 인증을 구현하기 위해 FirebaseContext를 불러오고 value값에 firebase를 넣었습니다.
// 9.0.0 최신버전부터는 compat에서 불러오셔야 합니다.
// this 9.0.0 version latest == src is compat
import Firebase from 'firebase/compat/app';
import 'firebase/compat/firestore';
import 'firebase/compat/auth';
// firbase 데이터 베이스 시드
// import { seedDatabase } from '../seed';
// config firebase 데이터 정보 id 는 본인 프로젝트의 api_key들을 복사해서 사용하시길 바랍니다.
const config = {
apiKey: process.env.REACT_APP_API_KEY,
authDomain:process.env.REACT_APP_AUTHDOMAIN_API_KEY,
projectId: process.env.REACT_APP_PROJECT_ID,
storageBucket: process.env.REACT_APP_STORAGE_BUKET,
messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_APP_ID,
measurementId: process.env.REACT_APP_MEASUREMENTID
};
const firebase = Firebase.initializeApp(config);
const auth = firebase.auth();
export { firebase, auth };
/* ------------------------------------------------------ 앱 초기화 부분 끝 ----------------------------------------------------*/
firebase에서 생성한 프로젝트 정보들을 불러와주는 파일입니다. config안에 나의 firebase정보를 넣어주었습니다.
.env
SKIP_PREFLIGHT_CHECK=true
REACT_APP_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
REACT_APP_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
REACT_APP_AUTHDOMAIN_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
REACT_APP_PROJECT_ID="xxxxxxxxxxxxxxxxxxxxxxxxxx"
REACT_APP_STORAGE_BUKET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
REACT_APP_MESSAGING_SENDER_ID="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
REACT_APP_APP_ID="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
REACT_APP_MEASUREMENTID="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
.env 로 변수를 주어서 firebase 키를 이안에 넣고 firebase.prod.js에 내가 설정한 변수들을 기입해주었습니다.
# API_KEY
.env
*.md
github에 push 할시 .env 에 담아둔 API_KEY 노출을 막기위해 .env 파일을 제외 했습니다.
src -> app.js
import React from 'react';
import { BrowserRouter as Router, Switch } from 'react-router-dom';
import { Home, Browse, SignIn, SignUp } from './pages';
import * as ROUTES from './constants/routes';
import { IsUserRedirect, ProtectedRoute } from './helpers/routes';
import { useAuthListener } from './hooks';
export function App() {
const { user } = useAuthListener();
return (
<Router>
<Switch>
<IsUserRedirect user={user} loggedInPath={ROUTES.BROWSE} path={ROUTES.SIGN_IN}>
<SignIn />
</IsUserRedirect>
<IsUserRedirect user={user} loggedInPath={ROUTES.BROWSE} path={ROUTES.SIGN_UP}>
<SignUp />
</IsUserRedirect>
<ProtectedRoute user={user} path={ROUTES.BROWSE}>
<Browse />
</ProtectedRoute>
<IsUserRedirect user={user} loggedInPath={ROUTES.BROWSE} path={ROUTES.HOME}>
<Home />
</IsUserRedirect>
</Switch>
</Router>
);
}
app.js 메인 파일입니다. BrowseRouter를 불러와 서 라우팅 시켜주고 비로그인시 IsUserRedicet파일을 생성해서
로그인 하지않을 경우에 접속되도록 설정했습니다. 영화목록이 보이는 Browse 파일에는 ProtectedRoute를 써서
로그인 시 에만 접속가능하도록 설정했습니다. Redirect에 user변수를 주어서 로그인 기능을 구현하도록 설정했습니다.
처음 접속시 비로그인이라면 isUserRedirect 가 처음 선언된 부분부터 진행됩니다. 조건이 만족될때마다 path 값이 바뀌며
조건이 만족될때 ProtectRoute 가 적용됩니다.
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
export function IsUserRedirect({ user, loggedInPath, children, ...rest }) {
return (
<Route
{...rest}
render={() => {
if (!user) {
return children;
}
if (user) {
return (
<Redirect
to={{
pathname: loggedInPath,
}}
/>
);
}
return null;
}}
/>
);
}
export function ProtectedRoute({ user, children, ...rest }) {
return (
<Route
{...rest}
render={({ location }) => {
if (user) {
return children;
}
if (!user) {
return (
<Redirect
to={{
pathname: 'signin',
state: { from: location },
}}
/>
);
}
return null;
}}
/>
);
}
IsUserReidrect는 비로그인 된 상태에서만 접속하게 만드는 변수를 주었습니다.
로그인 상태가 이닐 경우 IsUserRedirect에 children 값 들을 불러와 주고 로그인 성공시
ProtectRoute로 전환 되게 만들었습니다. ProtectRoute로 전환되는 구간은 sigin 페이지 로그인 페이지로 이동시에
ProtectRoute로 전환됩니다.
constants/routes.js
export const HOME = '/';
export const BROWSE = '/browse';
export const SIGN_UP = '/signup';
export const SIGN_IN = '/signin';
constants 폴더에 routes.js입니다. Routing시 이동할 페이지를 설정하는 곳입니다. 이동할 페이지를 변수로 선언해서
<IsUserRedirect user={user} loggedInPath={ROUTES.BROWSE} path={ROUTES.HOME}>
<Home />
</IsUserRedirect>
이동될 페이지를 path 값에 담았습니다. 불러올때는 import 로 ROUTES로 불러와 그안에 값을 ROUTES.HOME 으로 불러왔습니다. path는 기본으로 이동될 페이지이고 loggedInPath는 로그인 성공시 최종적으로 이동하는 페이지를 설정해주는 변수 입니다.
<IsUserRedirect user={user} loggedInPath={ROUTES.BROWSE} path={ROUTES.SIGN_IN}>
<SignIn />
</IsUserRedirect>
이렇게 SignIn 로그인 페이지에도 최종적으로 로그인 되는 경로를 BROWSE 로 정했습니다. SIgnIn의 조건이 만족하다면
Browse 페이지로 이동하게 됩니다.
처음 시작시 메인페이지 화면
├── __tests__
│ ├── components
│ │ ├── __snapshots__
│ │ │ ├── accordion.test.js.snap
│ │ │ ├── card.test.js.snap
│ │ │ ├── feature.test.js.snap
│ │ │ ├── footer.test.js.snap
│ │ │ ├── form.test.js.snap
│ │ │ ├── header.test.js.snap
│ │ │ ├── jumbotron.test.js.snap
│ │ │ ├── loading.test.js.snap
│ │ │ ├── opt-form.test.js.snap
│ │ │ ├── player.test.js.snap
│ │ │ └── profiles.test.js.snap
│ │ ├── accordion.test.js
│ │ ├── card.test.js
│ │ ├── feature.test.js
│ │ ├── footer.test.js
│ │ ├── form.test.js
│ │ ├── header.test.js
│ │ ├── jumbotron.test.js
│ │ ├── loading.test.js
│ │ ├── opt-form.test.js
│ │ ├── player.test.js
│ │ └── profiles.test.js
│ ├── containers
│ │ └── profiles.test.js
│ ├── pages
│ │ ├── browse.test.js
│ │ ├── home.test.js
│ │ ├── signin.test.js
│ │ └── signup.test.js
│ └── utils
│ └── selection-filter.test.js
├── app.js
├── components
│ ├── accordion
│ │ ├── index.js
│ │ └── styles
│ │ └── accordion.js
│ ├── card
│ │ ├── index.js
│ │ └── styles
│ │ └── card.js
│ ├── feature
│ │ ├── index.js
│ │ └── styles
│ │ └── feature.js
│ ├── footer
│ │ ├── index.js
│ │ └── styles
│ │ └── footer.js
│ ├── form
│ │ ├── index.js
│ │ └── styles
│ │ └── form.js
│ ├── header
│ │ ├── index.js
│ │ └── styles
│ │ └── header.js
│ ├── index.js
│ ├── jumbotron
│ │ ├── index.js
│ │ └── styles
│ │ └── jumbotron.js
│ ├── loading
│ │ ├── index.js
│ │ └── styles
│ │ └── loading.js
│ ├── opt-form
│ │ ├── index.js
│ │ └── styles
│ │ └── opt-form.js
│ ├── player
│ │ ├── index.js
│ │ └── styles
│ │ └── player.js
│ └── profiles
│ ├── index.js
│ └── styles
│ └── profiles.js
├── constants
│ └── routes.js
├── containers
│ ├── browse.js
│ ├── faqs.js
│ ├── footer.js
│ ├── header.js
│ ├── jumbotron.js
│ └── profiles.js
├── context
│ └── firebase.js
├── fixtures
│ ├── faqs.json
│ └── jumbo.json
├── global-styles.js
├── helpers
│ └── routes.js
├── hooks
│ ├── index.js
│ ├── use-auth-listener.js
│ └── use-content.js
├── index.js
├── lib
│ └── firebase.prod.js
├── logo.svg
├── pages
│ ├── browse.js
│ ├── home.js
│ ├── index.js
│ ├── signin.js
│ └── signup.js
├── seed.js
└── utils
├── index.js
└── selection-filter.js
메인 페이지 - Browse 페이지 까지의 폴더 구조는 이렇습니다.
import React from 'react';
import { Feature, OptForm } from '../components';
import { HeaderContainer } from '../containers/header';
import { JumbotronContainer } from '../containers/jumbotron';
import { FaqsContainer } from '../containers/faqs';
import { FooterContainer } from '../containers/footer';
export default function Home() {
return (
<>
<HeaderContainer>
<Feature>
<Feature.Title>Unlimited films, TV programmes and more.</Feature.Title>
<Feature.SubTitle>Watch anywhere. Cancel at any time.</Feature.SubTitle>
<OptForm>
<OptForm.Input placeholder="Email address" />
<OptForm.Button>Try it now</OptForm.Button>
<OptForm.Break />
<OptForm.Text>Ready to watch? Enter your email to create or restart your membership.</OptForm.Text>
</OptForm>
</Feature>
</HeaderContainer>
<JumbotronContainer />
<FaqsContainer />
<FooterContainer />
</>
);
}
먼저 비로그인시 접속되는 home.js 페이지 입니다. 상단 부분의 HeaderContainer로 감싸고 그안에 Feature로 Title과
SubTitle 을 입력해주었습니다. OptForm은 이메일 주소를 입력하는 Form을 만들었습니다.
윗부분은 home.js파일에 선언해주고 그밑에 부분들은 각각 파일로 분할했습니다.
import React from 'react';
import jumboData from '../fixtures/jumbo';
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>
);
}
Jumbotron 을 구성한 파일입니다. 이파일에도 동일하게 Jumbo파일을 Container 구조로 분할해서 불러와주었습니다.
JumboData 변수로 .json 파일을 생성해서 image와 title 부분들을 map 함수로 을 돌려서
선언한 3개 분량까지 불러와주었습니다.
[
{
"id": 1,
"title": "Enjoy on your TV.",
"subTitle": "Watch on smart TVs, PlayStation, Xbox, Chromecast, Apple TV, Blu-ray players and more.",
"image": "/images/misc/home-tv.jpg",
"alt": "Tiger King on Netflix",
"direction": "row"
},
{
"id": 2,
"title": "Download your programmes to watch on the go.",
"subTitle": "Save your data and watch all your favourites offline.",
"image": "/images/misc/home-mobile.jpg",
"alt": "Watch on mobile",
"direction": "row-reverse"
},
{
"id": 3,
"title": "Watch everywhere.",
"subTitle": "Stream unlimited films and TV programmes on your phone, tablet, laptop and TV without paying more.",
"image": "/images/misc/home-imac.jpg",
"alt": "Money Heist on Netflix",
"direction": "row"
}
]
fixtures 폴더에서 만든 jumbo.json 들 불러올 id 값들을 순서대로 작성하고 title 제목과 subTitle 이미지 설명 디렉션을 각각
id 값들로 설정해서 불러왔습니다. 현재 값들을 3개로 분할 했기때문에 작성된 id 개수만큼만 불러오게 됩니다.
불러온뒤 화면 이렇게 JumboTron 이 완성되었습니다.
import React from "react";
import {
Item,
Inner,
Container,
Pane,
Title,
Image,
SubTitle,
Video,
Show
} from "./styles/jumbotron";
export default function Jumbotron({ children, direction = 'row'}) {
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>
}
Jumbotron.Video = function JumboVideo () {
return (
<Video></Video>
)
}
Jumbotron.Videos = function JumboVideos ({ ...restProps}) {
return (
<Video
{...restProps}
type='video/mkv'
autoPlay="true"
loop="true"
muted
playsInline=""
>
</Video>
)
}
Jumbotron.Show = function JumboShow ({ ...restProps}) {
return <Show {...restProps}></Show>
}
Jumbotron에 대한 컴포넌트 설정 부분 입니다. 레이아웃에 깔아주기 위해서는 먼저 컴포넌트에 메인 이름을 깔아주고
그안에 기본값들과 children을 불러준뒤 부모 컴포넌트이름뒤에 자식들의 이름을 선언해 주어야합니다.
export { default as Jumbotron } from './jumbotron';
모든 설정이 완료되면 선언할 컴포넌트 전부를 내보내줄 index.js파일을 만들어서 그곳에 대신 내보내는 역할을 해줍니다.
그럼 최종적으로 JumboTron의 앞부분에 명시하고 . 뒤에 자식들을 불러오면
<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>
<div className="home-story-show">
<img src={item.image} alt={item.alt} />
<video autoPlay playsInline="" muted loop>
<source src={item.video} />
</video>
</div>
</Jumbotron.Pane>
</Jumbotron>
그안에 값들은 children 자식이 되어 json의 값들을 명시할 수 있게 됩니다.
다음은 넷플릭스의 질문 사항을 모은 Questions 컴포넌트를 불러올 차례입니다. 이 파일이름은 FaqsContainer 입니다.
import React from 'react';
import { Accordion, OptForm } from '../components';
import faqsData from '../fixtures/faqs';
export function FaqsContainer() {
return (
<Accordion>
<Accordion.Title>Frequently Asked Questions</Accordion.Title>
<Accordion.Frame>
{faqsData.map((item) => (
<Accordion.Item key={item.id}>
<Accordion.Header>{item.header}</Accordion.Header>
<Accordion.Body>{item.body}</Accordion.Body>
</Accordion.Item>
))}
</Accordion.Frame>
<OptForm>
<OptForm.Input placeholder="Email address" />
<OptForm.Button>Try it now</OptForm.Button>
<OptForm.Break />
<OptForm.Text>Ready to watch? Enter your email to create or restart your membership.</OptForm.Text>
</OptForm>
</Accordion>
);
}
Faqs 컨테이도 OptForm과 마찬가지로 json파일에 Data로 구성되었습니다. 컴포넌트는 Accodion과 OptForm컴포넌트고
Accordion은 맨위 Title과 모달 컴포넌트 OptForm은 위에서 설정된 이메일 창을 똑같이 설정 한 부분 입니다.
[
{
"id": 1,
"header": "What is Netflix?",
"body": "Netflix is a streaming service that offers a wide variety of award-winning TV programmes, films, anime, documentaries and more – on thousands of internet-connected devices.\n\nYou can watch as much as you want, whenever you want, without a single advert – all for one low monthly price. There's always something new to discover, and new TV programmes and films are added every week!"
},
{
"id": 2,
"header": "How much does Netflix cost?",
"body": "Watch Netflix on your smartphone, tablet, smart TV, laptop or streaming device, all for one low fixed monthly fee. Plans start from £5.99 a month. No extra costs or contracts."
},
{
"id": 3,
"header": "Where can I watch?",
"body": "Watch anywhere, anytime, on an unlimited number of devices. Sign in with your Netflix account to watch instantly on the web at netflix.com from your personal computer or on any internet-connected device that offers the Netflix app, including smart TVs, smartphones, tablets, streaming media players and game consoles.\n\nYou can also download your favourite programmes with the iOS, Android, or Windows 10 app. Use downloads to watch while you're on the go and without an internet connection. Take Netflix with you anywhere."
},
{
"id": 4,
"header": "How do I cancel?",
"body": "Netflix is flexible. There are no annoying contracts and no commitments. You can easily cancel your account online in two clicks. There are no cancellation fees – start or stop your account at any time."
},
{
"id": 5,
"header": "What can I watch on Netflix?",
"body": "Netflix has an extensive library of feature films, documentaries, TV programmes, anime, award-winning Netflix originals, and more. Watch as much as you want, any time you want."
}
]
Faqs.json 파일에 설정된 질문 각각 id값들을 주고 header 와 body로 제목과 클릭시 나오는 내용을 분할 해주었습니다.
const ToggleContext = createContext();
Accordion.Item = function AccordionItem({ children, ...restProps }) {
const [toggleShow, setToggleShow] = useState(false);
return (
<ToggleContext.Provider value={{ toggleShow, setToggleShow }}>
<Item {...restProps}>{children}</Item>
</ToggleContext.Provider>
);
};
Accordion.Header = function AccordionHeader({ children, ...restProps }) {
const { toggleShow, setToggleShow } = useContext(ToggleContext);
return (
<Header onClick={() => setToggleShow(!toggleShow)} {...restProps}>
{children}
{toggleShow ? (
<img src="/images/icons/close-slim.png" alt="Close" />
) : (
<img src="/images/icons/add.png" alt="Open" />
)}
</Header>
);
};
Accordion 모달 창이 열리는 방식. const로 ToggleShow 변수를 주고 기본값은 열리지 않는 상태인 false로 주었습니다.
context API를 활용해 상태관리 context를 생성하고 Item안에 상태관리된 ToggleContext.Provide값을 선언하고
그안에 value 값으로 const로 선언된 toggleshow를 선언해주었습니다. 이렇게 되면 닫힌 상태인 false가 기본값이되고
처음 상태는 닫힌 Modal 상태가 됩니다. 밑부분 Header는 그것을 조정하게 해주는 부분으로 위에 선언된 toggleShow를
그대로 가져와서 toggle값을 show/hide 변수로 대입해 false 시 + 아이콘 true 시 x 아이콘 으로 변경하게 했습니다.
on : off 시 상태마다 아이콘이 변경됩니다.
SignIn 페이지 입니다.
import React, { useState, useContext } from 'react';
import { useHistory } from 'react-router-dom';
import { FirebaseContext } from '../context/firebase';
import { Form } from '../components';
import { HeaderContainer } from '../containers/header';
import { FooterContainer } from '../containers/footer';
import * as ROUTES from '../constants/routes';
import 부분입니다 firebaseContext
import { createContext } from 'react';
export const FirebaseContext = createContext(null);
firebaseContext로 firebase 정보를 내보내주었습니다. 이렇게 되면 내가 firebase 페이지에서 만든 프로젝트를 context 로 선언해서 사용할 수 있게 됩니다.
const history = useHistory();
const { firebase } = useContext(FirebaseContext);
const [emailAddress, setEmailAddress] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const isInvalid = password === '' || emailAddress === '';
로그인시 유저 정보 값을 전달해주기 위해서 history 변수를 사용했습니다. 이렇게 되면 로그인 성공시 browse에게 내가 입력한
값을 기록해서 넘겨주게 됩니다. 내가 로그인시 가져오고 싶은 변수는 이메일 주소와 비밀번호 입니다. emailAddress 와 password 로 이메일 주소와 비밀번호를 만들어주고 useState값에는 아무값도 없는 ''로 선언했습니다.
error변수는 로그인 실패시 나타낼 에러 문구를 선언해주기 위해서 만들었습니다.
const handleSignin = (event) => {
event.preventDefault();
return firebase
.auth()
.signInWithEmailAndPassword(emailAddress, password)
.then(() => {
history.push(ROUTES.BROWSE);
})
.catch((error) => {
setEmailAddress('');
setPassword('');
setError(error.message);
});
};
로그인 handleSignin 액션으로 생성해주고 그안에 envet 값을 불러왔습니다. 이벤트가 취소될때 이벤트를 막기위해
e.preventDefault () 를 선언해주었습니다.
return firebase로 firebase 변수를 return해주고 순서대로 .auth() 값과
로그인시 필요한 정보를 선언할 .signInWithEmailAndPassword로 그안에 이메일 주소와 비밀번호 변수를 넣어줍니다.
.then & .catch 문을 돌려 로그인 성공시 .then 에 history.push가 발동되 Browse 페이지로 이동됩니다.
로그인 실패시 .catch 문이 발동됩니다 setEmailAddress & setPassword 안에 ''을 붙여 아무값도 입력되지않거나
비밀번호가 틀릴 시 오류가 발생하도록 설정했습니다.
로그인 실패시 .catch 문이 발동되서 Browse 페이지로 이동되지않게 사용자에게 에러를 반환합니다.
로그인 성공시 .then이 발동되 Browse 페이지로 이동됩니다.
<Form.Base onSubmit={handleSignin} method="POST">
<Form.Input
placeholder="Email address"
value={emailAddress}
onChange={({ target }) => setEmailAddress(target.value)}
/>
<Form.Input
type="password"
value={password}
autoComplete="off"
placeholder="Password"
onChange={({ target }) => setPassword(target.value)}
/>
<Form.Submit type="submit" data-testid="sign-in">
Sign In
</Form.Submit>
</Form.Base>
로그인 Form입니다. Input 값에 value로 emailAdress 와 Password를 주었고 onChange로 target 변수를 주어서
이벤트 를 발동시키게 했습니다.
import React from 'react';
import { BrowseContainer } from '../containers/browse';
import { useContent } from '../hooks';
import { selectionFilter } from '../utils';
export default function Browse() {
const { series } = useContent('series');
const { films } = useContent('films');
const slides = selectionFilter({ series, films });
return <BrowseContainer slides={slides} />;
}
로그인 시 이동되는 Browse 파일 입니다. useContent로 카테고리에대한 content를 만들었습니다.
slides변수를 설정해 series 영화와 films 영화 컨텐츠 를 각각 선언해주었습니다.
export default function selectionFilter({ series, films } = []) {
return {
series: [
{ title: 'Documentaries', data: series?.filter((item) => item.genre === 'documentaries') },
{ title: 'Comedies', data: series?.filter((item) => item.genre === 'comedies') },
{ title: 'Children', data: series?.filter((item) => item.genre === 'children') },
{ title: 'Crime', data: series?.filter((item) => item.genre === 'crime') },
{ title: 'Feel Good', data: series?.filter((item) => item.genre === 'feel-good') },
],
films: [
{ title: 'Drama', data: films?.filter((item) => item.genre === 'drama') },
{ title: 'Thriller', data: films?.filter((item) => item.genre === 'thriller') },
{ title: 'Children', data: films?.filter((item) => item.genre === 'children') },
{ title: 'Suspense', data: films?.filter((item) => item.genre === 'suspense') },
{ title: 'Romance', data: films?.filter((item) => item.genre === 'romance') },
],
};
}
selectionFilter 장르를 설정하는 파일입니다. 이 장르들은 넷플릭스 접속시 장르에 붙게될 title 제목과
영화에 장르를 맡아줄 item.genre 입니다.
import React, { useState, useEffect, useContext } from 'react';
import Fuse from 'fuse.js';
import { Card, Header, Loading, Player } from '../components';
import * as ROUTES from '../constants/routes';
import logo from '../logo.svg';
import { FirebaseContext } from '../context/firebase';
import { SelectProfileContainer } from './profiles';
import { FooterContainer } from './footer';
export function BrowseContainer({ slides }) {
const [category, setCategory] = useState('series');
const [profile, setProfile] = useState({});
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [slideRows, setSlideRows] = useState([]);
const { firebase } = useContext(FirebaseContext);
const user = firebase.auth().currentUser || {};
useEffect(() => {
setTimeout(() => {
setLoading(false);
}, 3000);
}, [profile.displayName]);
useEffect(() => {
setSlideRows(slides[category]);
}, [slides, category]);
useEffect(() => {
const fuse = new Fuse(slideRows, { keys: ['data.description', 'data.title', 'data.genre'] });
const results = fuse.search(searchTerm).map(({ item }) => item);
if (slideRows.length > 0 && searchTerm.length > 3 && results.length > 0) {
setSlideRows(results);
} else {
setSlideRows(slides[category]);
}
}, [searchTerm]);
return profile.displayName ? (
<>
{loading ? <Loading src={user.photoURL} /> : <Loading.ReleaseBody />}
<Header src="joker1" dontShowOnSmallViewPort>
<Header.Frame>
<Header.Group>
<Header.Logo to={ROUTES.HOME} src={logo} alt="Netflix" />
<Header.TextLink active={category === 'series' ? 'true' : 'false'} onClick={() => setCategory('series')}>
Series
</Header.TextLink>
<Header.TextLink active={category === 'films' ? 'true' : 'false'} onClick={() => setCategory('films')}>
Films
</Header.TextLink>
</Header.Group>
<Header.Group>
<Header.Search searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
<Header.Profile>
<Header.Picture src={user.photoURL} />
<Header.Dropdown>
<Header.Group>
<Header.Picture src={user.photoURL} />
<Header.TextLink>{user.displayName}</Header.TextLink>
</Header.Group>
<Header.Group>
<Header.TextLink onClick={() => firebase.auth().signOut()}>Sign out</Header.TextLink>
</Header.Group>
</Header.Dropdown>
</Header.Profile>
</Header.Group>
</Header.Frame>
<Header.Feature>
<Header.FeatureCallOut>Watch Joker Now</Header.FeatureCallOut>
<Header.Text>
Forever alone in a crowd, failed comedian Arthur Fleck seeks connection as he walks the streets of Gotham
City. Arthur wears two masks -- the one he paints for his day job as a clown, and the guise he projects in a
futile attempt to feel like he's part of the world around him.
</Header.Text>
<Header.PlayButton>Play</Header.PlayButton>
</Header.Feature>
</Header>
<Card.Group>
{slideRows.map((slideItem) => (
<Card key={`${category}-${slideItem.title.toLowerCase()}`}>
<Card.Title>{slideItem.title}</Card.Title>
<Card.Entities>
{slideItem.data.map((item) => (
<Card.Item key={item.docId} item={item}>
<Card.Image
src={`${process.env.PUBLIC_URL}` + `/images/${category}/${item.genre}/${item.slug}/small.jpg`}
/>
<Card.Meta>
<Card.SubTitle>{item.title}</Card.SubTitle>
<Card.Text>{item.description}</Card.Text>
</Card.Meta>
</Card.Item>
))}
</Card.Entities>
<Card.Feature category={category}>
<Player>
<Player.Button />
<Player.Video src="/videos/bunny.mp4" />
</Player>
</Card.Feature>
</Card>
))}
</Card.Group>
<FooterContainer />
</>
) : (
<SelectProfileContainer user={user} setProfile={setProfile} />
);
}
Browse 에 전체 코드들입니다.
const [category, setCategory] = useState('series');
영화 카테고리를 선언할 category입니다. 처음 값은 series 고 로그인 접속시 series 카테고리를 먼저 보여주게 됩니다.
const [profile, setProfile] = useState({});
const [loading, setLoading] = useState(true);
로그인 성공후 프로필 창을 보여줄 profile 변수와 프로필 선택시 로딩될 loading 변수입니다.
return profile.displayName ? (
<>
{loading ? <Loading src={user.photoURL} /> : <Loading.ReleaseBody />}
<Header src="joker1" dontShowOnSmallViewPort>
<Header.Frame>
<Header.Group>
<Header.Logo to={ROUTES.HOME} src={logo} alt="Netflix" />
<Header.TextLink active={category === 'series' ? 'true' : 'false'} onClick={() => setCategory('series')}>
Series
</Header.TextLink>
<Header.TextLink active={category === 'films' ? 'true' : 'false'} onClick={() => setCategory('films')}>
Films
</Header.TextLink>
</Header.Group>
<Header.Group>
<Header.Search searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
<Header.Profile>
<Header.Picture src={user.photoURL} />
<Header.Dropdown>
<Header.Group>
<Header.Picture src={user.photoURL} />
<Header.TextLink>{user.displayName}</Header.TextLink>
</Header.Group>
<Header.Group>
<Header.TextLink onClick={() => firebase.auth().signOut()}>Sign out</Header.TextLink>
</Header.Group>
</Header.Dropdown>
</Header.Profile>
</Header.Group>
</Header.Frame>
로그인 성공시 이곳 ProfileContainer가 보여지게 됩니다.
import React from 'react';
import { Header, Profiles } from '../components';
import * as ROUTES from '../constants/routes';
import logo from '../logo.svg';
export function SelectProfileContainer({ user, setProfile }) {
return (
<>
<Header bg={false}>
<Header.Frame>
<Header.Logo to={ROUTES.HOME} src={logo} alt="Netflix" />
</Header.Frame>
</Header>
<Profiles>
<Profiles.Title>Who's watching?</Profiles.Title>
<Profiles.List>
<Profiles.User
onClick={() => setProfile({ displayName: user.displayName, photoURL: user.photoURL })}
data-testid="user-profile"
>
<Profiles.Picture src={user.photoURL} />
<Profiles.Name>{user.displayName}</Profiles.Name>
</Profiles.User>
</Profiles.List>
</Profiles>
</>
);
}
프로필을 설정할 파일 Profiles.js Container export로 내보나주고 Container 에 user 값과 setProfile 값을 const로 대신 선언
해주었습니다. Profiles.Piture에 user에 profile 사진정보를 불러오고 user.displayName에 유저의 이메일 정보를 가져옵니다.
Profiles.User = function ProfilesUser({ children, ...restProps }) {
return <Item {...restProps}>{children}</Item>;
};
Profiles.Picture = function ProfilesPicture({ src, ...restProps }) {
return <Picture {...restProps} src={src ? `/images/users/${src}.png` : '/images/misc/loading.gif'} />;
};
Profiles.Name = function ProfilesName({ children, ...restProps }) {
return <Name {...restProps}>{children}</Name>;
};
profiles도 동일하게 Components에서 부모 자식 으로 나누어서 선언 했습니다.
ProfilesPiture은 사용자의 프로필 사진이고 src에 on & off 시 적용될 사진들을 선언해주었습니다.
프로필 사진은 users에 있는 유저 프로필 이미지이고 로그인성공시 프로필 로 넘어가면서 처음 로딩이 발동됩니다.
return profile.displayName ? (
<>
{loading ? <Loading src={user.photoURL} /> : <Loading.ReleaseBody />}
프로필 클릭시 return 이 발동되면서 로딩 spinner가 돌아가게 됩니다.
import React from 'react';
import { LockBody, ReleaseBody, Spinner, Picture } from './styles/loading';
export default function Loading({ src, ...restProps }) {
return (
<Spinner {...restProps}>
<LockBody />
<Picture src={`/images/users/${src}.png`} data-testid="loading-picture" />
</Spinner>
);
}
Loading.ReleaseBody = function LoadingReleaseBody() {
return <ReleaseBody />;
};
로딩도 동일하게 컴포넌트를 만들어 주었습니다.
접속후 화면
const [category, setCategory] = useState('series');
<Header.TextLink active={category === 'series' ? 'true' : 'false'} onClick={() => setCategory('series')}>
Series
</Header.TextLink>
<Header.TextLink active={category === 'films' ? 'true' : 'false'} onClick={() => setCategory('films')}>
Films
</Header.TextLink>
탭전환 방식 Series 와 Films 에 탭전환 부분입니다.
const 로 카테고리 변수를 주고 category에 fileter에 설정된 값을 입력해서 Navigation을 구성했습니다.
'포트폴리오 분석 > React' 카테고리의 다른 글
React 포트폴리오 참고 분석 (2) - Netflix-Web-App-Clone(Prem3997) - 영화 Modal 창 참고 (0) | 2021.09.21 |
---|