LOGIN AND SIGNUP (1)
🍔 핵심 내용
🥑 이제 본격적으로 프로젝트를 진행한다. 로그인 부분을 해보자.
- 앞서 배운 기본 개념들을 활용하여, 진행해보자. (css는 개념보다는 많이 해봐야 감을 잡을 수 있을 것 같음)
- css는 재사용 되는 부분이 많기 때문에 계속 사용 하는 것은 공용 코드로 쓰이게끔 사전에 염두
🍔 코드 리뷰
🥑 Login.js
import {
faFacebookSquare,
faInstagram,
} from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "react-router-dom";
import styled from "styled-components";
const Container = styled.div`
display: flex;
height: 100vh;
justify-content: center;
align-items: center;
flex-direction: column;
`;
const WhiteBox = styled.div`
background-color: white;
border: 1px solid ${(props) => props.theme.borderColor};
width: 100%;
`;
const TopBox = styled(WhiteBox)`
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding: 35px 40px 25px 40px;
margin-bottom: 10px;
form {
margin-top: 35px;
width: 100%;
display: flex;
justify-items: center;
flex-direction: column;
align-items: center;
}
`;
const Input = styled.input`
width: 100%;
border-radius: 3px;
padding: 7px;
background-color: #fafafa;
border: 0.5px solid ${(props) => props.theme.borderColor};
margin-top: 5px;
box-sizing: border-box;
&::placeholder {
font-size: 12px;
}
`;
const Button = styled.input`
border: none;
border-radius: 3px;
margin-top: 12px;
background-color: ${(props) => props.theme.accent};
color: white;
text-align: center;
padding: 8px 0px;
font-weight: 600;
width: 100%;
`;
const BottomBox = styled(WhiteBox)`
padding: 20px 0px;
text-align: center;
a {
font-weight: 600;
margin-left: 5px;
color: ${(props) => props.theme.accent};
}
`;
const Wrapper = styled.div`
max-width: 350px;
width: 100%;
`;
const Separator = styled.div`
margin: 20px 0px 30px 0px;
text-transform: uppercase;
display: flex;
justify-content: center;
width: 100%;
align-items: center;
div {
width: 100%;
height: 1px;
background-color: ${(props) => props.theme.borderColor};
}
span {
margin: 0px 10px;
font-weight: 600;
font-size: 12px;
color: #8e8e8e;
}
`;
const FacebookLogin = styled.div`
color: #385285;
span {
margin-left: 10px;
font-weight: 600;
}
`;
function Login() {
return (
<Container>
<Wrapper>
<TopBox>
<div>
<FontAwesomeIcon icon={faInstagram} size="3x" />
</div>
<form>
<Input type="text" placeholder="Username" />
<Input type="password" placeholder="Password" />
<Button type="submit" value="Log in" />
</form>
<Separator>
<div></div>
<span>Or</span>
<div></div>
</Separator>
<FacebookLogin>
<FontAwesomeIcon icon={faFacebookSquare} />
<span>Log in with Facebook</span>
</FacebookLogin>
</TopBox>
<BottomBox>
<span>Don't have an account?</span>
<Link to="/sign-up">Sign up</Link>
</BottomBox>
</Wrapper>
</Container>
);
}
export default Login;
1) html 태그 <a> 대신에 <Link>를 사용한다. 이유는 <a>는 새로고침을 하기 때문에, 리액트에서 제공하는 Link를 사용한다. html 태그에서는 <a>로 먹히기 때문에, css에서는 <a>에 대해 지정해도 <Link>에 적용되게 된다.
2) FontAwesomeIcon 사용
3) Seperator 에서 일자 선(-----) 기능을 만들었다. flex와 height : 1px 로
🥑 App.js
import { useReactiveVar } from "@apollo/client";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "./screens/Home";
import Login from "./screens/Login";
import NotFound from "./screens/NotFound";
import { darkModeVar, isLoggedInVar } from "./apollo";
import { ThemeProvider } from "styled-components";
import { darkTheme, GlobalStyles, lightTheme } from "./styles";
import SignUp from "./screens/SignUp";
function App() {
const isLoggedIn = useReactiveVar(isLoggedInVar);
const darkMode = useReactiveVar(darkModeVar);
return (
<ThemeProvider theme={darkMode ? darkTheme : lightTheme}>
<GlobalStyles />
<Router>
<Switch>
<Route path="/" exact>
{isLoggedIn ? <Home /> : <Login />}
</Route>
{!isLoggedIn ? (
<Route path="/sign-up">
<SignUp />
</Route>
) : null}
<Route>
<NotFound />
</Route>
</Switch>
</Router>
</ThemeProvider>
);
}
export default App;
isLoggedIn 이 false인 경우(public상태) 에서만 sign-up 주소로 갈 수 있다.
🥑 Styles.js
import { createGlobalStyle } from "styled-components";
import reset from "styled-reset";
export const lightTheme = {
accent: "#0095f6",
borderColor: "rgb(219, 219, 219)",
};
export const darkTheme = {
fontColor: "white",
bgColor: "#2c2c2c",
};
export const GlobalStyles = createGlobalStyle`
${reset}
input {
all:unset;
}
* {
box-sizing:border-box;
}
body {
background-color: #FAFAFA;
font-size:14px;
font-family:'Open Sans', sans-serif;
color:rgb(38, 38, 38);
}
a {
text-decoration: none;
}
`;
1) 자주 사용되는 컬러를 theme에 넣어두었다. (재사용성 증가)
2) * box-sizing: border-box 지정
3) font-family는 구글 글꼴에서 가지고 온 것
🥑 index.html
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600&display=swap"
rel="stylesheet"
/>
<head>부분에 구글 글꼴 추가 (구글 글꼴 추가)
🍔 핵심 내용
🥑 css 코드 재활용성 방안
Login화면과 SignUp화면의 구성은 거의 비슷하다. 따라서 비슷한 css부분이 많기 때문에 공용으로 쓰는 코드들은 별도 componens 폴더를 만들어 재사용 하자.
🥑 routes 이름 파일 생성
여러 부분에 routes 이름이 사용 되기 때문에, 실수 방지를 위해 별도 파일을 생성해서 끌어오는 식으로 만들어 주자.
🍔 코드 리뷰
🥑 routs.js
const routes = {
home: "/",
signUp: "/sign-up",
};
export default routes;
아래 파일들은 components 폴더 하위 파일
🥑 AuthLayout.js
import styled from "styled-components";
const Container = styled.div`
display: flex;
height: 100vh;
justify-content: center;
align-items: center;
flex-direction: column;
`;
const Wrapper = styled.div`
max-width: 350px;
width: 100%;
`;
function AuthLayout({ children }) {
return (
<Container>
<Wrapper>{children}</Wrapper>
</Container>
);
}
export default AuthLayout;
children(props.children)을 통해 하위 컴포넌트를 넣을 수 있다. (코드 재사용성 증가)
🥑 FormBox.js
import styled from "styled-components";
import { BaseBox } from "../shared";
const Container = styled(BaseBox)`
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding: 35px 40px 25px 40px;
margin-bottom: 10px;
form {
margin-top: 35px;
width: 100%;
display: flex;
justify-items: center;
flex-direction: column;
align-items: center;
}
`;
function FormBox({ children }) {
return <Container>{children}</Container>;
}
export default FormBox;
🥑 separator.js
import styled from "styled-components";
const SSeparator = styled.div`
margin: 20px 0px 30px 0px;
text-transform: uppercase;
display: flex;
justify-content: center;
width: 100%;
align-items: center;
div {
width: 100%;
height: 1px;
background-color: ${(props) => props.theme.borderColor};
}
span {
margin: 0px 10px;
font-weight: 600;
font-size: 12px;
color: #8e8e8e;
}
`;
function Separator() {
return (
<SSeparator>
<div></div>
<span>Or</span>
<div></div>
</SSeparator>
);
}
export default Separator;
🥑 Input.js
import styled from "styled-components";
const SInput = styled.input`
width: 100%;
border-radius: 3px;
padding: 7px;
background-color: #fafafa;
border: 0.5px solid ${(props) => props.theme.borderColor};
margin-top: 5px;
box-sizing: border-box;
&::placeholder {
font-size: 12px;
}
`;
function Input(props) {
return <SInput {...props} />;
}
export default Input;
🥑 Button.js
import styled from "styled-components";
const SButton = styled.input`
border: none;
border-radius: 3px;
margin-top: 12px;
background-color: ${(props) => props.theme.accent};
color: white;
text-align: center;
padding: 8px 0px;
font-weight: 600;
width: 100%;
`;
function Button(props) {
return <SButton {...props} />;
}
export default Button;
🥑 BottonBox.js
import { Link } from "react-router-dom";
import styled from "styled-components";
import { BaseBox } from "../shared";
const SBottomBox = styled(BaseBox)`
padding: 20px 0px;
text-align: center;
a {
font-weight: 600;
margin-left: 5px;
color: ${(props) => props.theme.accent};
}
`;
function BottomBox({ cta, link, linkText }) {
return (
<SBottomBox>
<span>{cta}</span>
<Link to={link}>{linkText}</Link>
</SBottomBox>
);
}
export default BottomBox;
props를 이렇게 받아와서 사용할 수도 있다. (cta는 call to action이라는 뜻)
위와 같이 모두 부품화 시켜놓은 후, 아래 Login.js를 보면 꽤나 깔끔해졌다.
🥑 Login.js
import {
faFacebookSquare,
faInstagram,
} from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import styled from "styled-components";
import AuthLayout from "../components/auth/AuthLayout";
import BottomBox from "../components/auth/BottomBox";
import Button from "../components/auth/Button";
import FormBox from "../components/auth/FormBox";
import Input from "../components/auth/Input";
import Separator from "../components/auth/separator";
import routes from "../routes";
const FacebookLogin = styled.div`
color: #385285;
span {
margin-left: 10px;
font-weight: 600;
}
`;
function Login() {
return (
<AuthLayout>
<FormBox>
<div>
<FontAwesomeIcon icon={faInstagram} size="3x" />
</div>
<form>
<Input type="text" placeholder="Username" />
<Input type="password" placeholder="Password" />
<Button type="submit" value="Log in" />
</form>
<Separator />
<FacebookLogin>
<FontAwesomeIcon icon={faFacebookSquare} />
<span>Log in with Facebook</span>
</FacebookLogin>
</FormBox>
<BottomBox
cta="Don't have an account?"
link={routes.signUp}
linkText="Sign up"
/>
</AuthLayout>
);
}
export default Login;
FaceBookLogin가 남아 있는 이유는, 이 부품이 다른 곳에 쓰이지 않기 때문에 공용 파일로 따로 분류해두지 않았다.
🍔 핵심 내용
🥑 SignUp 부분 마무리
Home 화면 (Log in) 이랑 SignUp 화면이랑 유사한 부분이 많기 때문에, 비슷한 css 코드들은 공용 코드로 따로 분류해두었다. 이를 활용 하여 SignUp부분을 마무리 해보자.
🍔 코드 리뷰
🥑 SignUp.js
import { faInstagram } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import styled from "styled-components";
import AuthLayout from "../components/auth/AuthLayout";
import BottomBox from "../components/auth/BottomBox";
import Button from "../components/auth/Button";
import FormBox from "../components/auth/FormBox";
import Input from "../components/auth/Input";
import { FatLink } from "../components/shared";
import routes from "../routes";
const HeaderContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
`;
const Subtitle = styled(FatLink)`
font-size: 16px;
text-align: center;
margin-top: 10px;
`;
function SingUp() {
return (
<AuthLayout>
<FormBox>
<HeaderContainer>
<FontAwesomeIcon icon={faInstagram} size="3x" />
<Subtitle>
Sign up to see photos and videos from your friends.
</Subtitle>
</HeaderContainer>
<form>
<Input type="text" placeholder="Name" />
<Input type="text" placeholder="Email" />
<Input type="text" placeholder="Username" />
<Input type="password" placeholder="Password" />
<Button type="submit" value="Sign up" />
</form>
</FormBox>
<BottomBox cta="Have an account?" linkText="Log in" link={routes.home} />
</AuthLayout>
);
}
export default SingUp;
🥑 shared.js
import styled from "styled-components";
export const BaseBox = styled.div`
background-color: white;
border: 1px solid ${(props) => props.theme.borderColor};
width: 100%;
`;
export const FatLink = styled.span`
font-weight: 600;
color: rgb(142, 142, 142);
`;
🍔 핵심 내용
🥑 Form 만들기
먼저, 리액트 전통 방식을 이용하여 Form을 만들어 보자. (불편한걸 먼저 해보자.)
Form에는 크게
1) 유효성 검사 (글자 몇자 이상 써야 하는지와 같은...)
2) onChange 관리
3) Submit 관리
를 해야 한다. 수작업으로 하나씩 하려면 매우 번거롭다.
(다음 내용에는, 리액트 훅을 이용하여 간단히 만드는 방법을 다룰 예정)
🍔 코드 리뷰
🥑 Login.js
import {
faFacebookSquare,
faInstagram,
} from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useState } from "react";
import styled from "styled-components";
import AuthLayout from "../components/auth/AuthLayout";
import BottomBox from "../components/auth/BottomBox";
import Button from "../components/auth/Button";
import FormBox from "../components/auth/FormBox";
import Input from "../components/auth/Input";
import Separator from "../components/auth/Separator";
import routes from "../routes";
const FacebookLogin = styled.div`
color: #385285;
span {
margin-left: 10px;
font-weight: 600;
}
`;
function Login() {
const [username, setUsername] = useState("");
const [usernameError, setUsernameError] = useState("");
const onUsernameChange = (event) => {
setUsernameError("");
setUsername(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
if (username === "") {
setUsernameError("Not empty pls.");
}
if (username.length < 10) {
setUsernameError("too short");
}
};
return (
<AuthLayout>
<FormBox>
<div>
<FontAwesomeIcon icon={faInstagram} size="3x" />
</div>
<form onSubmit={handleSubmit}>
{usernameError}
<Input
onChange={onUsernameChange}
value={username}
type="text"
placeholder="Username"
/>
<Input type="password" placeholder="Password" />
<Button
type="submit"
value="Log in"
disabled={username === "" && username.length < 10}
/>
</form>
<Separator />
<FacebookLogin>
<FontAwesomeIcon icon={faFacebookSquare} />
<span>Log in with Facebook</span>
</FacebookLogin>
</FormBox>
<BottomBox
cta="Don't have an account?"
linkText="Sign up"
link={routes.signUp}
/>
</AuthLayout>
);
}
export default Login;
🍔 핵심 내용
🥑 Helmet 사용
각 화면에 대해 title을 변경하기 위해 Helmet을 사용해보자.
🍔 코드 리뷰
🥑 App.js
import { useReactiveVar } from "@apollo/client";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "./screens/Home";
import Login from "./screens/Login";
import NotFound from "./screens/NotFound";
import { darkModeVar, isLoggedInVar } from "./apollo";
import { ThemeProvider } from "styled-components";
import { darkTheme, GlobalStyles, lightTheme } from "./styles";
import SignUp from "./screens/SignUp";
import routes from "./routes";
import { HelmetProvider } from "react-helmet-async";
function App() {
const isLoggedIn = useReactiveVar(isLoggedInVar);
const darkMode = useReactiveVar(darkModeVar);
return (
<HelmetProvider>
<ThemeProvider theme={darkMode ? darkTheme : lightTheme}>
<GlobalStyles />
<Router>
<Switch>
<Route path={routes.home} exact>
{isLoggedIn ? <Home /> : <Login />}
</Route>
{!isLoggedIn ? (
<Route path={routes.signUp}>
<SignUp />
</Route>
) : null}
<Route>
<NotFound />
</Route>
</Switch>
</Router>
</ThemeProvider>
</HelmetProvider>
);
}
export default App;
먼저, HelmetProvider로 App.js에서 감싸주어야 한다.
🥑 PageTitle.js
여러 화면에 사용되어야 하기 때문에, components폴더에 넣어 주었다.
import { PropTypes } from "prop-types";
import { Helmet } from "react-helmet-async";
function PageTitle({ title }) {
return (
<Helmet>
<title>{title} | Kimstaclone</title>
</Helmet>
);
}
PageTitle.propTypes = {
title: PropTypes.string.isRequired,
};
export default PageTitle;
🥑 Login.js
import {
faFacebookSquare,
faInstagram,
} from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import styled from "styled-components";
import AuthLayout from "../components/auth/AuthLayout";
import BottomBox from "../components/auth/BottomBox";
import Button from "../components/auth/Button";
import FormBox from "../components/auth/FormBox";
import Input from "../components/auth/Input";
import Separator from "../components/auth/Separator";
import PageTitle from "../components/PageTitle";
import routes from "../routes";
const FacebookLogin = styled.div`
color: #385285;
span {
margin-left: 10px;
font-weight: 600;
}
`;
function Login() {
return (
<AuthLayout>
<PageTitle title="Login" /> <--요기
<FormBox>
<div>
<FontAwesomeIcon icon={faInstagram} size="3x" />
</div>
<form>
<Input type="text" placeholder="Username" />
<Input type="password" placeholder="Password" />
<Button type="submit" value="Log in" />
</form>
<Separator />
<FacebookLogin>
<FontAwesomeIcon icon={faFacebookSquare} />
<span>Log in with Facebook</span>
</FacebookLogin>
</FormBox>
<BottomBox
cta="Don't have an account?"
linkText="Sign up"
link={routes.signUp}
/>
</AuthLayout>
);
}
export default Login;
로그인 화면에 적용
🥑 SignUp.js
import { faInstagram } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import styled from "styled-components";
import AuthLayout from "../components/auth/AuthLayout";
import BottomBox from "../components/auth/BottomBox";
import Button from "../components/auth/Button";
import FormBox from "../components/auth/FormBox";
import Input from "../components/auth/Input";
import PageTitle from "../components/PageTitle";
import { FatLink } from "../components/shared";
import routes from "../routes";
const HeaderContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
`;
const Subtitle = styled(FatLink)`
font-size: 16px;
text-align: center;
margin-top: 10px;
`;
function SingUp() {
return (
<AuthLayout>
<PageTitle title="Sign up" /> <--요기
<FormBox>
<HeaderContainer>
<FontAwesomeIcon icon={faInstagram} size="3x" />
<Subtitle>
Sign up to see photos and videos from your friends.
</Subtitle>
</HeaderContainer>
<form>
<Input type="text" placeholder="Name" />
<Input type="text" placeholder="Email" />
<Input type="text" placeholder="Username" />
<Input type="password" placeholder="Password" />
<Button type="submit" value="Sign up" />
</form>
</FormBox>
<BottomBox cta="Have an account?" linkText="Log in" link={routes.home} />
</AuthLayout>
);
}
export default SingUp;
SignUp화면에 적용