React 로 웹 홈페이지 만들기 기초
1. Visual Studio 환경 셋팅
1) 새 폴더 생성 - back, front 폴더 생성 > 터미널 front 접근 >
npm init
npm i next react react-dom
2) package.json 수정
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev":"next dev -p 3001 -H 0.0.0.0",
"build":"next build",
"start":"next start",
"lint":"next lint"
},
3) pages 폴더 생성 >index.jsx 생성 및 코드 작성
const Index =()=>{
return(
<>
hello Next.js
</>
)
}
export default Index
4) 실행
npm run dev
=> 위에서 설정한 port 3001로 들어가서 'hello Next.js' 가 잘 뜨는지 확인
* 노트북과 핸드폰 wifi 공유할 때 꿀팁
노트북 window key -> cmd -> ipconfig 입력 -> 핸드폰 브라우저에 ipv4 주소+3001 입력하면 노트북에서 실행시키는 브라우저 내용이 뜬다!
2. 라우터 / 동적 라우터 / pages 파일 트리 생성
1) pages > join.jsx, login.jsx, join.jsx + posts 폴더 > post.jsx 파일 생성 + 기본 코드 작성 + 각각 브라우저에 잘 나오는지 확인
* React 는 라우터가 위 파일트리처럼 아주 간단하게 가능하다 ! 폴더를 쓰면 한 번 더 들어가야함.
localhost:3001
localhost:3001/join
localhost:3001/login
localhost:3001/posts/post
2) post.jsx를 -----> 동적 라우팅되도록 만들기
* 파일명을 배열 형식으로 만들어야 함.
* 동적 라우
- post.jsx 파일명을 ---> [post].jsx로 변경
- [post].jsx 의 내용을 수정하기
import {useRouter} from 'next/router'
const Post =()=>{
const router = useRouter()
const {post} = router.query
return(
<>
hello Post 동적라우팅 {post}
</>
)
}
export default Post
localhost:3001/posts/ 블라블라블라ㅏㅏㅏㅏ <- 요기에 쓰는것이 나온다
3. 레이아웃 구성하기
Header & Footer Components를 고정시키고 안의 내용만 바뀌도록 만들 예쩡,
요즘에는 로그인, 회원가입 or 다른 페이지 경우 화면자체를 아예 바꿔버리는 게 트렌디 하다고 함 !
* BlogLayout이라는 레이아웃으로 전체 index, login, join, post 감싸기
children 사용하기
1) front > components 폴더 생성 > blogLayout.jsx 생성 및 코드 작성
const BlogLayout = ({children}) =>{
return(
<>
Header html
<div className="header">
{/* 로고 & 메뉴 */}
<h1>Logo</h1>
<ul>
<li>Home</li>
<li>글쓰기</li>
<li>로그인</li>
<li>회원가입</li>
</ul>
</div>
<div className="container">
{children} //<--------------------요 안에 index, join, login, post
</div>
Footer html
<div className="footer">
copyright © all reserved
</div>
</>
)
}
export default BlogLayout
질문: pages 라는 라우터 폴더 명은 정해진건지 ? 웅
2) index, join, login, post.jsx 파일에 blogLayout.jsx 가져오기 + 감싸기 (감싸는 건 chidren 덕분에 감싸기 가능)
아래 코드처럼 모두 바꾸기 ! 경로는 post의 경우 ../../ 두번 나가야함
import { useRouter } from 'next/router'
import BlogLayout from '../../components/blogLayout'
const Post = () => {
const router = useRouter()
const { post } = router.query
return (
<>
<BlogLayout>
hello Post 동적라우팅 {post}
</BlogLayout>
</>
)
}
export default Post
3) 트렌드에 따라 login, join page에서 header 없애기 !
components > FormLayout.jsx 파일 생성
const FormLayout = ({children})=>{
return(
<>
{children}
Footer html
<div className="footer">
copyright © all reserved
</div>
</>
)
}
export default FormLayout
blogLayout.jsx - 기존 코드에서 FormLayout코드로 대체
import FormLayout from "./FormLayout"
const BlogLayout = ({children}) =>{
return(
<>
Header html
<div className="header">
{/* 로고 & 메뉴 */}
<h1>Logo</h1>
<ul>
<li>Home</li>
<li>글쓰기</li>
<li>로그인</li>
<li>회원가입</li>
</ul>
</div>
<div className="container">
{children}
</div>
<FormLayout/>
</>
)
}
export default BlogLayout
login.jsx, join.jsx 수정 -> BlogLayout ---> FormLayout
import FormLayout from '../components/FormLayout'
const Join = () => {
return (
<>
<FormLayout>
hello join.jsx
</FormLayout>
</>
)
}
export default Join
4. 링크의 이동
React -> Single page application (SPA) -> url 변경이 안되어서 Next의 Link 를 활용하여 사용
Next Link 는 React Link to 와 다르게 href를 사용 ! 그리고 a tag 값이 비어있으면 안된다.
BlogLayout.jsx
import FormLayout from "./FormLayout"
import Link from 'next/link'
const BlogLayout = ({children}) =>{
return(
<>
Header html
<div className="header">
{/* 로고 & 메뉴 */}
<h1>Logo</h1>
<ul>
<li><Link href="/"><a>Home</a></Link></li>
<li><Link href="/posts/post"><a>글쓰기</a></Link></li>
<li><Link href="/login"><a>로그인</a></Link></li>
<li><Link href="/join"><a>회원가입</a></Link></li>
</ul>
</div>
<div className="container">
{children}
</div>
<FormLayout/>
</>
)
}
export default BlogLayout
=> 이제 home/글쓰기/login/join을 누르면 해당 페이지로 url 이동이 된다.
5. 뒤로가는 버튼 만들기
next/router 를 사용
header & footer가 있는 home, posts/post는 페이지 이동 없기 때문에 뒤로가는 버튼 일단 안만들고
login & join 만 ㄱㄱ ! -> Login & Join 을 감싸고 있는 FormLayout.jsx에만 적용해보기
FormLayout.jsx
import Router from 'next/router'
const FormLayout = ({children})=>{
return(
<>
<button onClick={()=>Router.back()}>뒤로가기</button>
{children}
Footer html
6. head 가 안보인다. ---> head 관련 수정 하기
파일 구성에 index.jsx가 없다. -> 리액트가 다 관리해줌
head를 수정하고 싶으면 이걸 바꿀 수 있는 (리액트가 제공해주는) Component에서 가능 !
index.jsx
import BlogLayout from '../components/blogLayout'
import Head from 'next/head'
const Index = () => {
return (
<>
<Head>
<title>My website</title>
</Head>
<BlogLayout>
My sebsite가 뜬다
각각의 page 들도 import & 코드 작성 해보기
6. _App.jsx 생성 - url 공유 시 그림도 뜨도록 만들기 (html기능)
head tag -> meta tag 작성
모든 page에 설정하기 귀찮으니 모든 compo가 실행 전 거쳐가는 곳 => app.js에 <head>를 생성하기
pages > _app.jsx 생성 (정해져 있는 이름 by 리액트)
1) pages>_app.jsx 생성 & 코드 작성
const App = ({Component})=>{
return(
<>
hello?
<Component/>
Hello back to u
</>
)
}
export default App
* _app.jsx에서 props로 가져온 Component 안에 모 - 든 pages내 Components 들이 들어가 있다.
따로 import 해오지않고 _App.js파일만 적어도 가능
2) 그림 뜨도록 하는 방법
7. CSS 만들기 -> _App.jsx에 넣기
front > index.css 생성 & 코드 작성
*{
padding:0;
margin:0;
}
ul,li{
list-style:none;
}
a{
color:darkcyan;
text-decoration: none;
}
_app.jsx에 index.css 가져오기
import '../index.css'
8. 글자에 폰트 넣기
1) google font에서 원하는 글자 + url 복사
2) _app.jsx Head import
3) <Head>에 해당 url 붙여넣기 -> url 닫기 /> 요고 !
4) url 중, crossorigin -> crossorigin="true" 로 수정
5) index.css google fonts 의 CSS rules to specify families 복사 -> index.css 에 추가
body{
font-family: 'Cute Font', cursive;
}
Google Fonts
9. 이미지 가져오기
1) front > public 폴더 생성 ( 요 파일명도 react에서 정한것)
2) 아무 이미지 넣기 -> index.jsx에 사용해보기
<BlogLayout>
hello Next.js
<div>
<img className="test_img" src="/github_profile2.jpg"/>
</div>
10. 각각 Component에 사용할 CSS 따로 빼서 적용해보기
위에서 만든 index.css 는 공용!
1) 요걸 하기 전 components > FooterLayout.jsx생성 따로 compo 뺴자
const FooterLayout = () => {
return (
<>
Footer html
<div className={form_style.footer}>
copyright © all reserved
</div>
</>
)
}
export default FooterLayout
2) BlogLayout.jsx, FormLayout.jsx에 FooterLayout 가져오기
3) FooterLayout에만 사용하고 싶은 css 만들어 보기
components> footer_style.module.css 생성
.footer{
background-color: yellow;
color:white;
}
FooterLayout.jsx
import footer_style from './footer_style.module.css'
const FooterLayout = () => {
return (
<>
Footer html
<div className={footer_style.footer}>
copyright © all reserved
</div>
</>
)
}
export default FooterLayout
객체로 오는 footer_style---> . 쩜 찍고 안의 내용 사용 가능
import footer_style from './footer_style.module.css' -> 요 안의 모든 css를 객체로 반환 !
11. styled-component 사용하여 햄버거버튼 만들기
1) 라이브러리 설치
npm i styled-components
2) components > NavToggle.jsx 파일 생성
const NavToggle=()=>{
return(
<div>
<input type ="checkbox" id="nav-toggle" className="nav-toggle"/>
<label htmlFor="nav-toggle">
<span>1</span>
<span>2</span>
<span>3</span>
</label>
</div>
)
}
export default NavToggle
3) BlogLayout.jsx 에 NavToggle 가져오기
import FooterLayout from "./FooterLayout"
import Link from 'next/link'
import NavToggle from './NavToggle'
const BlogLayout = ({children}) =>{
return(
<>
Header html
<div className="header">
{/* 로고 & 메뉴 */}
<h1>Logo</h1>
<ul>
<li><Link href="/"><a>Home</a></Link></li>
<li><Link href="/posts/post"><a>글쓰기</a></Link></li>
<li><Link href="/login"><a>로그인</a></Link></li>
<li><Link href="/join"><a>회원가입</a></Link></li>
</ul>
<NavToggle/>
4) styled component 불러오기 & 사용
* 자동 완성 기능 extension 다운 ex) vscode-styled-component
import Styled from 'styled-components'
const NavToggle = () => {
return (
<Toggle>
<input type="checkbox" id="nav-toggle" className="nav-toggle" />
<label htmlFor="nav-toggle">
<span></span>
<span></span>
<span></span>
</label>
</Toggle>
)
}
export default NavToggle
const Toggle = Styled.div`
background:transparent;
border-color:transparent;
& > .nav-toggle{
display:none;
}
& > .nav-toggle + label{
display:block;
width:2.5rem;
height:2rem;
position:relative;
cursor:pointer;
}
& > .nav-toggle + label > span{
display:block;
position:absolute;
width:100%;
height:5px;
border-radius:30px;
background:black;
transition:all .35s ;
}
& > .nav-toggle + label > span:nth-child(1){top:0}
& > .nav-toggle + label > span:nth-child(2){top:50%; transform:translateY(-50%)}
& > .nav-toggle + label > span:nth-child(3){bottom:0}
& > .nav-toggle:checked + label > span:nth-child(1){
top:50%;
transform:translateY(-50%) rotate(45deg);
}
& > .nav-toggle:checked + label > span:nth-child(2){
opacity:0;
}
& > .nav-toggle:checked + label > span:nth-child(3){
bottom:50%;
transform:translateY(50%) rotate(-45deg);
}
여기까지 내용 : https://blckchainetc.tistory.com/290
12. Header 따로 빼서 CSS (from Bloglayout)
1) components > layout 폴더 생성 > Header.jsx 파일 생성 + 코드(from blogLayout) 작성
Header.jsx
import Link from 'next/link'
import NavToggle from '../NavToggle'
const Header = () => {
return (
<>
<div className="header">
{/* 로고 & 메뉴 */}
<h1>Logo</h1>
<ul>
<li><Link href="/"><a>Home</a></Link></li>
<li><Link href="/posts/post"><a>글쓰기</a></Link></li>
<li><Link href="/login"><a>로그인</a></Link></li>
<li><Link href="/join"><a>회원가입</a></Link></li>
</ul>
<NavToggle />
</div>
</>
)
}
export default Header
BlogLayout.jsx
import FooterLayout from "./FooterLayout"
// import Link from 'next/link'
// import NavToggle from './NavToggle'
import Header from './layout/Header'
const BlogLayout = ({children}) =>{
return(
<>
<Header/>
<div className="container">
{children}
</div>
<FooterLayout/>
</>
)
}
export default BlogLayout
Header.jsx
Styeld CSS 추가 (div 대체 )
import Styled from 'styled-components' //추가
const HeaderContainer = Styled.div`
display:flex;
flex-direction:row;
align-items:center;
justify-content:space-between;
padding:0.5vw;
box-sizing:border-box;
border-bottom:1px solid #ddd;
width:100vw;
`
const Header = () => {
return (
<>
<HeaderContainer>
{/* 로고 & 메뉴 */}
<h1>Logo</h1>
<ul>
<li><Link href="/"><a>Home</a></Link></li>
<li><Link href="/posts/post"><a>글쓰기</a></Link></li>
<li><Link href="/login"><a>로그인</a></Link></li>
<li><Link href="/join"><a>회원가입</a></Link></li>
</ul>
<NavToggle />
</HeaderContainer>
</>
)
}
ul 대체 CSS 추가
import Link from 'next/link'
import NavToggle from '../NavToggle'
import Styled from 'styled-components'
const HeaderContainer = Styled.div`
display:flex;
flex-direction:row;
align-items:center;
justify-content:space-between;
padding:0.5vw;
box-sizing:border-box;
border-bottom:1px solid #ddd;
width:100vw;
`
const Gnb =Styled.ul`
// 모바일
display:flex;
flex-direction:row;
// pc
@media only screen and (max-width:768px){
display:none;
}
& > li{
margin-left:20px;
font-size:20px;
}
`
const Header = () => {
return (
<>
<HeaderContainer>
{/* 로고 & 메뉴 */}
<h1>Logo</h1>
<Gnb>
<li><Link href="/"><a>Home</a></Link></li>
<li><Link href="/posts/post"><a>글쓰기</a></Link></li>
<li><Link href="/login"><a>로그인</a></Link></li>
<li><Link href="/join"><a>회원가입</a></Link></li>
</Gnb>
<NavToggle />
</HeaderContainer>
</>
)
}
export default Header
13. 새로고침 시 CSS 풀리지 않게 만들기
NEXT 에서 styled component plugin설정을 안해놓음
styled component는 react가 아닌 다른 사람이 만든거라 -> 우리가 직접 설정해야함
1) pages > _document.jsx 생성 및 아래 코드 작성
import Document from "next/document";
import { ServerStyleSheet } from "styled-components";
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
// sheet을 사용해 정의된 모든 스타일을 수집
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
});
// Documents의 initial props
const initialProps = await Document.getInitialProps(ctx);
// props와 styles를 반환
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
}
2) front > .babelrc 파일 생성 및 코드 작성
{
"presets":["next/babel"],
"plugins":[
["styled-components", {"ssr":true}]
]
}
3) package down
npm i -D babel-plugin-styled-components
=> 새로고침해도 풀리지 않음
14. 대분류 메뉴 꾸미기 css
1) 메뉴 만들기
import Styled from 'styled-components'
const NavToggle = () => {
return (
<Toggle>
<input type="checkbox" id="nav-toggle" className="nav-toggle" />
<label htmlFor="nav-toggle">
<span></span>
<span></span>
<span></span>
</label>
<Accordion>
<ul>
<li>대분류 메뉴1</li>
<li>대분류 메뉴2</li>
<li>대분류 메뉴3</li>
<li>대분류 메뉴4</li>
<li>대분류 메뉴5</li>
</ul>
</Accordion>
</Toggle>
)
}
export default NavToggle
const Accordion = Styled.div`
position:absolute;
width:100%;
left:0px;
top:10vh;
z-index:5;
background:#fff;
padding:7vh 0;
& > ul{
margin-top:5vh;
display:flex;
flex-direction:column;
}
& > ul > li{
margin-top:20px;
text-align:center;
}
`
.
.
.
Styled Component의 장점 : props 값을 던질 수 있고 css 에서 받을 수 있다. -> display none or block 할 지 정할 수 있
15. NavToggle 햄버거에 onClick event
1) NavToggle.jsx 에 useState를 사용해서 visible = false 상태값 만들기
2) 해당 상태값을 Accordion css compo에게 props로 전달하기
import Styled from 'styled-components'
import {useState} from 'react'
const NavToggle = () => {
const [visible,setVisible] = useState(false)
const handleToggle=()=>{
setVisible(!visible)
}
return (
<Toggle>
<input
type="checkbox"
id="nav-toggle"
onClick={handleToggle}
className="nav-toggle"
/>
<label htmlFor="nav-toggle">
<span></span>
<span></span>
<span></span>
</label>
<AccordionMenu flag={visible}>
<ul>
3) 전달 받은 flag로 css 'block' or 'none' 결정하기
${} -> js 영역
const AccordionMenu = Styled.div`
position:absolute;
width:100%;
left:0px;
top:10vh;
z-index:5;
background:#fff;
padding:7vh 0;
// props 가 가리키는 것 : Accordion이 가진 모든 props를 말함
display:${(props)=>(props.flag) ? 'block':'none'};
16. Header없는 formLayout.jsx CSS
import Router from 'next/router'
import FooterLayout from './FooterLayout'
import Styled from 'styled-components'
const FormLayout = ({ children }) => {
return (
<>
<Background>
<div>
<button onClick={() => Router.back()}>뒤로가기</button>
{children}
<FooterLayout />
</div>
</Background>
</>
)
}
export default FormLayout
const Background = Styled.div`
width:100vw;
height:100vh;
background:#eee;
display:flex;
align-items:center;
justify-content:center;
&>div{
width:300px;
height:400px;
background:#fff;
padding:20px;
}
`
17. login.jsx 로그인 형식 + 기능 추가 ( form, input, button )
1) 형식 만들기
<FormLayout>
<h2>로그인 page</h2>
<form>
<input type="text" placeholder="아이디를 입력해주세요"/>
<input type="password" placeholder="패스워드를 입력해주세요"/>
<button type="submit">로그인 </button>
</form>
</FormLayout>
2) 기존의 방법으로 로그인 기능 추가해보기
import FormLayout from '../components/FormLayout'
import Head from 'next/head'
import {useState} from 'react'
const Login = () => {
const [userid, setUserid] = useState('')
const [userpw, setUserpw] = useState('')
const ChangeUserid = e=>{
setUserid(e.target.value)
}
const ChangeUserpw=e=>{
setUserpw(e.target.value)
}
const handleSubmit=e=>{
e.preventDefault()
console.log(userid, userpw)
}
return (
<>
<Head>
<title>login</title>
</Head>
<FormLayout>
<h2>로그인 page</h2>
<form onSubmit={handleSubmit}>
<input type="text" onChange={ChangeUserid} placeholder="아이디를 입력해주세요"/>
<input type="password" onChange={ChangeUserpw} placeholder="패스워드를 입력해주세요"/>
<button type="submit">로그인 </button>
</form>
</FormLayout>
</>
)
}
export default Login
=> 버튼 눌렀을 때 console.log 잘 찍히는지 쳌
3) 위의 기존 방식을 조금 더 편리(?)하게 바꿔보기 by custom hook
userInput 이라는 함수를 사용해서 userid, userpw input 값의 변경 시 value와 onChange 함수를 return 해줌
하나의 함수로 여러개의 기능을 관리할 수 있다 !
근데 input 쪽에 쓴 {...userid} 요 부분은 아무리봐도 낯설다,,
{...{"value":"ok"}} == value="ok"
import FormLayout from '../components/FormLayout'
import Head from 'next/head'
import {useState} from 'react'
const userInput = (defaultValue)=>{
const [value,setValue] =useState(defaultValue)
const onChange =e=>{
setValue(e.target.value)
}
return{
value,onChange
}
}
const Login = () => {
const userid = userInput('')
const userpw = userInput('')
const handleSubmit=e=>{
e.preventDefault()
console.log(userid, userpw)
console.log(userid.value,userpw.value)
}
return (
<>
<Head>
<title>login</title>
</Head>
<FormLayout>
<h2>로그인 page</h2>
<form onSubmit={handleSubmit}>
<input type="text" {...userid} placeholder="아이디를 입력해주세요"/>
<input type="password" {...userpw} placeholder="패스워드를 입력해주세요"/>
<button type="submit">로그인 </button>
</form>
</FormLayout>
</>
)
}
export default Login
4) id & pw 가 === asdf 일 때 메인으로 다시 redirect 되도록 만들기
Router 가져오기
어디론가 보내는건 Router.push 이군...
import FormLayout from '../components/FormLayout'
import Head from 'next/head'
import {useState} from 'react'
import Router from 'next/router'
.
.
.
const handleSubmit=e=>{
e.preventDefault()
console.log(userid.value,userpw.value)
userid.value==='asdf' && userpw.value==='asdf' ? Router.push('/') : alert('id or pw가 다릅니다.');
}
18. 로그인 시, main menu 바꿔주기
context & useReducer 사용해서 Login 되었다는 상태 저장할 공간 만들기
1) pages > store > context.jsx
import {createContext} from 'react'
export const initialState ={
IsLogin:false,
}
const Store = createContext(initialState)
export default Store
2) 만든 context를 최상위 Compo _app.jsx 에 가져오기 + _app.js 이하 모든 컴포에서 사용할 수 있도록 provider 로 감싸기
import '../index.css'
import Head from 'next/head'
import Store, { initialState } from './store/context'
const App = ({ Component }) => {
return (
<>
<Head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="true" />
<link href="https://fonts.googleapis.com/css2?family=Cute+Font&display=swap" rel="stylesheet" />
</Head>
<Store.Provider value={initialState}>
<Component />
</Store.Provider>
</>
)
}
export default App
3) Header에서 사용하기
- useContext , Store 가져오기
import Link from 'next/link'
import NavToggle from '../NavToggle'
import Styled from 'styled-components'
import {useContext} from 'react'
import Store from '../../pages/store/context'
IsLogin == true or false 값에 따라 변수값 다르게 설정
import Link from 'next/link'
import NavToggle from '../NavToggle'
import Styled from 'styled-components'
import {useContext} from 'react'
import Store from '../../pages/store/context'
const Header = () => {
const globalStore = useContext(Store)
const {IsLogin} = globalStore
let LoginOrLogout = (IsLogin) ? '로그아웃' : '로그인'
let JoinOrInfo = (IsLogin) ? '회원정보' : '회원가입'
return (
<>
<HeaderContainer>
{/* 로고 & 메뉴 */}
<h1>Logo</h1>
<Gnb>
<li><Link href="/"><a>Home</a></Link></li>
<li><Link href="/posts/post"><a>글쓰기</a></Link></li>
<li><Link href="/login"><a>{LoginOrLogout}</a></Link></li>
<li><Link href="/join"><a>{JoinOrInfo}</a></Link></li>
</Gnb>
<NavToggle />
</HeaderContainer>
</>
)
}
export default Header
css 생략
4) IsLogin 상태 바꿔주기 만들기 with Reducer
* react는 상태 state 가 바뀌어야 rerender를 한다 -> 그래서 Context만 쓰지않고 useReducer와 함께 쓴다.
context으로 변수 값이 바뀌어도 상태값이 바뀌지 않는다.
Context = 공용 저장소
Reducer = 상태값 저장소 + 변경소 (+ rendering)
_app.jsx
value={{state, dispatch}} 넘겨줄 때 객체 *** {{}} 두 번 쓰기
import '../index.css'
import Head from 'next/head'
import Store, { initialState } from './store/context'
import { useReducer, useContext } from 'react'
const App = ({ Component }) => {
const globalContext = useContext(Store)
const [state,dispatch] = useReducer(reducer,globalContext)
return (
<>
<Head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="true" />
<link href="https://fonts.googleapis.com/css2?family=Cute+Font&display=swap" rel="stylesheet" />
</Head>
<Store.Provider value={{state, dispatch}}>
<Component />
</Store.Provider>
</>
)
}
export default App
reducer 따로 빼서 만들어 주기
pages > store > reducer.jsx
export const reducer =(state,action)=>{
switch(action.type){
case 'login':
return{
...state,
}
case 'logout':
return{
...state,
}
}
}
_app.jsx 에 reducer 가져오기
import {reducer} from './store/reducer'
Header.jsx 수정
받는 value 가 객체가 되었고 그 중 state 안에 IsLogin 값을 담기
import Link from 'next/link'
import NavToggle from '../NavToggle'
import Styled from 'styled-components'
import {useContext} from 'react'
import Store from '../../pages/store/context'
const Header = () => {
const globalStore = useContext(Store)
const {IsLogin} = globalStore.state
console.log(IsLogin)
let LoginOrLogout = (IsLogin) ? '로그아웃' : '로그인'
let JoinOrInfo = (IsLogin) ? '회원정보' : '회원가입'
return (
<>
<HeaderContainer>
{/* 로고 & 메뉴 */}
<h1>Logo</h1>
<Gnb>
<li><Link href="/"><a>Home</a></Link></li>
<li><Link href="/posts/post"><a>글쓰기</a></Link></li>
<li><Link href="/login"><a>{LoginOrLogout}</a></Link></li>
<li><Link href="/join"><a>{JoinOrInfo}</a></Link></li>
</Gnb>
<NavToggle />
</HeaderContainer>
</>
)
}
export default Header
css 생략
login.jsx
import FormLayout from '../components/FormLayout'
import Head from 'next/head'
import {useState, useReducer, useContext} from 'react'
import Router from 'next/router'
import {reducer} from '../store/reducer'
import Store from '../store/context'
import userInput from '../hooks/userInput'
const Login = () => {
const userid = userInput('')
const userpw = userInput('')
const {dispatch} = useContext(Store)
const handleSubmit=e=>{
e.preventDefault()
dispatch({type:'login'})
userid.value==='asdf' && userpw.value==='asdf' ? Router.push('/') : alert('id or pw가 다릅니다.');
}
return (
<>
<Head>
<title>login</title>
</Head>
<FormLayout>
<h2>로그인 page</h2>
<form onSubmit={handleSubmit}>
<input type="text" {...userid} placeholder="아이디를 입력해주세요"/>
<input type="password" {...userpw} placeholder="패스워드를 입력해주세요"/>
<button type="submit">로그인 </button>
</form>
</FormLayout>
</>
)
}
export default Login
reducer.jsx 수정
export const reducer = (state, action) => {
switch (action.type) {
case 'login':
// state.state.IsLogin=true;
return {
...state,
IsLogin: true,
}
case 'logout':
console.log(state)
return {
...state,
IsLogin: false,
}
default:
return state
}
}
질문1 : return{ IsLogin:true, ...any} 요렇게 하면 false -> true 로 바뀌지 않음 (근데 logout은 바뀜)
-> Login.jsx에서 useContext를 사용하고 또 state에 객체를 넣었음 !
요 부분 위 코드 수정함
19. 로그 아웃
1) pages > logout.jsx 생성 & 코드 작성
import { useContext, useEffect } from "react"
import Store from './store/context'
import Router from 'next/router'
const Logout =()=>{
const {dispatch} = useContext(Store)
useEffect(()=>{
dispatch({type:'logout'})
setTimeout(()=>{
Router.back()
},1000)
},[])
return(
<>
로그아웃 되셨습니다
</>
)
}
export default Logout
1초 동안 '로그아웃 되셨습니다' 가 나오고 Router.back()으로 자동으로 url 넘어감
여기까지의 내용
https://blckchainetc.tistory.com/291
20. 파일 중간 정리
1) store 폴더를 front>하위 폴더로 이동
-> store 폴더 안 context, reducer 를 사용한 _app.js, header, login, logout, etc.....jsx 경로 바꾸기
2) hooks 따로 빼기 (login.jsx 안에 있던 userInput 함수)
front > hooks 폴더 생성 > userInput.jsx 파일 생성
import {useState} from 'react'
const userInput = (defaultValue)=>{
const [value,setValue] =useState(defaultValue)
const onChange =e=>{
setValue(e.target.value)
}
return{
value,onChange
}
}
export default userInput
login.jsx
뺴낸 userInput 가져오기
import FormLayout from '../components/FormLayout'
import Head from 'next/head'
import {useState, useReducer, useContext} from 'react'
import Router from 'next/router'
import {reducer} from '../store/reducer'
import Store from '../store/context'
import userInput from '../hooks/userInput'
const Login = () => {
const userid = userInput('')
const userpw = userInput('')
21. 회원가입 형식 만들기 with hook userInput
join.jsx
join도 login처럼 input box에 내용을 넣고 해당 내용을 출력받기 떄문에 위의 userInput hook을 사용 가능
import FormLayout from '../components/FormLayout'
import Head from 'next/head'
import userInput from '../hooks/userInput'
const Join = () => {
const username=userInput('')
const userid=userInput('')
const password=userInput('')
const passwordCheck=userInput('')
const userphone=userInput('')
const handleSubmit =(e)=>{
e.preventDefault()
console.log(username,userid,password,passwordCheck, userphone)
}
return (
<>
<Head>
<title>join</title>
</Head>
<FormLayout>
<h2>회원가입</h2>
<form onSubmit ={handleSubmit}>
<input type="text" {...username} placeholder="이름을 입력해주세요"/> <br/>
<input type="text" {...userid} placeholder="아이디를 입력해주세요"/> <br/>
<input type="text" {...password} placeholder="비밀번호를 입력해주세요"/> <br/>
<input type="text" {...passwordCheck} placeholder="비밀번호 확인"/> <br/>
<input type="text" {...userphone} placeholder="핸드폰 번호를 입력해주세요"/> <br/>
<button type="submit">회원가입하기</button>
</form>
</FormLayout>
</>
)
}
export default Join
console.log가 잘 나오는지 확인 -> .value 객체로 사용해야함을 알 수 있다
=> passwordCheck은 요 페이지에서 한 번밖에 안쓸거라 hooks 를 굳이 쓰지 않아도 됨 =>
const passwordCheck =userInput('') 제거!
22. 회원가입 시 password 실시간 체크
1) passwordCheck과 passwordError 라는 상태값을 만들기 (useState 사용하므로 useState도 가져오기 from 'react')
2) passwordCheck input의 value =passwordCheck을 주고 onChange로 passwordCheck 값 넣기 + passwordError true or false 정해서 set 하기
import {useState} from 'react'
const Join = () => {
const [passwordCheck, setPasswordCheck] = useState('')
const [passwordError, setPasswordError] = useState(false)
const handlePassword =e=>{
const {value} = {...e.target}
setPasswordError(password.value!==value) // 같지 않으면 true 다르면 false
setPasswordCheck(value)
}
return (
<input type="text" {...password} placeholder="비밀번호를 입력해주세요"/> <br/>
<input type="text" value={passwordCheck} onChange={handlePassword} placeholder="비밀번호 확인"/> <br/>
{passwordError && <div style={{color:'red'}}> 비밀번호가 일치하지 않습니다. </div>}
<input type="text" {...userphone} placeholder="핸드폰 번호를 입력해주세요"/> <br/>
<button type="submit">회원가입하기</button>
새로 배운 구문 {passwordError && <div></div>} passwordError 가 true 면 && 뒤의 명령 실행
23. 약관 동의 checkbox
join.jsx
1) checkbox 추가 -> checked = 는 {term}으로 설정
2) 상태값 term, termError 추가
3) label 추가해서 '약관 동의해주십쇼' 글씨를 클릭해서도 체크박스 체크되도록
-> 요 부분은
<input type="checkbox" id ="term" checked={term} onChange={handleTerm} />
<label htmlFor="term"> 약관에 동의해 주십시오</label> <br/>
input 에 id 를 넣고
lable은 htmlFor = 향하고 싶은 id 명 쓰기
import FormLayout from '../components/FormLayout'
import Head from 'next/head'
import userInput from '../hooks/userInput'
import {useState} from 'react'
const Join = () => {
const username=userInput('')
const userid=userInput('')
const password=userInput('')
const userphone=userInput('')
const [passwordCheck, setPasswordCheck] = useState('')
const [passwordError, setPasswordError] = useState(false)
const handlePassword =e=>{
const {value} = {...e.target}
setPasswordError(password.value!==value) // 같지 않으면 true 다르면 false
setPasswordCheck(value)
}
const [term,setTerm] = useState(false)
const [termError, setTermError] = useState(false)
const handleTerm=e=>{
// setTerm(!term) 또는
setTerm(e.target.checked)
}
const handleSubmit =(e)=>{
e.preventDefault()
console.log(username,userid,password, userphone)
}
return (
<>
<Head>
<title>join</title>
</Head>
<FormLayout>
<h2>회원가입</h2>
<form onSubmit ={handleSubmit}>
<input type="text" {...username} placeholder="이름을 입력해주세요"/> <br/>
<input type="text" {...userid} placeholder="아이디를 입력해주세요"/> <br/>
<input type="text" {...password} placeholder="비밀번호를 입력해주세요"/> <br/>
<input type="text" value={passwordCheck} onChange={handlePassword} placeholder="비밀번호 확인"/> <br/>
{passwordError && <div style={{color:'red'}}> 비밀번호가 일치하지 않습니다. </div>}
<input type="text" {...userphone} placeholder="핸드폰 번호를 입력해주세요"/> <br/>
<input type="checkbox" id ="term" checked={term} onChange={handleTerm} />
<label htmlFor="term"> 약관에 동의해 주십시오</label> <br/>
<button type="submit">회원가입하기</button>
</form>
</FormLayout>
</>
)
}
export default Join
4) termError 로직 추가
const [term,setTerm] = useState(false)
const [termError, setTermError] = useState(false)
const handleTerm=e=>{
// setTerm(!term) 또는
setTerm(e.target.checked)
setTermError(e.target.checked ===false)
}
return (
<>
<input type="checkbox" id ="term" checked={term} onChange={handleTerm} />
<label htmlFor="term"> 약관에 동의해 주십시오</label> <br/>
{termError && <div style={{color:'red'}}>약관 동의는 필수입니다.</div>}
<button type="submit">회원가입하기</button>
24. submit 했을 때 마지막으로 한번 더 체크해주기
const handleSubmit =(e)=>{
e.preventDefault()
if (password.value!==passwordCheck){
setPasswordError(true)
}else{
setPasswordError(false)
}
if(!term){
setTermError(true)
return;
}
}
join.jsx 전체 코드
import FormLayout from '../components/FormLayout'
import Head from 'next/head'
import userInput from '../hooks/userInput'
import {useState} from 'react'
const Join = () => {
const username=userInput('')
const userid=userInput('')
const password=userInput('')
const userphone=userInput('')
const [passwordCheck, setPasswordCheck] = useState('')
const [passwordError, setPasswordError] = useState(false)
const handlePassword =e=>{
const {value} = {...e.target}
setPasswordError(password.value!==value) // 같지 않으면 true 다르면 false
setPasswordCheck(value)
}
const [term,setTerm] = useState(false)
const [termError, setTermError] = useState(false)
const handleTerm=e=>{
// setTerm(!term) 또는
setTerm(e.target.checked)
setTermError(e.target.checked ===false)
}
const handleSubmit =(e)=>{
e.preventDefault()
if (password.value!==passwordCheck){
setPasswordError(true)
}else{
setPasswordError(false)
}
if(!term){
setTermError(true)
return;
}
}
return (
<>
<Head>
<title>join</title>
</Head>
<FormLayout>
<h2>회원가입</h2>
<form onSubmit ={handleSubmit}>
<input type="text" {...username} placeholder="이름을 입력해주세요"/> <br/>
<input type="text" {...userid} placeholder="아이디를 입력해주세요"/> <br/>
<input type="text" {...password} placeholder="비밀번호를 입력해주세요"/> <br/>
<input type="text" value={passwordCheck} onChange={handlePassword} placeholder="비밀번호 확인"/> <br/>
{passwordError && <div style={{color:'red'}}> 비밀번호가 일치하지 않습니다. </div>}
<input type="text" {...userphone} placeholder="핸드폰 번호를 입력해주세요"/> <br/>
<input type="checkbox" id ="term" checked={term} onChange={handleTerm} />
<label htmlFor="term"> 약관에 동의해 주십시오</label> <br/>
{termError && <div style={{color:'red'}}>약관 동의는 필수입니다.</div>}
<button type="submit">회원가입하기</button>
</form>
</FormLayout>
</>
)
}
export default Join
25. 대분류 메뉴 뿌리기
카테고리 만들어 뿌리기 From DB => 요걸 json으로 받을 것
1) NavToggle 파일 정리
components > Accordion.jsx 생성 + 코드 옮기기
import Styled from 'styled-components'
const Accordion = ({visible}) => { // props 받기
return (
<>
<AccordionMenu flag-{visible}>
<ul>
<li>대분류 메뉴1</li>
<li>대분류 메뉴2</li>
<li>대분류 메뉴3</li>
<li>대분류 메뉴4</li>
<li>대분류 메뉴5</li>
</ul>
</AccordionMenu>
</>
)
}
const AccordionMenu = Styled.div`
position:absolute;
width:100%;
left:0px;
top:10vh;
z-index:5;
background:#fff;
padding:7vh 0;
// props 가 가리키는 것 : Accordion이 가진 모든 props를 말함
display:${(props)=>(props.flag) ? 'block':'none'};
& > ul{
margin-top:5vh;
display:flex;
flex-direction:column;
}
& > ul > li{
margin-top:20px;
text-align:center;
}
`
export default Accordion
NavToggle.jsx
import Styled from 'styled-components'
import {useState} from 'react'
import Accordion from './Accordion'
const NavToggle = () => {
const [visible,setVisible] = useState(false)
const handleToggle=()=>{
setVisible(!visible)
}
return (
<Toggle>
<input
type="checkbox"
id="nav-toggle"
onClick={handleToggle}
className="nav-toggle"
/>
<label htmlFor="nav-toggle">
<span></span>
<span></span>
<span></span>
</label>
<Accordion visible={visible}/>
</Toggle>
)
}
export default NavToggle
const Toggle = Styled.div`
background:transparent;
border-color:transparent;
& > .nav-toggle{
display:none;
}
& > .nav-toggle + label{
display:block;
width:2.5rem;
height:2rem;
position:relative;
cursor:pointer;
}
& > .nav-toggle + label > span{
display:block;
position:absolute;
width:100%;
height:5px;
border-radius:30px;
background:black;
transition:all .35s ;
}
& > .nav-toggle + label > span:nth-child(1){top:0}
& > .nav-toggle + label > span:nth-child(2){top:50%; transform:translateY(-50%)}
& > .nav-toggle + label > span:nth-child(3){bottom:0}
& > .nav-toggle:checked + label > span:nth-child(1){
top:50%;
transform:translateY(-50%) rotate(45deg);
}
& > .nav-toggle:checked + label > span:nth-child(2){
opacity:0;
}
& > .nav-toggle:checked + label > span:nth-child(3){
bottom:50%;
transform:translateY(50%) rotate(-45deg);
}
`
2) DB에서 받았다고 가정하고 json 형태 임시로 만들어 놓기 -> map으로 뿌리기
Accordion.jsx
import Styled from 'styled-components'
import Link from 'next/link'
const menu = [
{
id:'1',
category:'대분류 메뉴 1',
url: '/posts/1'
},
{
id:'2',
category:'대분류 메뉴 2',
url: '/posts/2'
},
{
id:'3',
category:'대분류 메뉴 3',
url: '/posts/3'
},
{
id:'4',
category:'대분류 메뉴 4',
url: '/posts/4'
},
{
id:'5',
category:'대분류 메뉴 5',
url: '/posts/5'
}
]
const Accordion = ({visible}) => {
const category = menu.map((v)=>{
return <li key={v.id}><Link href={v.url}><a>{v.category}</a></Link></li>
})
return (
<>
<AccordionMenu flag={visible}>
<ul>
{category}
</ul>
</AccordionMenu>
</>
)
}
const AccordionMenu = Styled.div`
position:absolute;
width:100%;
left:0px;
top:10vh;
z-index:5;
background:#fff;
padding:7vh 0;
// props 가 가리키는 것 : Accordion이 가진 모든 props를 말함
display:${(props)=>(props.flag) ? 'block':'none'};
& > ul{
margin-top:5vh;
display:flex;
flex-direction:column;
}
& > ul > li{
margin-top:20px;
text-align:center;
}
`
export default Accordion
3) 페이지 이동 되도록 만들기
NavToggle.jsx 에서 Accordion에 handleToggle보내기
const NavToggle = () => {
const [visible,setVisible] = useState(false)
const handleToggle=()=>{
setVisible(!visible)
}
return (
<Toggle>
<input
type="checkbox"
id="nav-toggle"
onClick={handleToggle}
className="nav-toggle"
/>
<label htmlFor="nav-toggle">
<span></span>
<span></span>
<span></span>
</label>
<Accordion visible={visible} handleToggle = {handleToggle}/>
</Toggle>
)
Accordion.jsx 받아서 사용하기
const Accordion = ({visible, handleToggle}) => {
const category = menu.map((v)=>{
return <li key={v.id} onClick={handleToggle}><Link href={v.url}><a>{v.category}</a></Link></li>
})
return (
<>
<AccordionMenu flag={visible}>
<ul>
{category}
</ul>
</AccordionMenu>
</>
)
}
4) 글쓰기 post page 꾸미기
posts> [post].jsx
import { useRouter } from 'next/router'
import BlogLayout from '../../components/blogLayout'
import Head from 'next/head'
const data =[
{
id:'1',
subject:'my website',
content:'html 왼쪽 위에서부터 내려옵니다.',
date:'2021-07-29',
hit:'10',
},
{
id:'2',
subject:'my website',
content:'html- block과 inline 스타일로 나눠집니다. ',
date:'2021-07-30',
hit:'1',
}
]
const Post = () => {
const router = useRouter()
const { post } = router.query
const list = data.map(v=>{
return(
<div key={v.id}>
<ul>
<li>{v.subject}</li>
<li>{v.content}</li>
<li>{v.date}</li>
<li>{v.hit}</li>
</ul>
</div>
)
})
return (
<>
<Head>
<title>posting </title>
</Head>
<BlogLayout>
hello Post 동적라우팅 {post}
<div>
{list}
</div>
</BlogLayout>
</>
)
}
export default Post
d
질문 2
최상위 Compo에서 context 사용할 때 꼭 Provider 에 변수에 state, dispatch 등을 담아서 보내야 하는지 ? -> useConetxt(Store) 할 때 나오는 값들이 props에 담은 값 , reducer에서 사용할 때 state는 상태만 있음 dispatch는 없다.
받는 Logout Compo에서 useContext(Store) => 하면 나오는 값은 value 안의 값들인지 -> _app.js에서 value 라는 변수로 보낸 값들이 담겨있음 거기서 dispatch만 빼서 사용 한 것 !
질문 3
아래 는 왜 안되는지 ???? useEffect를 지움
-> 무한 랜더가 된다. -> dispatch 하면 render가 되고 또 Logout 이 실행 ..반복
useEffect는, 두 번째 인자값 []을 주면 == componentdidmount와 같은 생명주기가 되어 딱 한 번 render 된다.
import { useContext, useEffect } from "react"
import Store from './store/context'
import Router from 'next/router'
const Logout =()=>{
const {dispatch} = useContext(Store)
// useEffect(()=>{
dispatch({type:'logout'})
// setTimeout(()=>{
// Router.back()
// },1000)
// },[])
return(
<>
로그아웃 되셨습니다
</>
)
}
export default Logout
'블록체인 기반 핀테크 및 응용 SW개발자 양성과정 일기' 카테고리의 다른 글
[99일차] Redux 리액트 리덕스 Middleware Thunk (0) | 2021.07.30 |
---|---|
[98일차] 20210729 Redux 리덕스 사용법 / reducer 파일 쪼개기 / combine (0) | 2021.07.30 |
[97일차]20210728 React 리액트 회원가입 로그인 / 리덕스 설치 (0) | 2021.07.28 |
[96일차] 20210727 React 리액트, 레이아웃 CSS, 새로고침할 때 CSS 풀리는 현상, (0) | 2021.07.27 |
[95일차]20210726 react Link, head, title, css, 라우터, 폰트, 이미지 넣기, 핸드폰과 연동하기 (0) | 2021.07.26 |