반응형

오늘은 프로젝트에 상품을 추가할 수 있는 기능을 배웠다. 아직 적용은 안 됐지만 나중에 상품을 추가하려면 이미지나 파일을 보내는 기능이 필요하다. 0.2 버전을 업데이트해보자!

이번에도 역시 서버와 데이터베이스가 연동되어서 실행되기 때문에 코드와 이미지로만 올리겠다.

(이거진짜 코드 샌드박스로 안되는건가..) 

 

0.1 버전에서는 상품이미지를 클릭해서 들어가는 ProductPage에서 url파라미터를 useParams로 받아서

이미지 좌표만 바뀌게 해 줬는데 이번에는 클릭하고 들어갔을 때 url파라미터를 서버에서 받아서

특정데이터를 조회해서 응답으로 보내주게 만들어준다.

 

 

서버 측 index.js

app.get("/products/:id",(req,res)=>{
    const params = req.params;
    const {id} = params;
    conn.query(`select * from products where p_id=${id}`,function(error,result,fields){
        res.send(result); //url에 들어간 번호파라미터로 mysql레코드
    });
})

요청한 주소의 params가 req에 담기게 되고 그 값을 id에 할당해서 select 문의 where 조건절에

조건으로 넣어주었다. 

sql에서 조회된 결과는 res.send로  응답데이터로 클라이언트에게 응답해 준다.

 

Product / index.js

import axios from 'axios';
import React from 'react';
import { useParams } from 'react-router-dom';
import useAsync from '../customHook/useAsync';
import './index.css'



async function productFetch(id){
    const response = await axios.get(`http://localhost:8080/products/${id}`);
    return response.data
}

const ProductPage = () => {
    const {p_id} = useParams();
    const state = useAsync(()=>productFetch(p_id),[]);
    const {loading,error,data} = state;
    if (loading) return <div>로딩중</div>
    if (error) return <div>에러발생</div>
    if (!data) return null
    const [Product] = data
    return (
        <div className='productDetail'>
            <h2>기초스킨케어 세트</h2>
            <div className='productImg'>
                <img src={`../${Product.p_img}`} alt="" />
            </div>
            <div>
                <p>{Product.p_desc}</p>
                <p>{Product.p_name}</p>
                <p>가격 : {Product.p_price}</p>
                <p>무료배송</p>
            </div>
        </div>
    );
};

export default ProductPage;

product폴더에 있는 index.js폴더에서  axios로 서버에 product경로로 get요청을 보내고

useAsync 커스텀훅에서 처리해서 state를 반환 해준다.

받아온 state에서 data로 상품이름과 이미지 좌표 가격을 띄워준다. 

(받아온 데이터는 배열의 형태로 저장되어 있어서 배열의 인덱스로 사용하거나

배열 구조분해할당으로 변수에 할당해서 사용해야 한다 )

[{p_id: 4, p_name: '기초스킨케어4', p_price: 70000, p_desc: '인기있는 상품 입니다.', p_quantity: 60, …}]

이런 형식으로 저장되어 있어서 data에서 바로 값으로 접근할 수는 없다.  data[0].p_id / Product.p_id

데이터베이스에서 받아온 값으로 화면 띄우기

 

제품 등록하기 )

 

제품을 등록하기 위해서는 클라이언트에서 보내주는 제품정보를 서버에서 받아서

데이터베이스에 저장해 주는 과정이 필요하다.

제품정보는 Post요청 으로 클라이언트에서 보내주고 Server에서도 Post로 받아서 

데이터베이스에 저장한다.

 

서버 측에 Post를 받는 구문을 추가해 준다.

Node.js > index.js

1
2
3
4
5
6
7
8
app.post("/addProduct",async (req,res)=>//req 가 가고 res 가 바로오는게 아니기때문에 async 붙여줘야함
    const {p_name,p_price,p_desc,p_img,p_quantity} = req.body;
    conn.query("insert into products (p_name,p_price,p_desc,p_img,p_quantity) values(?,?,?,?,?)",
    [p_name,p_price,p_desc,p_img,p_quantity],
    (error,result,fields)=>{
        res.send("ok");
    })
})
cs

req.body에 클라이언트가 보내주는 데이터가 담겨있다. 구조분해 할당으로  각 변수에 할당해 준다.

데이터베이스에 insert문으로 받아온 데이터를 추가해 준다.

 

쿼리문 작성형식

conn.query("쿼리문insert value(?,?,?)",[들어갈값],함수()=>{})

 

데이터베이스에 정상적으로 데이터가 입력되면 응답으로 ok를 보내준다.

인풋태그에 데이터를 모두 입력하고 form이 submit 될 때 데이터를 addProduct경로로

보내주도록 만들어준다.

 

클라이언트 측 

upload / index.js

import axios from 'axios';
import React,{useState} from 'react';
import { useNavigate } from 'react-router-dom';
import './index.css'


const UploadPage = () => {
    const navigate=useNavigate()
    const [ formData,setFormData] = useState({
        p_name:"",
        p_price:"",
        p_img:"",
        p_desc:"",
        p_quantity:""
    })
    const onChange = (e)=>{
        const {name,value} = e.target;
        setFormData({
            ...formData,
            [name]:value
        })
    }
    const onReset =()=>{
        setFormData({
            p_name:"",
            p_price:"",
            p_img:"",
            p_desc:"",
            p_quantity:""    
        })
    }
    const onSubmit = (e)=>{
        //form에 연결된 이벤트를 제거
        e.preventDefault()
        // 입력이 다 되어있는지 체크
        if(formData.p_name && formData.p_price && formData.p_img && formData.p_desc && formData.p_quantity){
            insertProduct();
        }
    }
    function insertProduct(){
        // 서버에 post요청
        axios.post("http://localhost:8080/addProduct",formData)
        .then(res=>{
            console.log(res); //콘솔에 응답출력
            navigate('/'); //메인화면으로 이동
        })
        .catch(e=>{
            console.log(e)
        })
    }
    return (
        <div className='upload'>
            <h2>제품등록하기</h2>
            <form onSubmit={onSubmit}>
                <table>
                    <thead></thead>
                    <tbody>
                    <tr>
                        <td>상품이름</td>
                        <td>
                            <input type="text" name="p_name" value={formData.p_name} onChange={onChange}/>
                            
                        </td>
                    </tr>
                    <tr>
                        <td>이미지</td>
                        <td>
                            <input type="text" name="p_img"  value={formData.p_img} onChange={onChange}/>
                            
                        </td>
                    </tr>
                    <tr>
                        <td>상품가격</td>
                        <td>
                            <input type="number" name="p_price" value={formData.p_price} onChange={onChange}/>
                            
                        </td>
                    </tr>
                    <tr>
                        <td>상품수량</td>
                        <td>
                            <input type="number" name="p_quantity" value={formData.p_quantity} onChange={onChange}/>
                        </td>
                    </tr>
                    <tr>
                        <td>상세설명</td>
                        <td>
                            <textarea name="p_desc" onChange={onChange} value={formData.p_desc}>
                                {formData.p_desc}
                            </textarea>
                        </td>
                    </tr>
                    <tr>
                        <td colSpan={2}>
                            <button type='submit'>등록</button>
                            <button type='reset' onClick={onReset}>취소</button>
                        </td>
                    </tr>
                   </tbody>
                    <tfoot></tfoot>
                </table>
            </form>
        </div>
    );
};

export default UploadPage ;

각 input태그의 value값을 state로 관리하고 값이 입력돼서 변경되면 onChange={onChange} 함수가 실행돼서 상태값이 변경되게 된다. 아래에 취소버튼을 누르면 상태값이 다 공백으로 바뀌는 onReset함수 도 만들어 줬다.

등록 버튼을 누르면 onSubmit이벤트가 실행되는데 기본적으로 실행되는 onSubmit이벤트가 아니고

따로 함수를 만들어줄 거 기 때문에 preventDefault( )로 기본적으로 실행되는 이벤트를 막아준다

인풋태그에 데이터가 전부 입력되어 있으면 insertProduct( ) 함수를 실행시켜 준다.

 

onSubmit

    const onSubmit = (e)=>{
        //form에 연결된 이벤트를 제거
        e.preventDefault()
        // 입력이 다 되어있는지 체크
        if(formData.p_name && formData.p_price && formData.p_img && formData.p_desc && formData.p_quantity){
            insertProduct();
        }
    }

 

insertProduction

    function insertProduct(){
        // 서버에 post요청
        axios.post("http://localhost:8080/addProduct",formData)
        .then(res=>{
            console.log(res); //콘솔에 응답출력
            navigate('/'); //메인화면으로 이동
        })
        .catch(e=>{
            console.log(e)
        })
    }

insertProduct 함수가 실행되면 axios.post로 경로에 입력된 상태값 데이터(formData)를 보내주게 되고.

응답을 받으면 콘솔에 응답을 출력해 주고 navigate로 홈화면으로 이동한다.

 

서버에 이미지 업로드하기)

이미지파일을 클라이언트가 가지고 있을게 아니라

서버에서 이미지파일을 가지고 있다가 보내줘야 하기 때문에 업로드 할때

이미지파일도 같이 업로드 해주어야 한다.

 

클라이언트부터 만들어주자. 아직 upload에 넣을게 아니고 테스트만 해보는 거라서

ImageForm.js를 components폴더 안에 만들어 주었다.

 

components/ImageForm.js

import axios from 'axios';
import React,{useState} from 'react';

const ImageForm = () => {
    const [imageURL,setimageURL] = useState(null)
    const onChangeImage = (e)=>{
        const{name} = e.target;
        //<form>태그생성
        const imageFormData = new FormData();
        //<form>태그에 속성 추가하기
        imageFormData.append(name,e.target.files[0]);
        axios.post('http://localhost:8080/upload',imageFormData,{
            Headers:{'content-type':'multipart/formdata'},
        }).then(res=>{
            console.log(res);
            setimageURL(res.data.imageURL);
        }).catch(e=>{
            console.log(e)
        })
    }
    return (
        <div>
            <table>
                <tr>
                    <td>file</td>
                    <td>
                        <input type="file" name="file" encType="multipart/form-data" onChange={onChangeImage}/>
                        {
                            imageURL ? <img src={`./images/${imageURL}`} alt="" width='200px' height='200px'/>:
                            (<div id="upload-img-bg">
                                <img src="images/cameraicons.png" alt="" width='200px' height='200px'/>
                            </div>)
                        }
                    </td>
                </tr>
            </table>
        </div>
    );
};

