Backend
home

2024. 7. 4

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
복사

기타