쇼페이지 생성
오늘은 몽고DB의 스키마를 조금 다듬은 후에 View페이지를 구현 할 예정이다. 물론 아직 지도 API를 받아오는 것을 물론이고 회원가입 구현조차 되지 않았으므로 우선 보여지는 것만 하고, 후에 지도를 띄우고, 댓글을 띄우는 식으로 채워나가려 한다. 물론 validation역시 다뤄야할 중요한 이슈일 것이다.
우선 지금은 목록을 클릭하면 url을 보내도록 되어 있는데, 이때 목록에 담긴 _id주소로 get요청을 받아서 원하는 페이지를 띄우면 될 것이다...라고 짧게 생각 했는데, 이는 react를 사용하지 않는 경우 구현할 방식이고, react를 적용하기로 했고, 또 그렇게 했으므로 구상을 해야한다.
우선 서버측에서 react페이지로 데이터를 랜더링하여 보내고 그 정보를 react에서 쓰면 되지 않을까? 그런데 이렇게 되면 정보가 이미 클라이언트 측으로 다 전달 된 상황인데, axios를 써야하는가, 하는 의문이 생긴다. 당장은 필요 없을 것 같은데, 구현하면서 더 생각해 봐야겠다.
(여태까지 한 것들)
코드
https://github.com/fkthfvk112/YammyPusanUniv
리액트 페이지 띄우기
구현하다보니 React앱은 서버사이드로 랜더링이 안 된다고 한다. 그래서 app.render가 아닌 app.sendFile을 사용한다.
a태그
<a class ='card_link'href ="/cafe/<%= cafe._id %>">
라우터
app .get ('/aa', (req , res )=>{
res .sendFile (path .join (__dirname , '/public/client-react/build/index.html'));
})
*라우터란 클라이언트의 요청 경로를 보고 이 요청이 처리 가능한 곳으로 기능을 보내주는 역할을 한다
마주한 에러
리액트 파일을 올리니 에러가 발생하였다.
Refused to apply style from 'http://localhost:8080/static/css/main.073c9b0a.css' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.
경로가 잘못된 것을 확인할 수 있다. /뒤에 pulbic...하는 경로 후 static이 와야하기 때문이다. 하지만 현재 내 파일의 위치는 build 내부 다른 폴더 속에 위치했다.
리액트 폴더의 asset_manifest.json을 건드려 경로를 수정해주었다.
{
"files": {
"main.css": "/client-react/build/static/css/main.073c9b0a.css",
"main.js": "/client-react/build/static/js/main.06558838.js",
"static/js/787.39be7a49.chunk.js": "/client-react/build/static/js/787.39be7a49.chunk.js",
"static/media/logo.svg": "/client-react/build/static/media/logo.6ce24c58023cc2f8fd88fe9d219db6c6.svg",
"index.html": "/client-react/build/index.html",
"main.073c9b0a.css.map": "/client-react/build/static/css/main.073c9b0a.css.map",
"main.06558838.js.map": "/client-react/build/static/js/main.06558838.js.map",
"787.39be7a49.chunk.js.map": "/client-react/build/static/js/787.39be7a49.chunk.js.map"
},
"entrypoints": [
"/client-react/buildstatic/css/main.073c9b0a.css",
"/client-react/buildstatic/js/main.06558838.js"
]
}
그런데도 실행이 안 되다가 찾아보니 index.html내의 주소 역시 변경해야 한다고 한다.
<!doctype html ><html lang ="en"><head ><meta charset ="utf-8"/><link rel ="icon"href ="/favicon.ico"/><meta name ="viewport"content ="width=device-width,initial-scale=1"/><meta name ="theme-color"content ="#000000"/><meta name ="description"content ="Web site created using create-react-app"/><link rel ="apple-touch-icon"href ="/logo192.png"/><link rel ="manifest"href ="/manifest.json"/><title >React App </title ><script defer ="defer"src ="/client-react/build/static/js/main.06558838.js"></script ><link href ="/client-react/build/static/css/main.073c9b0a.css"rel ="stylesheet"></head ><body ><noscript >You need to enable JavaScript to run this app.</noscript ><div id ="root"></div ></body ></html >
href부분의 경로를 수정하여주었다. 저렇게 css나 js파일이 있는 곳의 위치를 명시해주었다.
별 거 아닌 일에 너무 많은 시간을 쏟아버렸다. 어쨌든 이렇게 해서 이전에 만든 리액트 파일은 띄울 수 있게 됐다.
서버-클라이언트 통신
서버에서 클라이언트로 json 전달
이후 노드 서버에서 클라이언트측 index.html로 객체, json을 전달할 방법을 한참 찾았다.
이에 두 가지 방법이 있다고 하는데
1. React 컴포넌트를 서버에서 렌더링해서 HTML 문자열로 만들고, 이를 클라이언트에게 전달하는 방법
2. Node.js 서버와 React 컴포넌트 간의 API를 구성해서 데이터를 주고 받는 방법
이중 HTML문자열을 만드는 방법은 보기에도 안 좋아보이고 후에 리액트 파일 내에서 정보를 새로 업데이트 할 수 없기에(즉 사용자가 리액트 화면에서 이것저것 변화하는 것은 좋은 UI경험이 될 수 있겠지만, 그 와중에 새로운 정보를 받아 올 수 없다. 즉, 결국 서버측에 의존하게된다. 이는 내가 리액트를 도입한 계기에 어긋난다.) 두 번째 방법을 선택했다.
즉, id가 주소로 들어오면 리액트 페이지를 띄우고(이는 서버에서 라우터 함수가 처리), 리액트 페이지에서 현재 url의 id를 파라미터로 받아와서 해당 id를 이용해 서버측 api를 요청하는 axios를 사용하는 것이다(즉, 클라이언트 측에서 서버로 데이터를 요청하여 api를 받아옴). 결국 오늘의 개발 시작할 때 했던 고민 ‘axios를 써야하는가?’는 써야하는 것으로 판명났다.
* 이후useParams을 사용해야 한다는 것을 알았으나(id값을 얻기 위해) 도저히 작동이 안 되었다. 추가적인 공부 후에 Route 태그 내에서 사용 가능하다는 것을 알게 되었다. 괜히 시간을 잡아먹었다. 오히려 처음부터 공부했더라면 좋았을 텐데. 앞으로는 모르면 대충 검색해서 넘기지 말고 좀 더 심화학습을 하는 습관을 들이도록 하자. 이해를 하는 것이 오히려 빠르다.
오류와 해결
function PageThree (){
const [data , setData ] = useState ([]);
const { id } = useParams ();
useEffect (() => {
const fetchData = async () => {
try {
const response = await axios .get (`http://localhost:8080/cafe/api/${id }`);
console .log ('데이터', response )
setData (response .data );
} catch (error ) {
console .log (error );
}
};
fetchData ();
}, [id ]);
let result = <li >
<ol >{data }</ol >
<ol >{data }</ol >
</li >
return (
<h1 >
hi
</h1 >
)
}
이 코드가 작동하지 않아서 한참 헤맸다.
에러내용 : react-dom.development.js:14887 Uncaught Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.
프로미스가 자식으로 사용되었다는 오류이다.
axios를 사용하여 data에 객체를 프로미스를 이용해 비동기적으로 가져오는데, result에서 data가 쓰일 때 아직 비동기적으로 데이터를 받아오지 못한 상황인 것이다. 즉 나는 초기에 자동으로 할당된 것을 data로 쓰려고 한 것이었다. 아래와 같이 코드를 수정하였다.
const [data , setData ] = useState (null );
const { id } = useParams ();
useEffect (() => {
const fetchData = async () => {
try {
const response = await axios .get (`http://localhost:8080/cafe/api/${id }`);
setData (response .data );
} catch (error ) {
console .log (error );
}
};
fetchData ();
}, [id ]);
let result = <li >
<ol >{data }</ol >
<ol >{data }</ol >
</li >
let menus = data &&data .menu .map (menu => (
<ol key ={menu ._id }>{menu .name }</ol >
));
return (
<div >
{data &&(
<li >
<ol >{data .name }</ol >
{menus }
</li >
)}
</div >
)
}
삼항 연산자를 이용해 null(false)가 아닐 경우에면 data를 사용하도록 했으며 위에서는 .map을 이용해 역시 data가 널이 아닌 경우에 배열을 생성하였다. 이에 따라 리턴 값 역시 삼항 연산자를 사용해 null(false)가 아닌 경우에만 데이터를 사용하고, 그렇지 않은 경우 빈 div태그를 띄우도록 구현하였다.
후에 데이터가 정상적으로 들어오면 데이터를 사용하게 처리되는데, 이는 React특징에 기인한다. 리액트는 state의 값이 바뀌면 랜더링을 다시 하게 되는데, 이 때에 data의 값에 올바른 api로 받아온 값이 들어있으므로 삼항 연산자의 첫 조건을 통과하게된다.
코드를 수정하는 과정에서 for문이 아닌 map을 사용하였는데, map은 기존 배열을 변경하지 않고 인자로 넘겨진 배열을 순회하며 작업을 처리해준다.
map, 삼항 연산자 등, 학교에서 많이 쓰지 않았지만, if문을 쓰는 것보다 코드 양도 짧기에 앞으로 애용해야겠다.
게다가 코드 리팩토링의 과점에서 flag를 사용하는 것은 지양해야 하는데, 이 때 삼항 연산자가 유용할 것이다.
모델 수정 + 디자인
페이지가 구성됨에 따라 원하는 기능도 추가되었다. 처음부터 상세히 명세해놓고 개발을 시작했어야하는데, 주먹구구식으로 구현하니까 수정에 시간이 너무 많이 들어간다. 우선 모델을 수정해야한다.
1. cafe.js 모델 수정
const mongoose = require ('mongoose');
const Schema = mongoose .Schema ;
const cafeSchema = new Schema ({
name : {
type :String ,
required :true ,
unique :true
},
menu : [
{
name :String ,
price :Number ,
}
],
descripton :{
type :String ,
required :true
},
repreMenu : [String ],
purpose :{
type :String ,
enum :['study', 'talk', 'nofeatures']
},
location : {
type :String ,
required :true
},
comment : [{
user :{
type :Schema .Types .ObjectId ,
ref :'User'
},
content :String ,
replies :[{
user :{
type :Schema .Types .ObjectId ,
ref :'User'
},
content :String
}]
}]
});
module .exports = mongoose .model ('Cafe', cafeSchema );//모델에 접근
댓글을 카페 모델 내부로 넣었으며, 댓글을 쓴 유저를 식별 가능하도록 유저를 참조하도록 하였다.
모델 스키마를 수정했으니 create페이지를 살짝 수정해야한다. 그리고 해당 페이지에서 정보를 받아와 우선 글로 보여주는 방식을 해야겠다. api를 받아 오는 것은 처음 페이지가 로드 되면 될 것 같기에 코드의 위치를 수정해야한다.
이를 수정함에 따라 디자인도 약간 변경해야하는데, 시작부터 요구사항을 모두 정리하고 디자인까지 정리한 후 시작했어야하는데, 하면서 점점 판을 키우는 꼴이니 쓸데없이 작업 시간만 길어지고 있다.
또한 웹 디자인 툴 피그마를 공부해놓기로 했는데 아직 하지 않아서 ppt로 대충 디자인을 해 보았다. 전체적인 목록 뷰는 아래와 같이 기획 중이다.
(목록 뷰)
목록중 한 카페를 누르면 나오는 쇼 페이지는 아래와 같이 구현되면 좋겠다.
(쇼페이지)
이를 위해 우선 백엔드 작업부터 하는 게 마음이 편할 것 같다. 우선 사진 업로드 기능을 추가하고, 대표 메뉴 역시 사진을 올릴 수 있도록 처리하여야한다. 또한 아직 user스키마를 활용하지 못했기에 유저를 만들고, 리뷰 기능을 구현해야하고. 지도 역시 삽입해야한다. 갈 길이 멀다.
쇼페이지 구현(리액트)
리뷰작성을 누르면 모달 팝업으로 리뷰를 쓰고 사진을 첨부 가능하도록 구현하였다. 여기서 완료를 누르면 axios.post로 node서버로 데이터를 보내면 서버에서 리뷰를 작성해주면 될 것이다.
하지만 당연히 리뷰를 불러오는 것도 클라이언트 측에서 해야한다.
+ 몽고 스키마를 수정하니 검색 기능이 맛이 갔다. 추후 수정해야한다.
+개발 중 어이없는 실수에 시간을 너무 많이 잡아먹었다. axios.post를 하는데 계속해도 서버측으로 값이 전달되지 않았다. 내가 axios사용을 잘못 한 건가 해서 별 짓을 다 해봤는데, 나중에 보니 submit역할을 하는 버튼이 form 외부에 있었다. 난 개발 중 항상 무언가 안 될 때 어렵게 생각하는 경향이 있는 것 같다. 사소한 실수부터 확인하고 어려운 걸 생각하자!
async function submitForm (data ) {
await axios .post ('http://localhost:8080/cafe/user/review/create:id', data )
.then ((res ) => {
console .log (data )
console .log (res );
})
.catch ((err ) => {
console .log (err );
});
}
서브측으로 위같이 데이터를 보냈는데 계속 빈 객체만 받아왔다. (클라이언트 측에서 axios로 보낸 post를 받는 라우터이다.) 원인을 찾아보니 아직 body-parser를 내가 사용하지 않고 있었다.
이제 express 에 바디파서가 내장 되어있으니 아래 코드를 사용한다.
app .use (express .json ());
app .use (express .urlencoded ({extended :true }));
* body-parser는 Node.js에서 요청(request)의 본문(body)을 해석(parse)하여 req.body 속성에 넣어주는 미들웨어(Middleware)이다. JSON, urlencoded, text, raw 등 다양한 형태의 요청 본문을 쉽게 해석할 수 있다.
내가 한 것처럼 클라이언트에서 post요청을 보내면, 이것을 서버에서 사용하기 위해 body-parser로 데이터를 해석 한 후 사용해야한다. 그렇지 않으면 req.body 속성이 존재하지 않아 처리가 불가능하다.
나는 주소로 보낸 id값을 사용하여 몽고 db에 접속한 후 review에 push를 하고자 한다.
req.params.id를 사용하면 넘겨온 url의 :id에 접근할 수 있다.(동적 경로 매개변수 추출)
클라이언트 측에서는 아래 코드를 추가해 url의 마지막 id값을 추출해서 넘겼다.
const url = window .location .pathname ;
const id = url .substring (url .lastIndexOf ('/') + 1 );
받아온 정보는 서버에서 DB에 저장하였다.
app .post ('/cafe/user/review/create/:id', async (req , res ) => {
const id = req .params .id ;
const comment = req .body .comment ;
const cafe = await Cafe .findById (id );
console .log ('디비', cafe );
const tempComment = { //have to edit //add user id!!
content :comment
}
cafe .comment .push (tempComment );
await cafe .save ()
res .send ('Success!');
});
리액트 구현을 하다보니 부모에서 자식이 아닌 자식에서 부모로 데이터를 넘겨야 할 일이 생겼다. React에서 이런 작업은 부모에서 자식에게 probs으로 함수를 넘기고 자식에서 부모에게 넘겨받은 함수에 데이터를 넣는 식으로 처리한다고한다.
3번 컴포넌트에서 서버로부터 데이터를 받아오면 2번 컴포넌트가 재실행되게 하기 위해서 부모 컴포넌트를 리랜더링하기로 했다. 이를 위해 값을 받아오면 부모가 넘겨준 함수를 실행하고, 그것을 처리해 랜더링을 다시 하도록 처리하고자한다. 사실 리액트를 제대로 배워본 적이 없어서 맞는지 모르겠다... 잘 알지도 못하는 리액트를 조사하며 하고자 한 것에서부터 잘못됐을지도?
아무튼 며칠간 리액트를 해보니 랜더링 되는 시점이 상당히 골치 아프고 중요하게 느껴진다.
React에서 랜더링이 일어나는 경우는 아래와 같다고 한다.
props가 바뀔 때
state값이 바뀔 때
부모 컴포넌트가 리렌더링 될 때
따라서 나는 부모에서 useState를 이용해 랜더링을 일으킬 것이다. 만약 이게 잘 안되면 해당 값을 props으로 넘겨보는 것도 나쁘지 않을 듯. 우선 useState의 set함수를 자식으로 넘겨서 값을 바꾸어보자.
하고나니 이상한 상황이다. 부모가 랜더링 되는데 result의 요소들은 다시 랜더링이 되지 않는 것이다....... 그렇담 probs값을 바꾸어주면 되지 않을까?
결론적으로 아래의 순서로 해결하였다.
1. 부모에서 3번 요소로 useState의 set을 전달
2. 부모에서 2번 요소(3이 바뀌면 랜더링 되길 바라는 요소로 probs 값 전달)
3. 2번 요소에서 useEffect의 두 번째 인자로 넘겨받은 probs값 삽입. 이제 리랜더링된다.
부모요소
function PageTwo (){
const [lastComment , setData ] = useState (null );
console .log ('부모', lastComment );
return (
<div >
<PageTwoSectionOne />
<PageTwoSectionTwo parentData = {lastComment }/>
<PageTwoSectionThree setParentData = {setData }/>
</div >
)
}
3번 요소
const handleSubmit = (event ) => {
event .preventDefault ();
submitForm ({ comment :formState });
probs .setParentData ({...{comment :formState } });
handleCloseModal ();
};
2번 요소
function PageTwoSectionTwo (probs ){
const [data , setData ] = useState (null );
const url = window .location .pathname ;
const id = url .substring (url .lastIndexOf ('/') + 1 );
useEffect (() => {
const fetchData = async () => {
try {
const response = await axios .get (`http://localhost:8080/cafe/api/${id }`);
setData (response .data );
} catch (error ) {
console .log (error );
}
};
fetchData ();
}, probs .lastComment );
결론
결론적으로 이랬던 리액트 페이지가
이렇게 변하게 되었다.
그래도 오늘은 양 서버간 통신의 기틀을 어느정도 잡아놓았고 리액트도 어제보다 조금 더 익숙해졌기에 내일은 더 빠르게 개발할 수 있지 않을까. 확실히 리액트 이틀차라 그런지 코드를 작성하는 데에 실수가 너무 많고 심지어 모르는 부분이 너무 많다. 다음에는 꼭 로그인 기능을 구현해야겠다. 유저의 _id를 쓸 일이 너무 많다.
우선 local로 구현해놓고 추후 네이버 구글까지 한번 해봐야겠다.
댓글