export default ImageForm;

 파일선택 버튼을 누르고 이미지 파일을 넣으면 onChange 이벤트가 실행되면서

onChangeImage함수가 실행된다.

    const onChangeImage = (e)=>{
        const{name} = e.target; //파일형식을 받음
        //<form>태그생성
        const imageFormData = new FormData();
        //<form>태그에 속성 추가하기
        imageFormData.append(name,e.target.files[0]);
        axios.post('http://localhost:8080/upload',imageFormData,{
            Headers:{'content-type':'multipart/formdata'},
        }).then(res=>{
            console.log(res);
            setimageURL(res.data.imageURL);
        }).catch(e=>{
            console.log(e)
        })
    }

input태그에서 바로 전송을 해주기 때문에 Form이 없어서

Form을 만들어서 거기에 데이터를 넣어서 보내준다.

        const imageFormData = new FormData();
        //<form>태그에 속성 추가하기
        imageFormData.append(name,e.target.files[0]);

이미지를 axios.post로 보내주고 Header에 콘텐츠의 타입을 정의해 준다. 이미지를 보내면

서버에서 응답으로 {imageURL:파일이름}을 보내준다.

응답을 받아서 이미지태그에 응답으로 받은 이미지이름 넣어준다.

{
      imageURL ? <img src={`./images/${imageURL}`} alt="" width='200px' height='200px'/>:
      (<div id="upload-img-bg">
         <img src="images/cameraicons.png" alt="" width='200px' height='200px'/>
      </div>)
}

 

이제 서버 측도 수정해줘야 한다 파일을 받아주려면 multer API를 설치해줘야 한다

 

GitHub - expressjs/multer: Node.js middleware for handling `multipart/form-data`.

Node.js middleware for handling `multipart/form-data`. - GitHub - expressjs/multer: Node.js middleware for handling `multipart/form-data`.

github.com

 

multer 설치하기

npm install --save multer

multer 불러오기

const multer = require("multer");

 

storage 생성하기

//diskstorage()--> 파일을 저장할때에 모든 제어 기능을 제공
const storage = multer.diskStorage({
    destination:(req,file,cb)=>{
        cb(null,'upload/');
    },
    filename:(req,file,cb)=>{
        const newFilename = file.originalname;
        cb(null,newFilename);
    }

})

파일을 저장할 경로와  저장할 파일의 이름을 설정해 줄 수 있다.

저장경로는 upload폴더 이고 

파일이름은 파일의 원래이름값인 file.originalname으로 해주었다.

 

upload객체 생성하기

const upload = multer({ storage : storage }); // {dest:storage} => {storage:storage}
app.post('/upload',upload.single('file'),async (req,res)=>{
    res.send({
        imageURL:req.file.filename
    })
});

이제 upload경로로 Post요청을 보내면 upload객체가 실행되면서 파일을 받아서 서버에서 저장해 준다.

 

클라이언트에서 파일을 올리면 서버로 보내져서 upload폴더에 저장되고 서버에서 응답으로 이미지의 이름을 보내주게 되고 클라이언트는 받은 이름으로 클라이언트자신의 images폴더에 접근해서 해당하는 이미지를 띄워준다.

일단은 테스트기 때문에 css도 없고 사실 파일도 클라이언트의 img폴더가 아니라 클라이언트가 이미지도 보내줘야하는 게 아닌가 싶긴한데 내일 학원가면 이어서 마저 가르쳐주실테니 일단 여기서 만족한다 multer를 쓰는방법은 링크의 공식문서를 읽어보면 있으니 찾아서 하면될것같고 요즘 하면서 점점느끼는게 외우는 걸 포기하게 되는거다.. ㅋㅋㅋ 어떻게 다외우나 싶어서 선생님께 여쭤봤더니 외우면서 하는게 아니라 그때그때 잘 찾아서 써야 한다고 한다. 이제 외우려고 노력하지 말고 열심히 블로그에 기록하고 다른 블로그에 좋은 포스팅이 있으면 저장해 뒀다가 잘 베껴서 써야겠다 코딩은 정말 엄청 두꺼운 책으로 오픈북 시험을 치는 느낌이다.. ㅠ

반응형
반응형

화장품을 파는 쇼핑몰 사이트를 만들어보자!!

메인페이지, 상품페이지, 상품업로드 페이지를 만들고 각 페이지는 폴더를 따로 만들어서 저장한다.

컴포넌트폴더를 만들고  HeaderFooter (상단화면 하단화면)을 만들어 준다.

메인페이지에 들어갈 상품태그도 컴포넌트로 만들어서 컴포넌트 폴더에 넣어주겠다.

 

코드샌드박스로 코드를 올릴 텐데 서버는 에러가뜨고 로컬에 있는 mysql이랑 어떻게 연결하는지 몰라서 리액트만 코드샌드박스로 올리고 서버와 mysql연결하는 부분은 코드로 올리겠다..

 

우선 headerfooter main page를 만들어준다

 

Header.js /Header.css

import React from 'react';
import { Link } from 'react-router-dom';
import './Header.css';
const Header = () => {
    return (
        <div className='header'>
            <h1><Link to='/'>Cosmeticsv</Link></h1>
            <ul>
            <li>스킨케어</li>
            <li>메이크업</li>
            <li><Link to='/upload'>제품등록</Link></li>
            </ul>
        </div>
    );
};
export default Header;

-----------------------------------------------------------------------------------------------

Header.css
a{
    text-decoration: none;
    color: inherit;
}
.header{
    display: flex;
    width: 100%;
    max-width: 1200px;
    padding: 20px;
    justify-content: space-between;
    margin: 0 auto;
}
.header ul{
    display: flex;
}
.header li{ padding:0 20px; list-style: none; }

Header에서는 제목을 클릭하면 메인화면으로 빠져나갈 수 있게 Link를 넣어주었고 제품등록을 클릭하면 제품등록창으로 이동할 수 있게 Link를 넣어주었다.

 

Footer.js /Footer.js

import React from 'react';
import './Footer.css'

const Footer = () => {
    return (
        <div className='footer'>
            <div className='info'>
                <div className='inner'>
                    <div >
                        <h2>무통장입금계좌</h2>
                        <div>
                            <p>BANK ACCOUNT</p>
                            <p>301-1234-5678-01</p>
                            <p>예금주 - 김혜라</p>
                        </div>
                    </div>
                    <div>
                        <h2>고객센터</h2>
                        <div>
                            <p>영업시간 이외에는 문의 게시판을 이용해주시면 당담자 확인 후 빠른 답변 도와드리겠습니다.</p>
                            <p>02-1263-1245</p>
                        </div>
                    </div>
                    <div>
                        <h2>공지사항</h2>
                        <ul>
                            <li>조명가이드 2022-06-20</li>
                            <li>신상품 입고 안내 2022-06-10</li>
                            <li>몰 오픈을 축하드립니다. 2022-02-20</li>
                        </ul>
                    </div>
                </div>
            </div>
            <div className='footermenu'>
                <div className='inner'>
                    <ul>
                        <li>홈</li>
                        <li>매장안내</li>
                        <li>이용약관</li>
                        <li>개인정보처리방침</li>
                    </ul>
                </div>
            </div>
            <div className='address'>
                <div className='inner'>
                상호 :  주소 : 울산광역시 남구 어딘가 대표전화 : 국번없이 123-456-7891 대표이사 : 김OO 개인정보관리자 : 이OO 사업자 등록번호 : 102-12-12345 copyright(c) Greck Lamp,.LTD all rights reserved.
                </div>
            </div>
        </div>
    );
};

export default Footer;
----------------------------------------------------------------------------------------------------------------------
Footer.css

.footer {
    margin-top: 60px;
    text-align: left;
}
.info{
    border-top:3px solid #333;
    border-bottom:1px solid #333;
    padding: 20px 0;
}
.inner{
    width:100%;
    max-width:1200px;
    margin: 0 auto;
}
.info .inner{
    display: flex;
    justify-content: space-between;
}
.info .inner > div{
    width: 32%;
    padding-bottom:20px ;
    line-height: 1.8;
}
.info .inner h2{
    border-bottom: 1px solid #ccc;
    line-height: 50px;
    font-size: 20px;
}

.info .inner > div div{
    padding:20px 0 ;
}
.info .inner > div ul {
    padding: 20px 0;
}
.footermenu ul{
    display: flex;
    height:60px;
    align-items: center;
}
.footermenu ul li{
    padding: 0 20px;
    border-right: 1px solid #ccc;
}
.footermenu ul li:last-child{
    border-right:none;
}
.address{
    border-top:1px solid #ccc;
    background-color: #eee;
    padding: 30px 0;
}

 

Footer는 좀 긴데 일반 사이트처럼 별로 기능은 들어간게없고 그냥 정보를 알려주는 용도이다. 

 

ProductList.js

import React from 'react';
import { Link } from 'react-router-dom';

const ProductList = ({p_id,p_price,p_name}) => {
    return (
        <li>
            <Link to={`/detailView/${p_id}`}>
                <img src={`../images/cosmetic${p_id}.JPG`} alt="" />
                <h3>{p_name}</h3>
                <p>{p_price}원</p>
                <p>간단한 설명입니다.</p>
            </Link>
        </li>
    );
};

export default ProductList;

ProductList는 메인페이지 의 ul태그에 넣어줄 컴포넌트이다 같은 li태그 구조가 반복되기 때문에

컴포넌트로 만들고 달라지는 이미지와 상품명 가격은 props로 받아서 넣어준다.

 

HeaderFootercomponents폴더에 넣어서 보관해준다.

 

index.js / index.css (src/main폴더)

import React from 'react';
import { Link } from 'react-router-dom';
import ProductList from '../components/ProductList';
import './index.css';

const data=[ //이 데이터를 서버에서 받아올거임
	{
		p_id:"1",
    	p_name:"상품1",
        p_price:"10000",
    },
    {
		p_id:"2",
    	p_name:"상품2",
        p_price:"20000",
    },
    {
		p_id:"3",
    	p_name:"상품3",
        p_price:"30000",
    },
    {
		p_id:"4",
    	p_name:"상품4",
        p_price:"40000",
    },
]

