[Node.js](개인 프로젝트)로그인과 페이지 연동
로그인 페이지 연동
오늘은 어제 구현한 로그인을 바탕으로 리액트 페이지를 보완할 것이다. 댓글 기능을 유저 id와 연동시키고, 댓글 삭제 및 평점 기능을 지원하도록 한다. 이를 위해 또 axios를 이용할 것이다. 즉, 페이지가 로드되면 현재 유저가 로그인 상태인지 아닌지, 로그인 상태라면 _id값이 뭔지 서버로 요청해서 페이지에서 가지고 있는다. 그리고 댓글은 로그인 상태에서만 쓸 수 있게 되며, 서버로 댓글을 DB에 저장하라는 명령을 또 전달해야한다. 그다음 다시 리액트 페이지에서 DB값을 읽어와서 해당 DB를 기준으로 페이지를 다시 렌더링 해야겠다.
원래 이렇게 쓸데 없이 비용이 많이 드는 방식으로 다들 구현하나, 하고 다른 사람들의 코드를 조금 살펴 봤는데, 별 다른 대안은 없을 듯 하다. 이 방식으로 오늘 구현하고자한다.
아, 위에 3번 사항에 대해서 고민을 많이 했다. 일반적으로는 기업이 자신의 회사를 등록하느 식으로 해야 할 텐데, 솔직히 사람들이 많이 쓰는 웹을 퍼블리싱 해 본적이 없어서 그런 것을 기대할 수가 없다. 결국 생각해낸 방식이 유저가 직접 카페를 생성하고, 메뉴 및 가격을 원하면 수정 가능하도록 하는 것이다. 이 방식을 사용하면 사실 프로그래밍 외적인 요소에 대한 고민이 필요하다. 대체 왜 유저가 시간을 써서 수정을 해 줄 것이며, 이 기능을 악용할 유저는 없을 것인가, 이다
우선 무언가 프로모션을 지원해야 함은 당연하다. 혜택이 주어져야 유저가 참여를 해 줄 것이다. 아직 이것에 대한 사업 모델을 떠올리지 못했다. 당장 생각 해 낸 것은 보통 카페를 많이 가고 리뷰를 남길 정도의 사람이면 개인 블로그를 가지고 있을 확률이 크다. 그래서 만약 리뷰를 많이 쓰고 카페를 많이 생성해준 사람은, 페이지 상단에 고마운 분든 명단에 그들의 이름을 써주는 것이다. 이 이름을 클릭하면 개인 페이지로 이동 가능한데, 이 개인 페이지에 블로그나 인스타, 유튜브 링크를 달 수 있게 해 주는 것이다.
이 방법을 쓰면 카페를 좋아하는 컨텐츠 크리에이터끼리 상호작용 하는 하나의 필드를 만들 수도 있다. 다만 그럼 개인 필드 + 게시판이라는 새로운 페이지를 생성하고 관리해야 한다. 사실 만드는 것 자체는 지금까지 해온 기술로 충분하리라 생각하는데, 유지 관리 비용(시간)이 만만치 않으리라는 생각이다. 아무튼 유지관리 비용이 많이 든다는 것은 누군가 이 사이트를 이용해줄 때의 이야기이고, 나는 평소 그렇게 희망적인 생각을 하는 사람이 아니기 때문에 우선 이렇게 구현해 보고자 한다.
혼자 개발을 하니 오늘 할 것을 정하고, 어떻게 구현할지 미리 청사진을 그리는 습관이 생겼다. 그도 그럴것이 막 하다가 시간 낭비 한 적이 너무나 많기 때문이다. 괜히 소프트웨어 공학에서 사전 계획(requirement:요구사항)의 개념을 강조 한 것이 아니구나 싶다.
오늘은 아무튼 저 위에 나열된 것정도만 구현하자. 다음 개발 쯤에는 사진 역시 넣을 수 있을 것이다. 아무튼 잡담은 이 정도로 하고 오늘 개발을 시작한다....시작하려고 코드를 보니 답답하다. 조만간 리팩토링 한 번 해야겠다.
에러로 시작
*기억하기
클라이언트에서 axios 사용시:
axios.post 클라이언트에서 서버로 데이터 전송
axios.get 서버에서 클라이언트로 데이터 전송
.
app.get('/user/api/islogIn', async(req, res)=>{
console.log('값', req.isAuthenticated());
이 코드가 아무리 해도 false로 나오는 오류가 발생하였다. 다른 주소로는 true를 반환하는데 axios를 요청하기만 하면 false를 반환하였다. cors는 했었는데, 보니 클라이언트 측에서도 허용을 해야한다고 한다.
axios .defaults .withCredentials = true ;
이 한 줄을 상단에 App.js 상단에 추가하니 정상 작동하였다. 크로스 도메인을 허용한다고 보면 되겠다.
CORS란?(Cross-Origin Resource Sharing)
한 출처에서 실행하는 어플리케이션이 다른 출처의 리소스에 접근 할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다. 즉, 원래는 보안상의 이유로 서로 다른 포트를 가지면 접근이 불가능하다.
어쩃든 이문제는 해결했는데 이제 또 populate가 문제다. 구조가 조금 복잡해지니 약간 어려워졌다. 하지만 이건 공부차원해서 굉장히 유용한 문제라고 생각하기 잘 짚고 넘어가고자한다.
Populate() 메서드
populate() 메서드는 Mongoose에서 제공하는 메서드 중 하나로, 문서를 검색하는 쿼리에서 사용할 수 있다. 이 메서드는 문서에 포함된 ref로 정의된 필드가 다른 컬렉션의 문서 참조일 경우, 이를 검색하여 필드 값을 교체한다.
즉, populate() 메서드를 사용하면, 검색 결과에서 참조되는 다른 컬렉션의 문서를 가져와 해당 필드의 값을 채울 수 있게된다.. 이를 통해 다른 컬렉션의 문서를 참조하는 필드 값을 쉽게 다룰 수 있다.
const cafe = await Cafe .findById (req .params .id )
.populate ('menu')
.populate ('repreMenu')
.populate ({
path :'comment',
populate : {
path :'user',
model :'User',
select :'-password'// 유저 데이터 중 password 필드는 제외
}
});
우선 배열 모두 object_id를 가진다. 모두 다 populate시켜줘야한다. menu, repremenu는 배열이기에 populate를 해줬다.
여기서 이중 populate가 헷갈리는데, comment는 배열이며 그 안에 유저_id들이 들어있다. 따라서 배열 안에 _id를 채우기 위해 1번 populate한다. 그 와중에(populate{에서} 해당 _id를 다시 유저 값으로 채우기 위해 2번 populate한다.
이렇게 해서 유저 실제 닉네임을 리뷰에 띄우는 데 성공했다. 데이터베이스에 등록했음은 물론이다!
로그인과 연동
이제 삭제 처리를 하여야한다.
logIn상태이다.
댓글의 Id와 현재 로그인 된 id가 일치한다.
이 위의 경우만 삭제 버튼이 보이면 되겠고, 버튼을 누르면 axios로 post하여서 서버측에서 삭제하면 되겠다. 어차피 get으로 데이터를 받아오는 건 상위 페이지에서 이미 하고 있으니 삭제만 처리하면 되리라.
이 아래 부분은 후에 기억하려고 작성하는 하나의 팁인데, probs.user.user._id가 있거나 없을 수 있다. 제대로 받아왔다면 값이 있고, 아니라면 undefinde인데, 만약 undefinde를 대입할 시 오류가 발생한다. 이때 삼항 연산자를 사용하면 해결 가능하다. 하나의 테크닉이다.
const connectedUserId = probs .user .user ._id ? probs .user .user ._id :null ;
+) 자주 왔다갔다 하다보니 변수들의 형식이 헷갈린다. c로 코딩을 시작했던 사람이라 그런지 형식이 한번에 안 보이면 오히려 헷갈리는 것 같다. 얼른 타입스크립트도 배우던가 해야지.
+) !!를 두 개 붙이면 undefinde, null을 bool형식으로 바꿀 수 있다. !를 붙여서 반대의 불리언 타입으로 바꾼 후 다시 !를 붙여 원래로 돌린다. 혼란스럽지만 타입 명시가 안 되어있기에 종종 쓰게된다.
이런 식으로 로그인 한 것과 안 한 차이를 구현했다. 문제는 서버에서 로그아웃해도 랜더링이 다시 되지 않는다는 사실. 이건 사실 리액트를 조금 공부하면 해결 할 수 있을 거라 생각한다.
일단 useEffct의 두 번째 인자 값을 어ᄄᅠᇂ게 하면 되지 않을까.
useEffect에 대한 설명은 아래와 같다.
의존성 배열 (Dependency Array)useEffect를 사용할 때 가장 많이 사용하는 인자입니다. 이 배열에 포함된 값들이 변경될 때만 useEffect가 실행됩니다. 배열 내의 값이 변경되지 않으면 useEffect는 실행되지 않습니다. 이 배열에는 의존하는 상태나 props 등을 포함시킬 수 있습니다.
빈 배열 (Empty Array)useEffect에 빈 배열을 전달하면, 이 useEffect는 최초 한 번만 실행됩니다. 이후 실행되지 않습니다. 이 경우, componentDidMount와 동일한 역할을 합니다.
아무 값도 전달하지 않음 (No Value)useEffect에 아무 값도 전달하지 않으면, 컴포넌트가 렌더링될 때마다 매번 실행됩니다. 이 경우, componentDidUpdate와 동일한 역할을 합니다.
+ 부모가 재랜더링 될 때 자식으로 probs을 넘기 경우는 자식도 다시 랜더링 되지만 그렇지 않은 경우는 다시 랜더링 되지 않는다.
에러와 해결
unction 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 ();
}, [data ]); //data수정. 컴포넌트 재 랜더링 되도록
console .log ('리뷰데이터', data );
const connectedUserId = probs .user .user ? probs .user .user ._id : null ;
const reviews = [];
if (data != null ){
for (let comment of data .comment ){
reviews .push (
<Container className ="row mt-3 pt-3 pb-3 mb-3"style ={{border :`2px solid ${grayColor }`, minHeight :'5rem', width :'30rem' }}>{/*2번항목*/}
<div style = {{textAlign :'right'}}>
{probs .isLoggedIn &&(comment .user ._id ===connectedUserId )&&<button type ="button"class ="btn btn-outline-secondary btn-sm">삭제 </button >}
</div >
<div className ="col-3">
<div style = {{textAlign :'left'}}>{comment .user .nickName }</div >
<img
className ="d-block w-100"
src ="https://cdn.pixabay.com/photo/2023/01/12/05/32/duck-7713310_640.jpg"/>
</div >
<div className ="col-9 ml-3"style ={{borderLeft :`2px solid ${grayColor }`}}>
<div >{comment .content }</div >
</div >
</Container >
);
}}
return (
<div >
{reviews }
</div >
)
}
처음 작성한 코드이다. UseEffect로 넘긴 두 번째 값이 무한으로 받아와지므로 웹이 무한으로 재싱행된다. 이전 값과 이후 값이 같다면 재실행 되지 않으나 문제는 data가 비동기적으로 진행되기에 값을 예측할 수 없다는 것이다. 아무튼 결국 부모 요소로부터 받아온 probs을 사용하여 해결하였다.
useEffect (() => {
const fetchData = async () => {
try {
const response = await axios .get (`http://localhost:8080/cafe/api/${id }`);
console .log (response .data === data )
setData (response .data );
} catch (error ) {
console .log (error );
}
};
fetchData ();
}, [probs .parentData ])
부모의 여기에서 온 데이터이다.
const [lastComment , setData ] = useState (null );
댓글을 작성해서 서버로 보낼 때, text가 setData로 lastCommet로 바뀐다. 그 값이 바뀔 때마다 다시 랜더링되도록 구현하였다.
또한 아래와 같이 삭제를 구현하였다.
async function submitForm (data ) {
const url = window .location .pathname ;
const id = url .substring (url .lastIndexOf ('/') + 1 );
await axios .post (`http://localhost:8080/cafe/user/review/delete/${id }`, {commentID :data })
.then ((res ) => {
setDeleteToggle (true );
})
.catch ((err ) => {
console .log (err );
});
}
현재 코멘트의 id를 넘기고 서버에서 처리한다.
app .post ('/cafe/user/review/delete/:id', async (req , res )=>{
const comment_id = req .body .commentID ;
const cafe = await Cafe .updateOne (
{_id :req .params .id },
{$pull :{comment :{_id :comment_id }}}
)
res .send ('Delete ok');
})
req.params.id는 카페의 id이다. 해당 id를 가진 카페에서 comment_id를 가진 comment를 빼낸다(배열구조이다.)
이전까지는 find로 찾은 후 거기에 이것저것 직접 수정하여 .save()를 하였는데, update를 호출하면 저장까지 한 번에 된다.
삭제와 랜더링
이게 랜더링이 참 어렵다고 느낀다. 댓글을 삭제햇을 때도 이제 랜더링이 되게끔 해야하는데, 토글 방식으로 구현하였다.
setDeleteToggle (true );
이 axios로 delete를 날리고 성공하면 위 코드를 실행하는데,
const [data , setData ] = useState (null );
const [deleteToggle , setDeleteToggle ] = useState (false );
const url = window .location .pathname ;
const id = url .substring (url .lastIndexOf ('/') + 1 );
useEffect (() => {
if (deleteToggle === true ) setDeleteToggle (false );
const fetchData = async () => {
try {
const response = await axios .get (`http://localhost:8080/cafe/api/${id }`);
console .log (response .data === data )
setData (response .data );
console .log ('실행');
} catch (error ) {
console .log (error );
}
};
fetchData ();
}, [probs .parentData , deleteToggle ]);
console .log ('토글', deleteToggle );
이곳에서 useEffect에 의존성 배열 내에 들어있는 부분이다. 즉, 해당 부분이 true가 되면 의존성 배열이 바뀌므로 useEffect가 재실행되고 다시 컴포넌트가 랜더링된다. 그리고 useEffect가 수행되면 사용자에게 보여줄 데이터를 다시 axios로 받아온다 잘 보면 토글 방식으로 동작시켰다는 것을 알 것이다. 더 나은 방법이 있으면 수정하겠지만, 리액트 써본지 4일차로서는 이 방법이 최선인 것 같다.
(...정리본을 포스팅으로 작성하는 지금 ture <-> false로 오가는 토글 방식보다 얕은 복사로 객체를 복사해 새로 할당하는 방식이 더 낫지 않을까 생각한다. 복사된 객체는 js내에서 알아서 처리해준다고 하니, 할당된 주소를 비워줄 필요도 없고 더 나은 방식 같다. )
마무리
그리고 문제가 하나 생긴 것이 서버측 페이지에서 로그아웃을 눌러도 여기서 랜더링이 다시 되지 않는다는 것이다. 하지만 새로고침을 하면 반영이 잘 된다. App컴포넌트에서 이것을 관리하기 때문이다.
이는 아무래도 네비게이션바를 여기서 구현할 것이기에, logout을 여기서 누르면 서버로 로그아웃 요청을 하고, 로그아웃을 하고 state를 바꾸면 될 것이다. 이럴 거면 그냥 보여지는 부분은 다 react로 만들 걸 그랬다. 괜히 두 개를 섞어서 만들고자 해서 일이 더 커진 느낌이 든다.