React - 커뮤니티 프로젝트
•
패키지 설치
$ yarn add axios polished react-icon react-router-dom react-slideshow-image styled-components
Shell
복사
•
패키지 제거
$ yarn remove react-icon
Shell
복사
•
icons 패키지 재설치
$ yarn add react-icons
Shell
복사
•
라우팅
// 라우팅: 주소에 따라 다른 화면을 보여주는 것! [react-router-dom]
// BrowserRouter로 전체를 감싼다.
// Routes 안에 Route로 경로와 컴포넌트 요소를 알려준다.
// a 태그를 통해서 페이지를 이동하면, 페이지를 아예 새롭게 불러온다. [상태 초기화]
// 상태 유지를 위해서는 Link 컴포넌트로 주소를 바꿔야 한다.
Shell
복사
•
json
json-server -w ./blog.json -p 4885
Shell
복사
•
파일 구성
◦
components > layouts > Header.jsx
import { Link } from 'react-router-dom';
import { styled } from 'styled-components';
import { IoBagCheckOutline } from "react-icons/io5";
const Header = () => {
const loginUser = localStorage.getItem("loginUser");
return (
<StyledHeader>
<Link to="/"><IoBagCheckOutline/></Link>
<nav>
<Link to="/">HOME</Link>
{/* 로그인 후에는 이 버튼은 로그아웃으로 변경 */}
{
loginUser ?
<>
<Link to="/info">{loginUser}</Link>
<Link to="/logout">LOGOUT</Link>
</> :
<Link to="/login">LOGIN</Link>
}
</nav>
</StyledHeader>
);
}
const StyledHeader = styled.header`
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 1rem;
background-color: black;
color: #fff;
& > a {
color: #fff;
text-decoration: none;
font-size: 2.5rem;
}
nav {
display: flex;
a {
font-size: 2rem;
color: #fff;
text-decoration: none;
margin: 0 1rem;
&:hover {
text-decoration: underline;
}
}
}
`
export default Header;
JavaScript
복사
◦
components > layouts > Main.jsx
import { Navigate, Outlet, Route, Routes } from "react-router-dom";
import styled from "styled-components";
import Home from "../pages/Home";
import Login from "../pages/Login";
import SignUp from "../pages/SignUp";
const Main = () => {
return (
<StyledMain>
<Routes>
<Route path="/" element={<Home />} />
<Route element={<GuestRoute />}>
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<SignUp />} />
</Route>
{/* 로그인한 회원만 접근 가능 */}
<Route element={<UserRoute />}>
<Route path="/logout" element={<h1>실제 로그아웃은 안 됨</h1>} />
<Route path="/info" element={<h1>인포 ㅎㅎ</h1>} />
</Route>
<Route path="/*" element={<h1>없는 경로 ㅋㅋㅋ</h1>} />
</Routes>
</StyledMain>
);
}
const GuestRoute = () => {
const loginUser = localStorage.getItem("loginUser");
const isLogin = !!loginUser;
return (
isLogin ? <Navigate to="/info" /> : <Outlet />
)
}
const UserRoute = () => {
const loginUser = localStorage.getItem("loginUser");
// loginUser = undefined || null
// !loginUser >>> true >> false
// const isLogin = loginUser ? true : false;
const isLogin = !!loginUser;
return (
isLogin ? <Outlet /> : <Navigate to="/login" />
)
}
const StyledMain = styled.main`
width: 70vw;
margin: 0 auto;
`
export default Main;
JavaScript
복사
◦
components > pages > Home.jsx
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const Home = () => {
const [detail, setDetail] = useState('');
const navigate = useNavigate();
const handlePageMove = () => {
// Link와 같은 동작
// navigate('/')
// navigate({
// pathname: '/'
// })
navigate({
pathname: '/',
search: `detail=${detail}`
})
}
return (
<>
<h1>홈</h1>
<input type='text' value={detail} onChange={(e) => setDetail(e.target.value)}/>
<Link to='/'>이동</Link>
<a href='/'>이동</a>
<button onClick={handlePageMove}>이동</button>
<h2>현재 state: {detail}</h2>
</>
);
}
export default Home;
JavaScript
복사
◦
components > pages > Login.jsx
import { useState } from "react";
import styled from "styled-components";
import useInputs from "../../hooks/useInputs";
import axios from "axios";
import { Button } from "../ui/Button";
const Login = () => {
const { form, handleChange, handleReset } = useInputs({ email: '', password: '' })
const { email, password } = form;
const handleLogin = async() => {
// 가짜 데이터이기 때문에, get 방식으로 로그인 진행 (실제로는 post 방식)
const url = `${process.env.REACT_APP_SERVER_ADDR}/users?email=${email}&password=${password}`;
try {
const res = await axios.get(url);
if (res.status === 200 && res.data.length == 1) {
// 로그인
console.log(res.data[0]);
localStorage.setItem("loginUser", res.data[0].email);
} else {
alert("로그인 불가능")
}
} catch (error) {
console.error(error);
}
}
return (
<>
<h1>로그인 화면</h1>
<StyledLoginBox>
<div className="input-group">
<input type="email" name="email" value={email} onChange={handleChange} />
<input type="password" name="password" value={password} onChange={handleChange} />
</div>
<Button color="#a151e2" onClick={handleLogin}>로그인</Button>
{/* <button onClick={handleReset}>초기화</button> */}
</StyledLoginBox>
</>
);
}
const StyledLoginBox = styled.div`
display: flex;
.input-group {
display: flex;
flex-direction: column;
}
`
export default Login;
JavaScript
복사
◦
components > pages > NotFound.jsx
const NotFound = () => {
return ();
}
export default NotFound;
JavaScript
복사
◦
components > pages > SignUp.jsx
import styled from "styled-components";
import useInputs from "../../hooks/useInputs";
import { Button } from "../ui/Button";
import axios from "axios";
import { useState } from "react";
const SignUp = () => {
const { form, handleChange, handleReset } = useInputs({
email: "",
nickname: "",
password: "",
password_chk: ""
});
const { email, nickname, password, password_chk } = form;
const [isDuplicate, setIsDuplicate] = useState(false);
const [errors, setErrors] = useState({});
const handleDuplicate = async () => {
if (email.trim()) {
const url = `${process.env.REACT_APP_SERVER_ADDR}/users?email=${email}`;
try {
const res = await axios.get(url);
if (!res.data.length) {
setErrors({});
setIsDuplicate(true);
} else {
setErrors({ email: "이미 존재하는 이메일입니다." })
setIsDuplicate(false);
}
} catch (error) {
console.error(error);
}
}
}
const validate = () => {
let isValid = true;
const newErrors = {};
for (const [key, value] of Object.entries(form)) {
// 빈값 여부 체크
if (!value.trim()) {
isValid = false;
newErrors[key] = `${key}를 입력해주세요`;
}
}
// password_chk 일치 여부 체크
if (form.password !== form.password_chk) {
isValid = false;
newErrors.password_chk = `비밀번호 불일치입니다`;
}
console.log(newErrors);
setErrors(newErrors);
return isValid;
// email 중복 체크
}
const handleSignUp = async (e) => {
e.preventDefault();
if (validate() && isDuplicate) {
const url = `${process.env.REACT_APP_SERVER_ADDR}/users`;
const user = { email, nickname, password };
try {
const res = await axios.post(url, user);
if (res.status === 201) {
alert('회원가입 완료');
} else {
throw new Error("회원가입 실패");
}
} catch (error) {
console.error(error);
} finally {
handleReset();
}
}
}
return (
// 회원가입할 때, { email, nickname, password, password_chk}
<>
<h1>회원가입</h1>
<JoinForm>
<div className='input-group'>
<label htmlFor="email">이메일</label>
<div>
<input type='email' id='email' name='email' value={email} onChange={handleChange} />
{errors.email && <ErrorMsg>{errors.email}</ErrorMsg>}
{isDuplicate && <SuccessMsg>사용 가능한 이메일입니다.</SuccessMsg>}
</div>
<Button type='button' color="#9a9a9a" onClick={handleDuplicate}>중복 확인</Button>
</div>
<div className='input-group'>
<label htmlFor="nickname">닉네임</label>
<div>
<input id='nickname' name='nickname' value={nickname} onChange={handleChange} />
{errors.nickname && <ErrorMsg>{errors.nickname}</ErrorMsg>}
</div>
</div>
<div className='input-group'>
<label htmlFor="password">비밀번호</label>
<div>
<input type='password' id='password' name='password' value={password} onChange={handleChange} />
{errors.password && <ErrorMsg>{errors.password}</ErrorMsg>}
</div>
</div>
<div className='input-group'>
<label htmlFor="password_chk">비밀번호 확인</label>
<div>
<input type='password' id='password_chk' name='password_chk' value={password_chk} onChange={handleChange} />
{errors.password_chk && <ErrorMsg>{errors.password_chk}</ErrorMsg>}
</div>
</div>
<div className='btn-group'>
<Button type="button" onClick={() => { handleReset(); setErrors({}); }} color="#ff8282">초기화</Button>
<Button color="#5f97f9" onClick={handleSignUp}>회원가입</Button>
</div>
</JoinForm>
</>
);
}
const JoinForm = styled.form`
display: flex;
flex-direction: column;
.input-group {
display: flex;
justify-content: space-between;
width: 90%;
margin: 1rem auto;
height: 2rem;
label {
margin-right: 1rem;
}
input {
border: none;
border-radius: 4px;
background-color: #b8f2f9;
padding: 0.8rem
}
}
.btn-group {
padding-top: 2rem;
margin: 0 auto;
}
`
const ErrorMsg = styled.div`
color: #ff5555;
font-size: 0.8rem;
margin-top: 0.2rem;
`
const SuccessMsg = styled.div`
color: #3a7102;
font-size: 0.8rem;
margin-top: 0.2rem;
`
export default SignUp;
JavaScript
복사
◦
ui > Button.jsx
import styled, { css } from "styled-components";
import { darken, lighten } from "polished"
export const Button = styled.button`
${props => css`
padding: 5px 10px;
cursor: pointer;
border: none;
border-radius: 10px;
margin: 0.2rem 0.5rem;
background-color: ${props.color};
color: #fff;
font-weight: bold;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
transition: background-color 0.3s ease-in;
&:hover {
background-color: ${darken(0.2, props.color)};
}
&:active {
transform: translateY()(1.5px);
}
`}
`
JavaScript
복사
◦
hooks > userInputs.jsx
import { useState } from "react";
const useInputs = (initForm) => {
const [form, setForm] = useState(initForm);
const handleChange = (e) => {
const { name, value } = e.target;
setForm(form => ({ ...form, [name]: value }));
}
const handleReset = () => {
setForm(initForm)
}
return { form, handleChange, handleReset };
}
export default useInputs;
JavaScript
복사
◦
.env
REACT_APP_SERVER_ADDR=http://localhost:4885
JavaScript
복사
계산기 (State 개념 이해)
import { useState } from "react";
const StatePractice = () => {
const [result, setResult] = useState(0)
const [x, setX] = useState(0);
return (
<>
<div>
<h1>{result}</h1>
<div>
<input type="number" value={x} onChange={(e) => setX(e.target.value)}/>
</div>
<div style={{ width: '50%', display: 'flex', margin: "0 auto" }}>
{/* parseInt(x) -> x를 숫자로 변환 */}
<button onClick={() => {setResult(result + parseInt(x))}}>더하기</button>
<button onClick={() => {setResult(result - parseInt(x))}}>빼기</button>
<button onClick={() => {setResult(result * parseInt(x))}}>곱하기</button>
<button onClick={() => {setResult(result / parseInt(x))}}>나누기</button>
</div>
</div>
</>
);
}
export default StatePractice;
JavaScript
복사
useReducer
/* eslint-disable default-case */
import { useReducer } from "react";
// useReducer는 무엇인가..!!!
// useState보다 더 다양한 컴포넌트 상태 관리가 가능한 훅!
// const [state, setState] = useState(initialValue);
// const [state, dispatch] = useReducer(reducer, initialValue);
// 첫번째 매개변수 : reducer...?
// reducer는 함수이다.
// reducer(state, action) => {}
// state : 상태값
// action : 특정 타입에 따라서 변화하도록 하는 조건?
// - 두번째 매개변수 : initialValue...?
// initialValue : 초기값
// - dispatch...? [보내다..?]
// action 객체를 파라미터 받아, reducer 함수를 호출
// dispatch(action객체);
const ReducerPractice = () => {
const reducer = (state, action) => {
switch (action.type) {
case 'plus':
return { value: state.value + 1 };
case 'minus':
return { value: state.value - 1 };
case 'reset':
return { value: 0 };
}
// if (action == 'plus') {
// return state + 1;
// } else if (action == 'minus') {
// return state - 1;
// } else if (action == 'reset') {
// return 0;
// }
};
const [state, dispatch] = useReducer(reducer, { value: 0 });
return (
<>
<h1>useReducer</h1>
<h2>숫자 상태 : {state.value} </h2>
<button onClick={() => dispatch("plus")}>더하기</button>
<button onClick={() => dispatch("minus")}>빼기</button>
<button onClick={() => dispatch("reset")}>초기화</button>
</>
);
}
export default ReducerPractice;
JavaScript
복사