const MainPage = () => {    
    return (
        <div className='main'>
            <div className='visual'>
                <img src ="images/banner1.jpg" alt="배너이미지1"/> 
            </div>
            <div className='product'>
                <h2>신상품</h2>
                <ul>
                    {data.map(pro=>
                    <ProductList key={pro.p_id} p_id={pro.p_id} p_name={pro.p_name} p_price={pro.p_price}/>)}
                </ul>
            </div>
        </div>
    );
};

export default MainPage;

----------------------------------------------------------------------------------------------------------
index.css

.visual img{
    width: 100%;
}
.product{
    width:100%;
    max-width: 1200px;
    margin: 0 auto;
}
.product h2{
    padding-bottom:40px
}
.product ul {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
}
.product ul li{
    width:25%;
    /* border: 1px solid #ddd; */
    padding-bottom: 20px;
}
.product ul li img{
    width: 100%;
}
.product ul li h3{
    padding: 10px;
}
.product ul li p {
    padding: 0 10px;
}

 

 

원래 mainpage에 들어갈 이미지는 mysql에서 받아와야 하는데 코드샌드박스로는 할 수 없기 때문에

data 객체 를 따로 코드 안에 만들어 주었다. 

 

이제 상품이미지를 클릭했을 때 상품상세화면으로 들어가게 해 주겠다. 

ProductList.js에서 이미지를 클릭할 때 이동할 링크를 '/detailView/${p_id}'로 줬기 때문에

App에서도 <Route path = '/detailView/:p_id' element=(<ProductPage/>)/>를 넣어서 받아준다

function App() {
  return (
    <div className="App">
      <Header/>
      <Routes>
        <Route path='/' element={<MainPage/>}/>
        <Route path='/detailView/:p_id' element={<ProductPage/>}/>
      </Routes>
      <Footer/>
    </div>
  );
}

 

이제 ProductPage를 만들어보자

 

index.js /index.css (src / product폴더)

import React from 'react';
import { useParams } from 'react-router-dom';
import './index.css'

const ProductPage = () => {
    const {p_id} = useParams();

    return (
        <div className='productDetail'>
            <h2>기초스킨케어 세트</h2>
            <div className='productImg'>
                <img src={`../images/cosmetic${p_id}.JPG`} alt="" />
            </div>
            <div>
                <p>스킨케어 주간 베스트</p>
                <p>헤라 스킨</p>
                <p>가격 : 65,000</p>
                <p>무료배송</p>
            </div>
        </div>
    );
};

export default ProductPage;
-------------------------------------------------------------------------------------------------

.productDetail{
    margin: 0 auto;
    width:100%;
    max-width: 1200px;
}
.productDetail h2{
    padding-top: 30px;
    padding-bottom: 30px;
}
.productImg{
    text-align: center;
}
.productImg img{
    border-radius:50% ;
}

ProductPage에서는 ProductLists에서 Link에  /detailView/${p_id}를 넣어준 것을

useParams( )로 객체로 받아서 값을 

p_id에 할당해 주고 이미지 src를 설정하는 데 사용한다.

 

상품을 추가해 주는 upload페이지도 만들어 보겠다.

 

index.js/index.css (src / upload폴더)

import React from 'react';
import './index.css'

const UploadPage = () => {
    return (
        <div className='upload'>
            <h2>제품등록하기</h2>
            <form>
                <table>
                    <thead></thead>
                    <tbody>
                    <tr>
                        <td>상품사진</td>
                        <td>
                            <input type="file" name="p_img" />
                            
                        </td>
                    </tr>
                    <tr>
                        <td>상품이름</td>
                        <td>
                            <input type="text" name="p_name" />
                            
                        </td>
                    </tr>
                    <tr>
                        <td>상품소개</td>
                        <td>
                            <input type="text" name="p_info" />
                            
                        </td>
                    </tr>
                    <tr>
                        <td>상품가격</td>
                        <td>
                            <input type="number" name="p_price" />
                            
                        </td>
                    </tr>
                    <tr>
                        <td>상품수량</td>
                        <td>
                            <input type="number" name="p_quantity" />
                        </td>
                    </tr>
                    <tr>
                        <td>상세설명</td>
                        <td>
                            <textarea name="p_desc" ></textarea>
                        </td>
                    </tr>
                    <tr>
                        <td colSpan={2}>
                            <button type='submit'>등록</button>
                            <button>취소</button>
                        </td>
                    </tr>
                   </tbody>
                    <tfoot></tfoot>
                </table>
            </form>
        </div>
    );
};

export default UploadPage ;

------------------------------------------------------------------------------------------------

.upload{
    margin: 40px auto;
    width: 100%;
    max-width:1200px;
}
.upload h2{
    font-size: 24px;
    padding-bottom:20px;
}
.upload table{
    border-collapse: collapse;
    width: 100%;
}
.upload td{
    border-bottom: 1px solid #ccc;
    padding: 16px;
}
.upload td:nth-child(1){
    width:20%;
}
.upload input[type='text'], .upload textarea{
    width:100%;
}

upload페이지는 아직 형태만 만들어놓고 아무 기능도 추가하지 않았다

나중에는 추가했을 때 서버로 데이터가 전송되도록 해줄 것 같다.

upload페이지로 이동하는 주소는 Header에서  <Linkto='/upload'> 제품등록 </Link>으로 주고 있다. 

 

App.js 

import './App.css';
import Header from './components/Header';
import Footer from './components/Footer';
import MainPage from './main';
import ProductPage from './product';
import { Routes,Route } from 'react-router-dom';
import UploadPage from './upload';

function App() {
  return (
    <div className="App">
      <Header/>
      <Routes>
        <Route path='/' element={<MainPage/>}/>
        <Route path='/detailView/:p_id' element={<ProductPage/>}/>
        <Route path='/upload' element={<UploadPage></UploadPage>}/>
      </Routes>
      <Footer/>
    </div>
  );
}

export default App;

App에서 일치하는 경로에 따라 Route들을 렌더링 해주고 있다. 

 

 

 

 

데이터를 서버에서 받아오기

 

서버코드

//express 서버 만들기
const express = require("express");//import express
const cors = require("cors");

//mysql부르기
const mysql = require("mysql");

//서버 생성 --> express( )호출
const app = express();
//프로세서의 주소 포트번호 지정
const port = 8080;
// JSON형식의 데이터를 처리할수 있도록 설정
app.use(express.json());
// 브라우저의 CORS 이슈를 막기 위해 사용하는 코드
app.use(cors());

//sql 연결선 만들기
const conn = mysql.createConnection({
    host:"localhost",
    port:'3306',
    user:"root",
    password:"1234",
    database:"shopping"
})
//sql 연결하기 
conn.connect();



// get요청시 응답 app.get(경로,콜백함수)
app.get('/products',(req,res)=>{
    conn.query('select * from products',function(error,result,fields){
        res.send(result);
    });
})


//서버구동
app.listen(port,()=>{
    console.log("서버가 돌아가고있습니다.")
})

(서버실행방법: 터미널에 node 서버이름.js 입력) 

서버에서 로컬에 있는 데이터베이스에 접근해서 데이터를 가져와서 응답으로 보내준다.

 

서버에서 보내주는 값은 사이트에서 axios로 받아온다

 

커스텀훅 

useAsync.js

import React, { useEffect, useReducer } from 'react';
//1.상태초기화
const initialState = {
    loading:false,
    data:null,
    error:null,
}
//2.리듀서 함수구현
//로딩중일때 상태
//데이터를 성공적으로 받았을때 상태
//에러일때 상태
function reducer(state,action){
    switch(action.type){
        case "LOADING":
            return{
                loading:true,
                data:null,
                error:null,
            }
        case "SUCCESS":
            return{
                loading:false,
                data:action.data,
                error:null,
            }
        case "ERROR":
            return{
                loading:false,
                data:null,
                error:action.error,
            }
        default:
            return state;
    }
}


const useAsync = (callback,deps=[]) => {
    const [state,dispatch] = useReducer(reducer,initialState);
    //데이터 요청 함수
    const fetchData = async ()=>{
        //로딩의 value를 true로 업데이트
        dispatch({
            type:"LOADING"
        });
        // try catch 에러 처리 구문 ,,데이터를 요청하는 과정은 에러발생할 확률이 높음
        try{
            //axios에서 받는 함수가 들어감 useAsync를 사용하는 컴포넌트에서 각각 다른 주소를 넣어서 보내줌
            const data = await callback(); 
            dispatch({
                type:"SUCCESS",
                data  //data : data 인데 같으니까 생략가능함
            })
        }
        catch(e){
            dispatch({
                type:"ERROR",
                error:e
            })
        }
    }
    useEffect(()=>{
        fetchData();
    },deps)
    return state;
};

export default useAsync;

데이터를 받았을 때 상태를 바꿔주는 커스텀 훅을 만들어주고 

 

main.js

import axios from 'axios';
import React from 'react';
import { Link } from 'react-router-dom';
import ProductList from '../components/ProductList';
import useAsync from '../customHook/useAsync'
import './index.css';

async function productFetch(){
    const response = await axios.get("http://localhost:8080/products");
    return response.data
}

const MainPage = () => {
    const state = useAsync(productFetch,[])
    const{loading,error,data}=state;
    if (loading) return <div>로딩중</div>
    if (error) return <div>에러발생</div>
    if (!data) return null
    console.log(data)
    return (
        <div className='main'>
            <div className='visual'>
                <img src ="images/banner1.jpg" alt="배너이미지1"/>
            </div>
            <div className='product'>
                <h2>신상품</h2>
                <ul>
                    {data.map(pro=>
                    <ProductList key={pro.p_id} p_id={pro.p_id} p_name={pro.p_name} p_price={pro.p_price}/>)}
                </ul>
            </div>
        </div>
    );
};

export default MainPage;

메인화면에서 useAsync를 import 해서 사용 useAsync에 파라미터로 입력한 주소의 데이터를 얻어서

리턴해주는 함수와 빈 배열을 넣어준다. 이렇게 넣어주면 useAsync의 콜백함수로 들어가서

주소에서 받아온 데이터가 data상태값에 들어가게 되고 useAsync에서 반환해 주는 state값을

