이제 3월 23일 수료가 2달 앞으로 다가오면서 슬슬 포트폴리오 압박이 들어오기 시작했다..
무조건 리액트로 만들어야하는데 아직 리액트로 엉성한 사이트도 제대로 만들어보지 못해서 걱정이 크다..
검색하다 보면 어떻게든 또 만들겠지 라는 생각으로 일단 함께할 팀원을 모아서 총 4명이서 여행코스를 만들어주는 사이트를 만들기로 했다. 각자 해외에 살다 온 경험이 있는 분들이라서 각 지역별로 추천관광지를 리스트업 하고 사용자가 추천관광지를 클릭하면 지도에 마크가 찍히고 서로 선으로 이어져서 여행동선을 한눈에 보기 편하게 만들어주는 사이트이다.
이걸 위해서 지도이미지와 마크 그리고 경로를 그려주는 기능이 필요했는데 마침 구글 maps API에서 모든 기능을 다 지원한다고 해서 일단 구글맵스를 공부하면서 어떻게 적용할지 고민하고 있다.
리액트로 처음 페이지를만들면서 가장 고민되는 점은 어떤 변수들을 상태로 관리해야 하는지이다. 아직 해결은 못했지만 돌아가는 구성을 하나씩 맞춰보면서 사용자와 상호작용하면서 계속해서 변경되는 값들을 상태값으로 관리하려고 생각 중이다.
모듈이 여러 개일 때는 리듀서가 여러 개이기 때문에 리듀서들을 하나로 합쳐서 store를 만들 때 사용한다.
modules/index.js
import { combineReducers } from "redux";
import counter from "./counter";
const rootReducer = combineReducers({ counter })
export default rootReducer
3. 스토어 만들기 , 전역에 스토어 사용하기
만들어진 rootreducer를 이용해서 스토어를 만들고 스토어를 사용할 수 있게 하위컴포넌트들을 묶어준다.
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { legacy_createStore as createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';
//스토어 만들기.
const store = createStore(rootReducer)
console.log(store.getState()) //상태값 조회
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
useSelector로 counter의 상태값을 number로 받아서 Counter컴포넌트에 props로 보내준다.
useDispatch로 dispatch함수를 받아와서 디스패치를 실행하는 함수를 만들었다 onIncrease ,onDecrease
dispatch에 액션객체로는 액션생성 함수를 넣어줬다.
이렇게 만들어준 함수를 Counter컴포넌트에 props로 넘겨준다.
이제 이 컨테이너 컴포넌트를 app.js에 넣어서 렌더링만 해주면 된다.
이제 여기에 미들웨어를 하나 만들어서 값이 리듀서로 가기 전에 작업을 할 수 있게 해 주겠다.
*리덕스 미들웨어 미들웨어는 함수를 연달아서 두 번 리턴하는 함수.
const middleware = store => next => action =>{
//하고싶은작업..
}
function middleware(store){
return function(next){
return function(action){
//하고싶은작업..
}
}
}
위와 같은 구조가 미들웨어의 구조이다 store는 파라미터로 들어가고 함수를 두번 리턴해준다.
각 함수에서 받아오는 파라미터 -store 리덕스 스토어 store dispatch( ) / getState( ) / subscribe( ) 내장함수 들어있음 -next 액션을 다음 미들웨어에 전달하는 함수 ex) next(action) -action 은 현재 처리하고 있는 액션객체
-next(action)을 해주지 않으면 action이 reducer나 다음 미들웨어로 넘어가지 않아서 에러가 생긴다.
middlewares/myLogger.js
const myLogger = store => next =>action=>{
//액션 출력하기
console.log(action)
//next는 다음미들웨어 또는 리듀서 에게 액션을 전달.
const result = next(action); //다음 미들웨어가 없어서 리듀서가호출되고 상태값이 변함
// 업데이트 이후의 상태를 출력
console.log(store.getState());
//여기에서 반환하는 result값은 dispatch(action)의 결과물.
return result
}
export default myLogger;
지금 보내주는 액션을 콘솔에 출력해 주고 action을 다음미들웨어로 넘겨준다.
미들웨어로 action이 넘어간 다음 바뀐 상태값을 출력해 준다.
이제 만들어진 이 미들웨어를 사용해 보자
store를 만들 때 미들웨어를 같이 넣어주면 된다.
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { applyMiddleware , legacy_createStore as createStore } from "redux";
import { Provider } from "react-redux";
import App from "./App";
import rootReducer from "./modules";
import myLogger from './middlewares/myLogger';
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
const store = createStore(rootReducer,applyMiddleware(myLogger));
console.log(store.getState());
root.render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>
);
applyMiddleware를 import 해서 createstore안에 넣어주고 사용할 미들웨어를 파라미터로 넣어준다.
콘솔을 확인해 보면 어떤 액션이 들어가고 상태값이 어떻게 변하는지 미들웨어에서 출력을 해준다
이렇게 리듀서로 들어가는 자료들을 중간에서 받아서 출력해 줄 수도 있고 변경해서 다시 보내줄 수도 있는 게 미들웨어의 역할이다.
만들어진 미들웨어 써보기
redux-logger
npm install --save react-redux
redux-logger는 만들어져 있는 미들웨어이며 이전상태값과 액션 다음상태값을 출력해준다.
원래 저기에 이전상태값이랑 action객체 다음 상태값이 다 떠야되는데 왜 안뜨는건지...
원래는 이렇게 상태값과 액션객체가 뜬다.
피드백
사실 미들웨어는 저번주 금요일에 배웠던 개념인데 이해가 너무안되서 정리를 좀더 해보고 오늘 학원에서도 다시해보고 thunk를 배우면서 어느정도 개념에 대해서 받아들여진것같다. 역시 코딩은 안되면 일단 반복하면 되는구나..
다음 포스팅은 thunk에 대해서 할것같은데 이것도 아직 제대로 알아먹은건아니라서 두루뭉술하게 될지도. 일단 인강이나 이런거 찾아보고 내 뇌속의 개념을 조금더 보충해서 알아보기쉬운 포스팅을 쓸려고 하고있다. 그냥 코드만 따라하는건 아무나 하는거고 코드를 보고 이해할수있는게 중요한것같다. 이제 포트폴리오 준비도 해야되고 정신도 없지만 . 배우는 개념들은 모두 놓치지않고 이해하고싶다.
액션 타입 - 다른 리덕스 모듈과 겹치지 않게 하기 위해 컴포넌트 이름의 접두사를 앞에 붙여줘야 한다
counter/SET_DIFF
액션 생성함수 -액션객체를 리턴해주는 함수. (export로 내보내기)
리듀서 - 컴포넌트 이름으로 만들어지며 state 파라미터에 디폴트 초기값을 지정합니다.
(export default로 내보내기)
state=initialState
모듈들을 관리해 줄 modules폴더를 만들고 그 안에 모듈들을 만들어준다.
modules/counter.js
//액션 : 타입 만들기
const SET_DIFF = "counter/SET_DIFF";
const INCREMENT = "counter/INCREMENT";
const DECREMENT = "counter/DECREMENT";
//액션 생성 함수 만들기
export const setDIFF = (diff)=>({type:SET_DIFF,diff});
export const increase = () =>({type:INCREMENT});
export const decrease = () =>({type:DECREMENT});
//초기 상태 선언
const initialState = {
number: 0 ,
diff: 1
}
//리듀서 선언
export default function counter(state=initialState , action){
switch(action.type){
case SET_DIFF:
return{
...state,
diff:action.diff
}
case INCREMENT:
return{
...state,
number:state.number + state.diff
}
case DECREMENT:
return{
...state,
number:state.number - state.diff
}
default:
return state;
}
}
modules/todos.js
//액션타입선언
const ADD_TODO = 'todos/ADD_TODO';
const TOGGLE_TODO = 'todos/TOGGLE_TODO';
//액션 생성 함수
let nextId = 1
export const addTodo = (text) =>({
type:ADD_TODO,
todo:{id:nextId,text: text,done:false}
})
export const toggleTodo = (id)=>({
type:TOGGLE_TODO,
id:id
})
//초기상태값
//초기 상태는 배열이어도 되고,원시타입(숫자,불린,문자열) 객체도 가능하다.
const initialState = [
// {
// id:1,
// text:"해야할일",
// done:false
// }
]
//리듀서
export default function todos(state=initialState,action){
switch(action.type){
case ADD_TODO:
return[
...state,
action.todo
]
case TOGGLE_TODO:
return state.map(todo => todo.id === action.id ? {...todo, done:!todo.done} : todo )
default:
return state
}
}
modules/index.js rootreducer 만들기
import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";
//한프로젝트에 리듀서가 여러개일때 하나로 합쳐서 사용 => rootreducer 루트리듀서
//combinereducers( )
const rootReducer = combineReducers(
{
counter:counter,
todos:todos
}
)
export default rootReducer
스토어 생성하기
src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { legacy_createStore as createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';
import { devToolsEnhancer } from '@redux-devtools/extension'; // 크롬확장 redux devtools
//스토어 만들기
const store = createStore(rootReducer,devToolsEnhancer());
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
store를 만들고 Provider로 묶어줘야 하위 컴포넌트에서 store요소를 불러서 사용할 수 있다.
이제 이 값들을 불러서 화면을 구성하고 상태값을 변경해 줄 컴포넌트를 만들어주면 된다.
컴포넌트 만들기
1. 프레젠테이셔널 컴포넌트
실제 보이는 태그들을 만들어주는 컴포넌트, redux스토어에 직접 접근하지 않음 필요한 값은 props로 받아와서 사용하는 컴포넌트, 주로 UI를 선언하는 것에 집중
2. 컨테이너 컴포넌트
프레젠테이셔널 컴포넌트를 감싸고 store에 접근해서 값을 props로 프레젠테이셔널로 전달. 리덕스 스토어의 상태를 조회하거나, 액션을 디스패치 할 수 있는 컴포넌트. 프리젠테이셔널 컴포넌트를 불러와서 사용
꼭 이렇게 만들어줄 필요는 없지만 기능적으로 분리해 주면 나중에 에러가 났을 때 찾기 편하다
화면을 구성해 주는 요소들이 들어있고 필요한 값들은 props로 컨테이너 컴포넌트한테서 받아온다.
2. 컨테이너 컴포넌트
containers/CounterContainer.js
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Counter from '../components/Counter';
import { decrease, increase,setDIFF } from '../modules/counter';
const CounterContainer = () => {
//useSelector() 는 리덕스스토어의 상태를 조회하는 hook이다
const { number,diff } = useSelector(state=>state.counter);
const dispatch = useDispatch();
// 각 액션들을 디스패치 하는 함수
const onIncrease = ()=> dispatch(increase());
const onDecrease = ()=>dispatch(decrease());
const onsetDIFF = (diff) => dispatch(setDIFF(diff));
return (
<Counter number={number} diff={diff} onIncrease={onIncrease} onDecrease={onDecrease} onsetDIFF={onsetDIFF}/>
);
};
export default CounterContainer;
컨테이너 컴포넌트에서 Counter컴포넌트를 불러서 사용하고 Props로
함수=dispatch(액션생성함수)와 상태값을 보내주고 있다.
App.js에서 CounterContainer 컴포넌트를 불러서 사용하면 끝
redux자체는 그렇게 되게 어렵지는 않은데 이번에 redux를 배우면서 새로운 개념들을 같이 배워서 이해하기가 정말 너무 힘들었다. 모듈을 다 나눠서 만들고 합쳐서 하나로 만들고 액션객체도 일일이 만들어줬었는데 이번에는 액션객체를 만드는 함수를 만들어서 사용하니 갑자기 연결된 애들이 많아져서 뭐가 어디서 불러서 쓰고 어디에 들어간게 맞는건지 구분을 못해서 수업시간에 좀 힘들었다.. 그래도 redux는 많이 사용하고 있다고하니 연습해서 꼭 잘 쓰고싶다.. 포기하지말자.. 수료 2달남았다.. ㅠㅠ
//diskstorage()--> 파일을 저장할때에 모든 제어 기능을 제공
const storage = multer.diskStorage({
destination:(req,file,cb)=>{
cb(null,'upload/');
},
filename:(req,file,cb)=>{
const newFilename = file.originalname;
cb(null,newFilename);
}
})
이제 upload경로로 Post요청을 보내면 upload객체가 실행되면서 파일을 받아서 서버에서 저장해 준다.
클라이언트에서 파일을 올리면 서버로 보내져서 upload폴더에 저장되고 서버에서 응답으로 이미지의 이름을 보내주게 되고 클라이언트는 받은 이름으로 클라이언트자신의 images폴더에 접근해서 해당하는 이미지를 띄워준다.
일단은 테스트기 때문에 css도 없고 사실 파일도 클라이언트의 img폴더가 아니라 클라이언트가 이미지도 보내줘야하는 게 아닌가 싶긴한데 내일 학원가면 이어서 마저 가르쳐주실테니 일단 여기서 만족한다 multer를 쓰는방법은 링크의 공식문서를 읽어보면 있으니 찾아서 하면될것같고 요즘 하면서 점점느끼는게 외우는 걸 포기하게 되는거다.. ㅋㅋㅋ 어떻게 다외우나 싶어서 선생님께 여쭤봤더니 외우면서 하는게 아니라 그때그때 잘 찾아서 써야 한다고 한다. 이제 외우려고 노력하지 말고 열심히 블로그에 기록하고 다른 블로그에 좋은 포스팅이 있으면 저장해 뒀다가 잘 베껴서 써야겠다 코딩은 정말 엄청 두꺼운 책으로 오픈북 시험을 치는 느낌이다.. ㅠ
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값을
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접속 생성
//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("서버가 돌아가고있습니다.")
})
아까 객체로 보내줬던 값을 이번에는 데이터베이스에서 조회해서 페이지로 전달해주고 있다.
배열에 데이터베이스에서 받아온 값들이 객체로 들어가서 전달된다.
여기까지 기본서버와 express서버를 만들었고 url파라미터를 받는 방법과 mysql을 연결해서 데이터베이스를 받아오는 걸 공부해 봤다. 다음포스팅은 데이터베이스에서 받아온 데이터를 서버에 올려주고 서버에 올린 값을 리액트에서 axios로 받아서 이용하는 페이지를 만들어 보겠다.
나는 프런트엔드 수업을 듣고 있어서 서버를 만드는 방법은 배웠지만 이 서버가 어떤 원리로 동작하고 어떤 코드가 정확하게 어떤 역할을 하는지는 간단한 설명만 듣고 넘어갔다. 아마 백엔드는 이것보다 배울게 많겠지.. 지금은 리액트를 하느라 바쁘지만 나중에 조금 시간이 생긴다면 서버에 대해서 조금 더 공부해보고 싶다.