Mainpage에서 받아서 이용할 수 있게 된다.

 

 

 

 

 

반응형
반응형

노드서버 만들기


브라우저가 아닌 곳에서도 node.js를 이용하면 javascript 를 실행할 수 있다.
노드에서는 모듈을 로딩하는 방법에서 차이가 있다 import 대신 require 사용

 

모듈로딩

es6 방식

export functuin Hello( ){ }
import Hello from './Hello.js'

common js 방식

function hello( ){ }
module.export = hello;
const hello = require('/hello.js')

 

서버를 만들어줄 폴더를 생성 
생성한 폴더에서 npm init으로 노드를 초기화해 준다.
RestAPI [post, get,] 데이터를 어떻게 받아오고 보낼지 만드는 것

 

프로토콜://호스트주소:포트번호/경로? 쿼리
http://localhost:8080/product? name=item

 

http: 80 포트번호가 생략
https :433 포트번호가 생략

Server.js

//node는 common JS를 사용함
//불러올때 require를 사용
const http = require('http');
//본인 컴퓨터 주소를 의미함!!
const hostname = "127.0.0.1"; //localhost
const port = 8080; //포트번호
//createServer() --> 서버생성
//요청정보 req, 응답정보 res
const server = http.createServer(function(req,res){
    const path = req.url;
    const method = req.method;
    if(path==="/products"){ //주소가 /products일때
        //응답을 보낼때 json 객체를 보낼거임
        res.writeHead(200,{'Content-type':'application/json'})//json파일을 다룰수있게해줌
        //객체를 json파일 로 변환 JSON.stringify(obj)
        const product = JSON.stringify({
            name:"기초화장품",
            price:50000
        })
        res.end(product); //응답을 보내줌 .end( )
    }else{
        res.end('hahahahahahahahah');
    }
})

//listen은 대기 호스트네임과 포트 번호 요청을 기다림
server.listen(port,hostname); //서버를 응답할수잇도록 대기상태로 만듬.
console.log("화장품 서버가 동작 중입니다.") //서버를 시작했을때 터미널에 뜨는문자

이렇게 넣어주고 node 파일이름 을 터미널에 입력해 주면 서버가 실행된다.

서버가 실행되면 해당주소인 http://localhost:8080에 들어가면 접근할 수 있고

path조건에 /product를 달아놔서 http://localhost:8080/products로 들어가면 JSON객체가 뜨게 되고

그 외 에는 'hahahaha'가 뜨는 창으로 이동한다.

 

지금은 좌표를 /product로 지정해 놔서 바로 JSON객체를 보여준다 hahaha가 뜨는 걸 보고 싶으면

open Sandbox버튼을 누르고 들어가서 주소에서/products를 지워주면 hahaha가 보인다.

기본좌표 화면

 

이게 서버를 만드는 방법인데 설정해 줄 것도 많고

불편해서 보통은 express라는 라이브러리 프레임워크를 쓴다고 한다.

 

Express프레임워크

npm install express
npm install cors

 

index.js

//express 서버 만들기
const express = require("express");//import express
const cors = require("cors"); //import cors


//서버 생성 --> express( )호출
const app = express(); 
//프로세서의 주소 포트번호 지정
const port = 8080;
// JSON형식의 데이터를 처리할수 있도록 설정
app.use(express.json());
// 브라우저의 CORS 이슈를 막기 위해 사용하는 코드
app.use(cors());




// get요청시 응답 app.get(경로,콜백함수)
app.get('/',(req,res)=>{ //기본경로 
        res.send([ //클라이언트에게 응답을 보내줌
            {
                id:"1",
                name:"제품1",
                price:"10000"
            },
            {
                id:"2",
                name:"제품2",
                price:"20000"
            },
            {
                id:"3",
                name:"제품3",
                price:"30000"
            },
            {
                id:"4",
                name:"제품4",
                price:"40000"
            }
        ])
});

app.get("/products/:id",(req,res)=>{
    const params = req.params; //주소창에서보내주는 파라미터를 받아줌 {id:입력한값}
    const {id} = params;
    res.send(`id는${id}이다`)
})

//서버구동
app.listen(port,()=>{ //서버대기
    console.log("서버가 돌아가고있습니다.")
})

node index.js로 서버실행(터미널에 입력)

이번코드는 샌드박스에서 컴파일이 안되고 에러가 떠서 비주얼스튜디오로 해서

실행화면만 캡처했다.(왜 안될까..)

 

JSON객체를 반환

 

똑같이 입력하고 서버를 실행하면 나랑 똑같은 화면이 안 나올 수도 있는데 나는 JSONView라는

크롬 확장프로그램을 써서 보기 편하게 해 두었다. 확장프로그램이 없다면 한 줄로 쭈욱 나올 듯..

(크롬 웹스토어에 들어가서 검색하면 설치할수있다!)

 

URL파라미터 받기 (params)

서버에서도 url에 입력된 파라미터를 받아줄 수 있다. 

app.get("/products/:id",(req,res)=>{
    const params = req.params; //주소창에서보내주는 파라미터를 받아줌 {id:입력한값}
    const {id} = params;
    res.send(`id는${id}이다`)
})

params를 받아주는 구문이다 /products 뒤에 파라미터가 담기면 id키로 받고

값은 req에 있는 params에 저장된다.

req.params로 객체를 받아서 id에 구조분해할당을 해주는 구문이다.

 

req를 console.log로 콘솔에 띄워보면 아래 그림처럼 값들이 나온다

 

req.params

products/1로 접속하면 위 사진처럼 params에 값이 나온다.

 

express서버에서  mysql 연동

 

1.database 설계

1) create database shopping
2) crate table products( )
3) insert문으로 데이터 생성

 

예제 테이블

 

데이터베이스 생성

create database shopping

use shopping

테이블 생성

create table products(
     p_id int primary key auto_increment,
     p_name varchar(20) not null,
     p_price int not null,
     p_desc text ,
     p_quantity int not null,
     p_img varchar(100)
)

데이터삽입

insert into products(p_name, p_price, p_desc, p_quantity, p_img)
values("기초스킨케어 1",50000, "인기 있는 상품입니다.",80, "images/cosmetic1.JPG");
insert into products(p_name, p_price, p_desc, p_quantity, p_img)
values("기초스킨케어 2",70000, "인기 있는 상품입니다.",50, "images/cosmetic2.JPG");
insert into products(p_name, p_price, p_desc, p_quantity, p_img)
values("기초스킨케어 3",40000, "인기 있는 상품입니다.",30, "images/cosmetic3.JPG");
insert into products(p_name, p_price, p_desc, p_quantity, p_img)
values("기초스킨케어 4",70000, "인기 있는 상품입니다.",60, "images/cosmetic4.JPG");

 

똑같이 입력해주면 아래와같은 데이터베이스 테이블이 하나 생성된다.

예제 테이블

2.express서버에 mysql설치

npm install mysql

 

1) mysql 라이브러리 불러오기 //const mysql = require("mysql");
2) mysql접속 생성 

mysql.createConnection({
	host:"localhost",
	user:"root",
	password:"1234",
	database:"shopping",
	port:"3306"
})


3) mysql 접속 conn.connect( )
4) 쿼리 전송

app.get('/products',(req,res)=>{
	conn.query(쿼리문,함수(error,result,field){ 
	res.send(result)
	})   // field=table의 컬럼이름
})

 

완성코드)

index.js

//express 서버 만들기
const express = require("express");//import express
const cors = require("cors");

//mysql import
const mysql = require("mysql");
//서버 생성 --> express( )호출
const app = express();
//프로세서의 주소 포트번호 지정
const port = 8080;
// JSON형식의 데이터를 처리할수 있도록 설정
app.use(express.json());
// 브라우저의 CORS 이슈를 막기 위해 사용하는 코드
app.use(cors());

//sql연결선
const conn = mysql.createConnection({
    host:"localhost",
    port:"3306",
    user:"root",
    password:"1234",
    database:"shopping"
})
//sql 연결하기
conn.connect()


// get요청시 응답 app.get(경로,콜백함수)
app.get('/products',(req,res)=>{
    conn.query('select * from products',function(error,result,field){
        if(error){
            res.send(error)
        }
        res.send(result);
        console.log(result);
    })    
});

app.get("/products/:id",(req,res)=>{
    const params = req.params;
    const {id} = params;
    res.send(`id는${id}이다`)
})

//서버구동
app.listen(port,()=>{
    console.log("서버가 돌아가고있습니다.")
})

아까 객체로 보내줬던 값을 이번에는 데이터베이스에서 조회해서 페이지로 전달해주고 있다.

배열에 데이터베이스에서 받아온 값들이 객체로 들어가서 전달된다.

sql을 받아서 출력한값

 

 

여기까지 기본서버와 express서버를 만들었고 url파라미터를 받는 방법과 mysql을 연결해서 데이터베이스를 받아오는 걸 공부해 봤다. 다음포스팅은 데이터베이스에서 받아온 데이터를 서버에 올려주고 서버에 올린 값을 리액트에서 axios로 받아서 이용하는 페이지를 만들어 보겠다. 

 

 

 

 

 

 

나는 프런트엔드 수업을 듣고 있어서 서버를 만드는 방법은 배웠지만 이 서버가 어떤 원리로 동작하고 어떤 코드가 정확하게 어떤 역할을 하는지는 간단한 설명만 듣고 넘어갔다. 아마 백엔드는 이것보다 배울게 많겠지.. 지금은 리액트를 하느라 바쁘지만 나중에 조금 시간이 생긴다면 서버에 대해서 조금 더 공부해보고 싶다.

반응형
반응형

리액트 라우터

각각의 url에 따라 선택된 데이터를 하나의 페이지에서 렌더링 해주는 라이브러리
SPA(Single Page Application)에서 새로운 페이지를 로드하지 않고

하나의 페이지 안에서 필요한 컴포넌트만 가져와서 다른 페이지를 나타내는 방식을 지원.

*라우팅 : 사용자가 요청한 url에 따라 해당 url에 맞게 페이지를 보여주는 것
이전 MPA(Multiple Page Application)에서는 새로운 페이지를 로드하며 페이지를 이동하는 방식
<a href="sub.html"> 서브페이지 </a>

 

 

Home v6.6.2

I'm on v5 The migration guide will help you migrate incrementally and keep shipping along the way. Or, do it all in one yolo commit! Either way, we've got you covered to start using the new features right away.

reactrouter.com

 

1) 라우터 설치하기

npm install react-router-dom



2) 라우터 사용하기

1. 상위 컴포넌트에서 라우터를 적용 <BrowserRouter> 컴포넌트
ex>
<BrowserRouter>
            <App></App>
</BrowserRouter>

2. 경로에 따라 보일 component를 설정 <Routes><Route> 컴포넌트
Routes컴포넌트는 여러 Route를 감싸서 그중 url이 일치하는 라우트 단 하나만 렌더링 시켜줌
Route는 path속성에 경로element속성에 컴포넌트를 지정

<Routes>
	<Route path="경로" element={<component /> } /> //path가 일치하면 element에 있는 컴포넌트를 렌더링함
	<Route path="경로" element={<component /> } />
</Routes>

 

예제) 

우선은 주소로 이동했을 때 보여줄 페이지를 만들어 주자 pages폴더를 만들어서 폴더 안에서 관리해 준다.

Home.js

import React from "react";
import { Link } from "react-router-dom";

const Home = () => {
  return (
    <div>
      <h2>홈</h2>
      <p>가장 먼저 보여지는 페이지입니다.</p>
      <Link to="/product">제품페이지</Link>
    </div>
  );
};

export default Home;

Home은 기본주소일 때 보여줄 거 기 때문에 <Router path=' '  element={<Home/>}/>으로

App에  넣어주면 된다.

 

홈에서는 제품페이지로 이동할 수 있는 링크가 있고 링크 주소는 해당주소에서 /product로 이동한다.

 

Product.js

import React from "react";
import { Link } from "react-router-dom";

const Product = () => {
  return (
    <div>
      <h2>제품페이지</h2>
      <p>제품페이지입니다</p>
      <Link to="/">홈</Link>
    </div>
  );
};

export default Product;

Product는 링크를 타고 /product로 이동했을 때 렌더링되도록

<Router path='/product' element={<Product/>}/>으로 App에 넣어 준다.

Product에서 다시 홈으로 돌아갈 수 있도록 링크를 달아주었다.

 

App.js

import "./styles.css";
import { Route, Routes } from "react-router-dom";
import Home from "./pages/Home";
import Product from "./pages/Product";

export default function App() {
  return (
    <div className="App">
      <Routes>
        <Route path="" element={<Home />} />
        <Route path="/product" element={<Product />} />
      </Routes>
    </div>
  );
}

Routes안에 Route들을 넣어주면 안에서 Path조건에 맞는 한 개의 Route만 렌더링 된다.

 

그리고 꼭 까먹지 말고 상위컴포넌트를 BrowserRouter로 감싸줘야 한다.

index.js

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";

import App from "./App";

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>
);

URL 파라미터와 쿼리 스트링 (주소창에 데이터 전송)

페이지 주소를 정의할 때 가끔은 변하는 유동적인 값을 사용해야 할 때도 있다.
1 ) URL 파라미터 예시
Link에서 value를 전달

<Link to='/product/item1'> //


Route에서 키를 전달

<Route path='/product/:productname' element={<Product></Product>}/>


useParams( ); --> 객체를 리턴 // 해당주소가 들어가는 컴포넌트에서 값을 받음 아래의 객체를 리턴함

{
   productname:item1
}

Product.js

import React from 'react';
import { Link, useParams } from 'react-router-dom';
const data= {
    item1:{
        title:"아이폰",
        price: "150만원",
        desc:"비싸요 예뻐요"
    },
    item2:{
        title:"갤럭시",
        price:"30만원",
        desc:"싸고 예뻐요"
    }
}

const Product = () => {
    const {productname} = useParams(); //객체를 리턴 {productname:'item1'}
    const product = data[productname]
    return (
        <div>
            <h2>제품페이지</h2>
            <p>제품페이지 입니다.</p>
            {product ? <div>
                <h3>{product.title}</h3>
                <p>{product.price}</p>
                <p>{product.desc}</p>
            </div>:<p>존재하지 않는 페이지입니다.</p>}
            
        </div>
    );
};

export default Product;

Product에 data객체를 하나 만들어주고 useParams( )로 받아온 값을 data객체의 key값으로 넣어서

필요한 값을 받아왔다.

 

두 개의 값 중에 선택해서 받을 수 있도록 링크를 하나 더 만들어주자.

링크하나는 product/item1로 이동하고

두 번째는 product/item2로 이동하게 만들어준다.

 

2) 쿼리스트링 예시

<Link to='/about?name=green&age=30'>About</Link>

 

useLocation(  ) => 객체 리턴 location객체랑 똑같이 나옴

. search로 접근 ?name=green&age=30이렇게 나옴

useSearchParams( ) 배열을 리턴해줌--> [searchParams, setSearchParams]
searchParams.get("받아온 키값") //name, age

 

About.js

import React from "react";
import { useLocation, useSearchParams } from "react-router-dom";

const About = () => {
  const location = useLocation();
  const [searchParams, setParams] = useSearchParams();
  const name = searchParams.get("name");
  const age = searchParams.get("age");
  console.log(location);
  return (
    <div>
      <h2>소개페이지</h2>
      <p>소개페이지 입니다.</p>
      <p>{location.search}</p>
      <p>name값은{name}</p>
      <p>age값은{age}</p>
    </div>
  );
};

export default About;

쿼리스트링을 받을 페이지로 About컴포넌트를 만들어줬다. 쿼리스트링값을 두 가지 방법으로 받는데 

useLocation

useSearchParams로 받았다.

useLocation  은 location 객체를 받아주고  

useSearchParams는 location에서  search를 따로따로 떨어트려서 반환해 준다 반환해 준 값은. get(키값)

 


중첩된 루트 <outlet/>

app --> subpage --> subpage1
                                 subpage2
                                 subpage3

subpages 안에 subpage를 넣어주고  subpage컴포넌트 리턴 부분에 outlet을 넣어줌

<Route path='/subpages' element={<Subpages/>}>
      <Route path='/subpages/:id' element={<Subpage/>}/>
</Route>
return (
        <div>
            <li><NavLink to='/subpages/1'
            style={({isActive})=> isActive? activeStyle:undefined}
            >서브페이지1</NavLink></li>
            <li><NavLink to='/subpages/2'
            style={({isActive})=> isActive? activeStyle:undefined}
            >서브페이지2</NavLink></li>
            <li><NavLink to='/subpages/3'
            style={({isActive})=> isActive? activeStyle:undefined}
            >서브페이지3</NavLink></li>
            <Outlet/>
        </div>
    );


예제) 

우선은 링크가 많아지니 링크를 모아서 header컴포넌트에 넣어주도록 하겠다

Header.js

import React from "react";
import { Link } from "react-router-dom";

const Header = ({ sitename }) => {
  return (
    <div>
      <h1>{sitename}</h1>
      <ul>
        <li>
          <Link to="/">home</Link>
        </li>
        <li>
          <Link to="/about?name=green&age=30">About</Link>
        </li>
        <li>
          <Link to="/product/item1">iphone</Link>
        </li>
        <li>
          <Link to="/product/item2">galaxy</Link>
        </li>
        <li>
          <Link to="/subpages">서브페이지</Link>
        </li>
      </ul>
    </div>
  );
};

export default Header;

각 페이지로 이동하는 링크들을 모두 헤더에 넣어주고 h1태그에는 Props로 이름을 받아와서 넣어준다.

 

Subpages.js

import React from "react";
import { Link, Outlet } from "react-router-dom";

const Subpages = () => {
  return (
    <div>
      <li>
        <Link to="/subpages/1">서브페이지1</Link>
      </li>
      <li>
        <Link to="/subpages/2">서브페이지2</Link>
      </li>
      <li>
        <Link to="/subpages/3">서브페이지3</Link>
      </li>
      <Outlet />
    </div>
  );
};

export default Subpages;

 

subpages에서 각 서브페이지로 이동할 수 있는 링크를 만들고 서브페이지가 렌더링 되면 subpages내부에 렌더링 되기 때문에 <Outlet/>을 써서 보여준다.

Subpage.js

import React from "react";
import { useParams } from "react-router-dom";

const Subpage = () => {
  const { id } = useParams();
  return (
    <div>
      <h2>서브페이지{id}</h2>
    </div>
  );
};

export default Subpage;

보내주는 url값을 useParams로 받아서 id에 할당해 주고 h2태그에 넣어준다.

 

App.js

import "./styles.css";
import { Route, Routes } from "react-router-dom";
import Home from "./pages/Home";
import Product from "./pages/Product";
import About from "./pages/About";
import Header from "./components/Header";
import Subpage from "./pages/Subpage";
import Subpages from "./pages/Subpages";

export default function App() {
  return (
    <div className="App">
      <Header sitename="green" />
      <Routes>
        <Route path="" element={<Home />} />
        <Route path="/product/:productname" element={<Product />} />
        <Route path="/about" element={<About />} />
        <Route path="/subpages" element={<Subpages />}>
          <Route path="/subpages/:id" element={<Subpage />} />
        </Route>
      </Routes>
    </div>
  );
}

Route subpages 태그 안에 Route  Subpage를 넣어주었다. 이렇게 해주면

Route subpages가 렌더링 될 때 subpages도 조건이 일치하면 같이 렌더링 될 수 있다. (Outlet 써줘야 함)

 

useNavigate( )

Link컴포넌트를 사용하지 않고 다른 페이지로 이동을 해야 할 때 사용하는 Hook

const navigate = useNavigate();
navigate(-1)      //이전페이지로 이동
navigate(1)        //다음페이지로 이동
navigate('/product') 경로로 이동

 

Header.js

import React from "react";
import { Link, useNavigate } from "react-router-dom";

const Header = ({ sitename }) => {
  const navigate = useNavigate();
  const goback = () => {
    //이전페이지 이동
    navigate(-1);
  };
  const goSubpage = () => {
    //서브페이지 이동
    navigate("/subpages");
  };
  return (
    <div>
      <h1>{sitename}</h1>
      <ul>
        <li>
          <Link to="/">home</Link>
        </li>
        <li>
          <Link to="/about?name=green&age=30">About</Link>
        </li>
        <li>
          <Link to="/product/item1">iphone</Link>
        </li>
        <li>
          <Link to="/product/item2">galaxy</Link>
        </li>
        <li>
          <Link to="/subpages">서브페이지</Link>
        </li>
      </ul>
      <div>
        <button onClick={goback}>뒤로가기</button>
        <button onClick={goSubpage}>subpage</button>
      </div>
    </div>
  );
};

export default Header;

useNavigatenavigate변수에 할당해 주고 이전페이지로 이동하는 함수와 서브페이지로 이동하는 함수를 만들어서 버튼에 클릭이벤트로 넣어주었다.

<NavLink> 컴포넌트

링크에서 사용하는 경로가 현재 라우트의 경로와 일치하는 경우 특정 스타일 또는
css클래스를 적용하는 컴포넌트
<NavLink> 컴포넌트를 사용할 때에는 style 또는 className을 설정할 때

 {isActive:boolen}을 파라미터로 전달받는 함수 타입의 값을 전달함.

ex>

const activeStyle={
	backgroundColor:'pink',
	fontSize: 24
}

<NavLink style = {({isActive})=> isActive? activeStyle : undefined }/>
<NavLink className = {({isActive})=> isActive? "active" : undefined }/>

Subpages.js

import React from "react";
import { NavLink, Outlet } from "react-router-dom";

const Subpages = () => {
  const activeStyle = {
    backgroundColor: "pink",
    fontSize: 24
  };
  return (
    <div>
      <li>
        <NavLink
          to="/subpages/1"
          style={({ isActive }) => (isActive ? activeStyle : undefined)}
        >
          서브페이지1
        </NavLink>
      </li>
      <li>
        <NavLink
          to="/subpages/2"
          style={({ isActive }) => (isActive ? activeStyle : undefined)}
        >
          서브페이지2
        </NavLink>
      </li>
      <li>
        <NavLink
          to="/subpages/3"
          style={({ isActive }) => (isActive ? activeStyle : undefined)}
        >
          서브페이지3
        </NavLink>
      </li>
      <Outlet />
    </div>
  );
};

export default Subpages;

 서브페이지를 링크를 클릭하면 클릭한 링크의  isActive가 true가 되면서 스타일 이 적용된다.

 

NotFound 페이지 만들기

미리 정의되지 않는 경로에 사용자가 진입했을 때. 

보이는 페이지 페이지를 찾을 수 없을 때 나타나는 페이지

 

NotFound.js

import React from 'react';

const NotFound = () => {
    return (
        <div>
            404<br/>
            페이지를 찾을 수 없습니다      
        </div>
    );
};

export default NotFound;
<Route path="*" element={<Notfound/>}/>

 

 

예제)

 

 

리액트라우터 라이브러리는 페이지를 이동해줘야 하는 웹의 특성상 필수로 알아둬야 하고 또 잘 쓸 수 있어야 할 것 같다. 확실히 링크를 타고 다른 페이지로 이동되는 웹사이트보다 같은 페이지에서 계속 렌더링 시켜주는 방식이 화면전환이 더 깔끔하고 부드러워 보인다. 

반응형
반응형

react-icons를 이용하면 css 없이 아이콘을 이용할 수 있다.

 

사용방법

Installation (for standard modern project)

npm install react-icons --save

 

 

React Icons

React Icons Include popular icons in your React projects easily with react-icons, which utilizes ES6 imports that allows you to include only the icons that your project is using. Installation (for standard modern project) npm install react-icons --save Usa

react-icons.github.io

react-icons를 프로젝트에 설치했으면 이제 사이트에 들어가서 필요한 아이콘을 찾고 

해당페이지의 부분을 import 해주고 아이콘을 넣어서 사용하면 된다.

 

사이트에 들어가서 ionicons5에 들어가면 IoLogoReact 아이콘이 있다

위에 import 부분을 복사해서 붙여 넣어주고 iconName에 사용할 아이콘이름을 넣어주고 

아이콘을 컴포넌트형식으로  넣어주면 된다. (아이콘을 클릭하면 아이콘이름이 복사됨)

 

App.js

import "./styles.css";
import { IoLogoReact } from "react-icons/io5";
export default function App() {
  return (
    <div className="App">
      <IoLogoReact></IoLogoReact>
    </div>
  );
}

 

 

이번내용은 별로 어려운 것도 없고 설명할 것도 없어서 짧게 끝내겠다 react icon만 설치해 주고 필요한 아이콘을 사이트에서 찾아서 import 해주고 넣기만 하면 끝이다. 

 

반응형
반응형

axios(필수)

 

브라우저와 node.js에서 사용할 수 있는 Promise 기반 HTTP 클라이언트 라이브러리

API: 요청 메소드
GET: 데이터조회
POST: 데이터등록
PUT: 데이터수정
DELETE: 데이터 제거

 

axios 사용법

npm 사용하기:

$ npm install axios

import axios from 'axios';
axios.get('/users/1')
axios.post('경로',{username:"green",id:"aa"})

useState,useEffect로 데이터 로딩하기
상태관리
1.요청의결과 - null
2.로딩상태 - false
3.에러 - null

로딩상태가 true 면 로딩중 하면을 보여줌
에러가 null이 아니면 에러가 발생했습니다 화면을 나타냄
결과값이 없으면 null을 보여줌

로딩이 false 이고 에러가 null이고 결과값이 null이 아닐때 받아온 데이터를 화면에 나타냄 

 

App.js

import "./styles.css";
import { useEffect, useState } from "react";
import axios from "axios";

export default function App() {
  const [data, setdata] = useState(null); //data상태값
  const [loading, setloading] = useState(false); //loading상태값
  const [error, seterror] = useState(null); //
  useEffect(() => {
    const fetchUsers = async () => {
      try {
        //요청이 시작되면 error 와 users를 초기화
        seterror(null); //에러 아직 없음
        setdata(null); //데이터 아직 없음
        //loading상태는 true로 변경
        setloading(true); //로딩중
        const response = await axios.get(
          "https://jsonplaceholder.typicode.com/users"
        );
        setdata(response.data);
      } catch (e) {
        seterror(e);
        console.error(e);
      }
      setloading(false);
    };
    fetchUsers();
  }, []);
  if (loading) return <div>로딩중...</div>;
  if (error) return <div>에러가 발생했습니다.</div>;
  if (!data) return null;

  return (
    <div className="App">
      {data.map((data) => (
        <li key={data.id}>
          {data.username} {data.name}
        </li>
      ))}
    </div>
  );
}

loading이 true이면 로딩중

error에 값이 담겨있으면 에러가 발생했습니다.

data에 데이터가없으면 null을 리턴해준다.

useEffect(() => {
    const fetchUsers = async () => {
      try {
        //요청이 시작되면 error 와 users를 초기화
        seterror(null); //에러 아직 없음
        setdata(null); //데이터 아직 없음
        //loading상태는 true로 변경
        setloading(true); //로딩중
        const response = await axios.get(
          "https://jsonplaceholder.typicode.com/users"
        );
        setdata(response.data);
      } catch (e) {
        seterror(e);
        console.error(e);
      }
      setloading(false);
    };
    fetchUsers();
  }, []);

useEffect로 처음에 마운트될때만 자료를 받아오게해준다. async await으로 비동기문을 만들어주고 try catch로 혹시나 로딩이 정상적으로 이루어지지않았을때 에러처리를 해준다. 

response에 await axios.get(주소를받아올서버주소) // 이렇게 입력해준다 서버주소는 Jsonplaceholder에서 받아왔다.

 

JSONPlaceholder - Free Fake REST API

{JSON} Placeholder Free fake API for testing and prototyping. Powered by JSON Server + LowDB. Tested with XV. As of Oct 2022, serving ~1.7 billion requests each month.

jsonplaceholder.typicode.com

 

이제 이 코드를 좀 더 간결하게 만들어 보겠다. 

useReducer를 사용해서 상태를 관리해주는 코드로 변경

import "./styles.css";
import { useEffect, useReducer } from "react";
import axios from "axios";

const initialstate = {
  data: null,
  error: null,
  loading: false
};
//reducer 함수
function reducer(state, action) {
  switch (action.type) {
    case "LOADING":
      return {
        data: null,
        error: null,
        loading: true
      };
    case "SUCCESS":
      return {
        data: action.data,
        error: null,
        loading: false
      };
    case "ERROR":
      return {
        data: null,
        error: action.error,
        loading: false
      };
    default:
      return state;
  }
}

export default function App() {
  // const [data, setdata] = useState(null);
  // const [loading, setloading] = useState(false);
  // const [error, seterror] = useState(null);
  const [state, dispatch] = useReducer(reducer, initialstate);
  const { data, error, loading } = state;
  useEffect(() => {
    const fetchUsers = async () => {
      try {
        dispatch({
          type: "LOADING"
        });
        const response = await axios.get(
          "https://jsonplaceholder.typicode.com/users"
        );
        dispatch({
          type: "SUCCESS",
          data: response.data
        });
      } catch (e) {
        dispatch({
          type: "ERROR",
          error: e
        });
        console.error(e);
      }
    };
    fetchUsers();
  }, []);
  if (loading) return <div>로딩중...</div>;
  if (error) return <div>에러가 발생했습니다.</div>;
  if (!data) return null;

  return (
    <div className="App">
      {data.map((data) => (
        <li key={data.id}>
          {data.username} {data.name}
        </li>
      ))}
    </div>
  );
}

useState로 줬던 상태값들을 useReducer로 하나로묶어주었고 상태변경도 dispatch를 통해서 하도록 바꿨다.

 

이제여기서 주소에서 값을 받아오는 async await 문을 따로 커스텀 react훅으로 빼서 다른 컴포넌트에서도 주소만 바꿔서 이용할수 있게 만들어주겠다.

useAsync.js

import { useReducer, useEffect } from "react";

//초기값
const status = {
  data: null,
  loading: false,
  error: null
};
//reducer 함수
function reducer(state, action) {
  switch (action.type) {
    case "LOADING":
      return {
        data: null,
        error: null,
        loading: true
      };
    case "SUCCESS":
      return {
        data: action.data,
        error: null,
        loading: false
      };
    case "ERROR":
      return {
        data: null,
        error: action.error,
        loading: false
      };
    default:
      return state;
  }
}
const useAsync = (callback, deps = []) => {
  // callback함수와 deps배열을 받아옴 //
  const [state, dispatch] = useReducer(reducer, status);
  // async aswait
  const fetchUsers = async () => {
    try {
      dispatch({
        type: "LOADING"
      });
      const response = await callback(); //callback 함수는 axios.get(주소) 를 리턴
      dispatch({
        type: "SUCCESS",
        data: response.data
      });
    } catch (e) {
      dispatch({
        type: "ERROR",
        error: e
      });
    }
  };
  useEffect(() => {
    fetchUsers();
  }, deps);
  return [state, fetchUsers]; //상태값과 fetchUsers를 돌려줌
};

export default useAsync;

callback함수에 담기는값

async function getUser() {
  const response = await axios.get(
    "https://jsonplaceholder.typicode.com/users"
  );
  return response;
}

response 를 리턴해주는 비동기 함수를 담는다. useAsync에서 실행시키고 결과값을 response에 저장한다음 data에 담아준다.

 

useAsync가 다 돌아가면 state와 fetchUsers함수를 리턴해준다.

 

App.js

import "./styles.css";
import axios from "axios";
import useAsync from "./Hooks/useAsync";

async function getUser() {
  const response = await axios.get(
    "https://jsonplaceholder.typicode.com/users"
  );
  return response;
}

export default function App() {
  const [state, refetch] = useAsync(getUser, []);
  const { data, error, loading } = state;
  if (loading) return <div>로딩중...</div>;
  if (error) return <div>에러가 발생했습니다.</div>;
  if (!data) return null;

  return (
    <div>
      <ul>
        {data.map((user) => (
          <li ket={user.id}>
            {user.username} {user.name}
          </li>
        ))}
      </ul>
      <button onClick={refetch}>재요청</button>
    </div>
  );
}

 

이렇게 서버에서 값을 받아오는 axios에 대해서 배웠다. 또 함수를 묶어서 커스텀hook을 만들어서 사용할수도 있다는걸 알게되었다 물론 아직 스스로 이런걸 짤수있는 능력은 없지만 읽어서 어떻게 돌아가는지는 아니까 잘 찾아서 따라할수있지않을까? axios는  promise를 기반으로 만들어졌기때문에 async await 말고도 .then으로도 사용할수 있다. 일단 이런건 쓸려면 자바스크립트가 기초가되야되는데 이번에 비동기함수를 보면서 자바스크립트에서 부족한점을 알게된것같다.

반응형
반응형

useReducer )  새로운 상태관리 Hooks

이때까지는 useState로 상태를 만들고 상태 값을 업데이트해 주는 함수를 이용해서 상태를 관리했었다.

useState는 설정하고 싶은 상태를 직접 설정하고 지정해서 상태를 업데이트해 준다. 

 

useReducer는 액션 객체를 기반으로 상태를 업데이트해 준다.

Dispatch --> Reducer --> state

Dispatchstate에 바로 접근할 수 없고 dispatch함수를 호출하고 dispatch에 연결된 reducer함수를 호출하고 action객체를 이용해서 state값에 접근할 수 있다.                  

                             전송

Dispatch(action)  --------> Reducer( state , action ) 

원래 있던 state에서 action이 호출한 값을 받아서 업데이트해 준다.

 

const [ number , dispatch ] = useReducer( 함수,  초기값 ) --> return [ state, dispatch( fn )]

number에 초기값이 담기고 ,

dispatch에 상태를 변경할 수 있는 함수(Reducer)에 접근하는 함수(dispatch)가 담기게 된다.

 

저번에 만든 TodoList에서는 useState로 상태값을 만들어서썻는데 이번에는 useReducer로 만들어보겠다.

 

App.js (useState사용)

mport './App.css';
import Todoheader from './Todoheader';
import Todolists from './Todolist';
import { useState } from 'react';

function App() {
  const [todoState,setTodostate] = useState({
    todoLists:[],
    inputText:""
  })
  const Change = (e)=>{
    setTodostate({
      ...todoState, //원래있던 todostate값을 저장
      inputText: e.target.value
    });
  };
  const Addlist=()=>{
    const newtodolist = [
      ...todoState.todoLists,
      {id:todoState.todoLists.length,text:todoState.inputText}
    ]
    setTodostate({
        todoLists:newtodolist,
        inputText:""
    })
  }
  const delTodoLists = (id)=>{
    const newtodolist = todoState.todoLists.filter(list=>list.id !== id);
    setTodostate({
      ...todoState,
      todoLists:newtodolist
    })  
  }
  return (
    <div className="App">
      <Todoheader
      inputText={todoState.inputText} //value로 들어갈값
      Change ={Change}
      Addlist={Addlist}      
      />
      <Todolists
      todoLists = {todoState.todoLists} // li를 만들기위해 리스트값을 받아옴
      deltodolists = {delTodoLists}
      />
    </div>
  );
}

export default App;

Todoheader.js

import React from 'react'

const Todoheader = ({inputText,Change,Addlist})=>{

    return(
        <div className='header'>
            <h2>To do List</h2>
            <div>
                <input value={inputText} onChange={Change}/>
                <button onClick={Addlist}>+</button>
            </div>
        </div>
    )
}

export default Todoheader

 Todolists.js

import React from 'react'

const Todoheader = ({inputText,Change,Addlist})=>{

    return(
        <div className='header'>
            <h2>To do List</h2>
            <div>
                <input value={inputText} onChange={Change}/>
                <button onClick={Addlist}>+</button>
            </div>
        </div>
    )
}

export default Todoheader

여기까지는 전에 했던 todolist의 코드이다. 이제 여기서 useState로 만들어줬던 상태값을 useReducer로 만들고 상태변환 함수도 reducer라는 이름의 함수로 만들어줘야 한다. 

 

**상태변환함수의 이름은 reducer가 아니어도 상관없다 편의에 따라 붙일 수 있는 함수의 이름이다

 

App.js (useReducer사용)

import "./styles.css";
import Header from "./Header";
import Todolist from "./Todolist";
import { useReducer } from "react";
//초기값
const initialState = {
  todoLists: [],
  inputText: ""
};
//상태 변환함수 action객체를 받아서 조건에따라 상태를 변경
function reducer(state, action) {
  switch (action.type) {
    case "changeInput":
      return {
        ...state,
        inputText: action.payload
      };
    case "addTodo":
      return {
        ...state,
        todoLists: [...state.todoLists, action.todo],
        inputText: ""
      };
    case "deleteTodo":
      return {
        ...state,
        todoLists: state.todoLists.filter((list) => list.id !== action.id)
      };
    case "toggleTodo":
      return {
        ...state,
        todoLists: state.todoLists.map((todo) =>
          todo.id === action.id ? { ...todo, isDone: !todo.isDOne } : todo
        )
      };
    default:
      return state; //조건에 일치하는게 없을 경우 원래의 상태값을 반환 변화 X
  }
}
export default function App() {
  // 상태선언
  const [state, dispatch] = useReducer(reducer, initialState); //상태변화함수 , 초기값값
  //상태값구조분해 할당
  const { todoLists, inputText } = state;
  const Change = (e) => {
    dispatch({
      type: "changeInput",
      payload: e.target.value
    });
  };
  const Addlist = () => {
    dispatch({
      type: "addTodo",
      todo: { id: todoLists.length, text: inputText, isDone: false }
    });
  };
  const deltodolists = (id) => {
    dispatch({
      type: "deleteTodo",
      id: id
    });
  };
  const toggleTodo = (id) => {
    dispatch({
      type: "toggleTodo",
      id: id
    });
  };

  return (
    <div className="App">
      <Header inputText={inputText} Change={Change} Addlist={Addlist} />
      <Todolist
        todoLists={todoLists}
        deltodolists={deltodolists}
        toggleTodo={toggleTodo}
      />
    </div>
  );
}

여기서는 우선 App컴포넌트 위에 상태값으로 사용할 초기값을 변수로 만들어주고 상태변환함수로 사용할 reducer 도 만들어줬다 reducerdispatch에서 보내주는 action 객체를 받아서 상태값을 return 해준다. action 객체 의 type을 스위치문의 조건으로 사용해서 조건에 맞을 때 상태를 넣어준다.

 

컴포넌트에서 상태값을 만들 때 useState처럼 구조분해할당으로 상태값상태변환함수를 받아주고 

useReducer(reducer함수, 초기값 )을 넣어준다.

// 상태선언
const [state, dispatch] = useReducer(reducer, initialState); //상태변화함수 , 초기값값
//상태값구조분해 할당
const { todoLists, inputText } = state;

 

그리고 아래에서는 HeaderTodolist Props 로 보내줄 값과 함수를 만들어서 보내준다. 

함수는 dispatch함수를 이용해서 필요한 값이 들어간 객체를 리턴 시켜주면 reducer 에서 받아서 상태값을 변화시켜 준다.

 

useReducer를 사용하면 상태를 더 관리하기 쉽고 보기 편하다고 했는데 지금은 그런지 전혀 모르겠다. 나중에 ContextAPI와 같이 사용하면 조금 더 깔끔하게 사용이 가능하다.. 

 

 

 

다음은 ContextAPI를 이용해서 dispatch를 보내주고 각 컴포넌트에서 받아서 쓸 수 있게 해 주겠다

그러면 app에서 함수를 일일이 만들어서 props로 보내지 않아도 

각 컴포넌트에서 dispatchreducer함수를 쓸 수 있고 상태값을 바꿀 수 있게 된다.

 

컴포넌트 바깥 부분에 Context를 만들어주고.↓

export const UserDispatch = createContext();

 

Context태그로 감싸주고 값으로 dispatch함수를 넣어준다

이제 태그내부의 컴포넌트에서 useContext 를 써서 dispatch함수를 사용할 수 있다.↓

  return (
    <UserDispatch.Provider value={dispatch}>
      <div className="App">
        <Header inputText={inputText} id={id} />
        <Todolist todoLists={todoLists} />
      </div>
    </UserDispatch.Provider>
  );

 

Header.js

import React, { useContext } from "react";
import { UserDispatch } from "./App"; //Context가져오기

const Header = ({ inputText, id }) => {
  const dispatch = useContext(UserDispatch); //useContext를 써서 Context값 사용 dispatch함수가 담김
  return (
    <div className="header">
      <h2>To do List Reducer</h2>
      <div>
        <input
          value={inputText}
          onChange={(e) => {
            dispatch({
              type: "changeInput",
              payload: e.target.value
            });
          }}
        />
        <button
          onClick={() => {
            dispatch({
              type: "addTodo",
              todo: { id: id, text: inputText, isDone: false }
            });
          }}
        >
          +
        </button>
      </div>
    </div>
  );
};

export default Header;

이런 방법으로 원래는 함수를 App컴포넌트에서 만들어서 보내줬었지만 dispatch함수를 각 컴포넌트에서 사용할 수 있게 되면 컴포넌트에서 dispatch를 사용해서 상태를 변경시킬 수 있다.

 

App.js (ContextAPI로 dispatch 보내기)

import "./styles.css";
import Header from "./Header";
import Todolist from "./Todolist";
import { createContext, useReducer } from "react";
//초기값
const initialState = {
  todoLists: [],
  inputText: "",
  id: 1
};
//상태 변환함수 action객체를 받아서 조건에따라 상태를 변경
function reducer(state, action) {
  switch (action.type) {
    case "changeInput":
      return {
        ...state,
        inputText: action.payload
      };
    case "addTodo":
      return {
        ...state,
        todoLists: [...state.todoLists, action.todo],
        inputText: ""
      };
    case "deleteTodo":
      return {
        ...state,
        todoLists: state.todoLists.filter((list) => list.id !== action.id)
      };
    case "toggleTodo":
      return {
        ...state,
        todoLists: state.todoLists.map((todo) =>
          todo.id === action.id ? { ...todo, isDone: !todo.isDOne } : todo
        )
      };
    default:
      return state; //조건에 일치하는게 없을 경우 원래의 상태값을 반환 변화 X
  }
}
//context만들기
export const UserDispatch = createContext();
export default function App() {
  // 상태선언
  const [state, dispatch] = useReducer(reducer, initialState); //상태변화함수 , 초기값값
  //상태값구조분해 할당
  const { todoLists, inputText, id } = state;
  // props로 보내주던 함수들 이제는 context로 각 컴포넌트에서 dispatch를 사용해서 만들어서보내주지않아도된다
  // const Change = (e) => {
  //   dispatch({
  //     type: "changeInput",
  //     payload: e.target.value
  //   });
  // };
  // const Addlist = () => {
  //   dispatch({
  //     type: "addTodo",
  //     todo: { id: todoLists.length, text: inputText, isDone: false }
  //   });
  // };
  // const deltodolists = (id) => {
  //   dispatch({
  //     type: "deleteTodo",
  //     id: id
  //   });
  // };
  // const toggleTodo = (id) => {
  //   dispatch({
  //     type: "toggleTodo",
  //     id: id
  //   });
  // };

  return (
    <UserDispatch.Provider value={dispatch}>
      <div className="App">
        <Header inputText={inputText} id={id} />
        <Todolist todoLists={todoLists} />
      </div>
    </UserDispatch.Provider>
  );
}

 

Header.js

import React, { useContext } from "react";
import { UserDispatch } from "./App";

const Header = ({ inputText, id }) => {
  const dispatch = useContext(UserDispatch);
  return (
    <div className="header">
      <h2>To do List Reducer</h2>
      <div>
        <input
          value={inputText}
          onChange={(e) => {
            dispatch({
              type: "changeInput",
              payload: e.target.value
            });
          }}
        />
        <button
          onClick={() => {
            dispatch({
              type: "addTodo",
              todo: { id: id, text: inputText, isDone: false }
            });
          }}
        >
          +
        </button>
      </div>
    </div>
  );
};

export default Header;

Header컴포넌트에서 useContextdispatch함수를 받아와서 reducer함수에 접근해서

상태값을 바꿀 수 있다.

 

Todolist.js 

import React, { useContext } from "react";
import { UserDispatch } from "./App";

const Todolist = ({ todoLists }) => {
  const dispatch = useContext(UserDispatch);
  return (
    <div>
      <ul>
        {todoLists.map((list) => (
          <li key={list.id} style={{ color: list.isDone ? "#eee" : "#333" }}>
            <span
              onClick={() => {
                dispatch({
                  type: "toggleTodo",
                  id: list.id
                });
              }}
            >
              {list.text}
            </span>
            <button
              onClick={() =>
                dispatch({
                  type: "deleteTodo",
                  id: list.id
                })
              }
            >
              삭제
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
};
export default Todolist;

 

아직은 좀 익숙치 않기는 한데 몇 번 더쓰고나면 useState만큼 편하게 쓸수있지않을까., 많은 상태값을 한번에 관리 하기는 좋은것같다.

반응형
반응형

useCallback 은 useMemo와 비슷한 React-Hook이다. 값을 메모리에 저장해 두고 일반적으로 리렌더링 될 때에는 초기화되지 않고 값이 변경되는 것을 감지해서 값이 변경될 때만 함수를 실행시켜 주고 값을 업데이트해 준다.

 

차이점

useMemo(콜백함수, [ ]) 을 memoization (메모리에 저장)
useCallback(콜백함수, [ ]) 함수를 memoziation (메모리에 저장)

 

useCallback을 쓰는 이유 

 

자바스크립트에서 함수는 객체의 한 종류로 취급된다. 그래서 리렌더링 되면 함수가 초기화되면서 새로운 주소값을 할당받기 때문에 이전과 같은 모양의 함수라도 다른 함수로 취급된다. 그래서 함수를 저장하고 값이 변경되었을 때만 함수를 바꿔주는 hooks이 필요하다.

 

예제) 크기가 바뀌는 박스, 다크모드 

App.js

import { useState } from "react";
import Box from "./Box";
import "./styles.css";

export default function App() {
  const [size, setSize] = useState(100);
  const createBoxStyle = () => {
    return {
      width: `${size}px`,
      height: `${size}px`,
      backgroundColor: "turquoise"
    };
  };
  return (
    <div>
      <input
        type="number"
        value={size}
        onChange={(e) => {
          setSize(e.target.value);
        }}
      />
      <Box createBoxStyle={createBoxStyle} />
    </div>
  );
}

Box.js

import React, { useEffect, useState } from "react";

const Box = ({ createBoxStyle }) => {
  const [style, setStyle] = useState({});

  useEffect(() => {
    console.log("👍👍박스 키우기");
    setStyle(createBoxStyle()); //스타일에 함수가반환하는 스타일값을 넣어줌
  }, [createBoxStyle]); //createboxstyle함수가 변경될때만 실행

  return <div style={style}></div>;
};

export default Box;

이렇게 인풋태그의 숫자가 변경되면 박스의 크기가 변경되고 useEffect로 createBox함수가 바뀔 때마다 '박스 키우기'가 콘솔에 뜨게 해 줬다. 지금은 이 코드가 잘 작동하지만 상태값을 하나 더 추가하면 문제가 생긴다. 

 

App.js (dark상태값추가 버튼태그추가)

import { useState } from "react";
import Box from "./Box";
import "./styles.css";

export default function App() {
  const [size, setSize] = useState(100);
  const [dark, setDark] = useState(false);
  const createBoxStyle = () => {
    return {
      width: `${size}px`,
      height: `${size}px`,
      backgroundColor: "turquoise"
    };
  };
  const style = {
    backgroundColor: dark ? "#333" : "#fff",
    color: dark ? "#fff" : "#333"
  };
  return (
    <div style={style}>
      <div>
        <input
          type="number"
          value={size}
          onChange={(e) => {
            setSize(e.target.value);
          }}
        />
        <Box createBoxStyle={createBoxStyle} />
        <button
          onClick={() => {
            setDark(!dark);
          }}
        >
          모드변경
        </button>
      </div>
    </div>
  );
}

이렇게 dark라는 상태값을 하나 추가해 주고 버튼을 누르면 상태값이 변경된다. 모드변경모드를 누를 때마다 상관없는 박스 키우기가 콘솔에 뜨게 되는데 이유는 dark상태값이 변경되면서 컴포넌트가 리렌더링 되고 createBox함수도 리렌더링 되면서 같은 모양의 함수이지만 다른 주소를 가지게 되면서 Box컴포넌트에서 useEffect가 다른 함수라고 인식하게 되어 실행되어서 그렇다. 그래서 이를 막기 위해서는 함숫값이 변경되지 않게 해줘야 한다. 사이즈가 변경될때만 함수를 다시호출하고 변경되지않을 때는 저장되어있는 함수를 그대로 이용해서 같은주소의 함수가 보내지게 해줘야한다.

 

useCallback을 이용해서 함수를 저장해 준다.

 

App.js

import { useCallback, useState } from "react";
import Box from "./Box";
import "./styles.css";

export default function App() {
  const [size, setSize] = useState(100);
  const [dark, setDark] = useState(false);
  const createBoxStyle = useCallback(() => { //박스스타일을 만들어주는 함수를 저장 
    return {								//size상태가 변경되면 실행
      width: `${size}px`,
      height: `${size}px`,
      backgroundColor: "turquoise"
    };
  }, [size]);

  const style = {
    backgroundColor: dark ? "#333" : "#fff",
    color: dark ? "#fff" : "#333"
  };
  return (
    <div style={style}>
      <div>
        <input
          type="number"
          value={size}
          onChange={(e) => {
            setSize(e.target.value);
          }}
        />
        <Box createBoxStyle={createBoxStyle} />
        <button
          onClick={() => {
            setDark(!dark);
          }}
        >
          모드변경
        </button>
      </div>
    </div>
  );
}

이렇게 써주면 useCallback이 boxstyle을 주던 함수를 저장하게 되고 size가 변경될 때만 함수가 다시 만들어진다.

사이즈가 변경되지 않았을 때는 원래 있던 함수를 box컴포넌트에 보내주고 주소가 이전과 같기 때문에 useEffect에서도 다시 실행되지 않는다.

 

 

저번 useMemo와 같이 useCallback도 값을 저장해 뒀다가 쓰는 방식이기 때문에 많이 쓰면 자원을 낭비하게 된다. 꼭 필요할 때 적절히 쓸 수 있도록 해야 한다. 아직은 언제 어떤 방식으로 써야 적절하게 쓰는 건지 감이 잘 오지는 않는다. 나중에 포트폴리오를 하면서 쓰다가 보면 감이 오겠지.

반응형

+ Recent posts