728x90
728x90

#1.7 Nicknames part One

이제 메세지를 콘솔창이 아닌 스크린에 띄워보자.

위 html 내애서 만든 li에 메세지를 입력한뒤 그 li 를  ul 안에 넣어주면 된다.

front가 새로운 메세지를 받으면 새로운 li 를 만든 후, (line 11)

새로 만든 li에 받은 메세지를 넣어주고 (line 12)

메세지를 messagList에 append 해준다. (line 13)

 

그 후 메세지를 입력하면 각 브라우저에서 보낸 메세지가 양 쪽 브라우저 스크린에 출력된다. 

 

이제 닉네임을 설정할 수 있도록 해보자. 

그러나 그 전에 앞서, 우리가 입력한 메세지가 닉네임인지 또는 채팅 내용인지를 구분해줘야한다.

html에 form 을 하나 더 추가함으로써 nick, message 가 구분되도록 한다. 

front 쪽도 수정을 해준다.

상단에 변수 하나를 더 추가하여 nickname 과 message 를 구분한다. (line 2,3)

그 후 nickForm에 대한 eventListener 도 추가하고 이에 대한 function 도 새로 작성한다. 

여기까지 하면 입력창은 두개 생기지만 message 를 구분할 수 없고 동일하게 입력한 닉네임 값을 브라우저 스크린에 그대로 출력한다. 

이 문제를 json을 활용해서 해결해보자. 

현재 input value 값이 string type 인데 이걸 json type으로 변경해주자. 

 

아래와 같이 type과 payload를 지정한 뒤 재실행해보면 

 

아래와 같이 object 형식으로 나온다. 즉 safari -> server -> chrome 으로 가는 과정 중 server 에서 type 변환이 되지 않은 것이다.

어떻게 하면 이걸 변경할 수 있을까? 

콘솔창에 아래와 같이 입력해본다. 

그럼 parsing 가능한 것을 확인할 수 있다. 즉 backend 에서 이 string type 데이터를 가져다가 JSON 형식으로 parse 할 수 있다는 것이다. 

역으로 JSON.parse 를 해보면 다시 정상적으로 원복된다. 

이제 JSON 즉 Object 형식을 기반으로 front <-> back 간에 통신을 해보자. 단 이때 front, back 양 측에 전송 전 후로 Type 변환(Object <-> String) 을 해주는 기능이 필요하다. 

 

먼저 front 쪽에 해당 기능을 구현해보자. 

line 6~9에 makeMessage 메소드를 구현했다. 

그 후 front->back 으로 메세지 전송하는 부분에 makeMessage 메소드를 넣어준다. (line 29, 38)

즉 backend 에 string 형식으로 메세지를 보내야하지만 JSON으로 type 구분을 해야하므로

string 을 보내기 전에 Object를 만들고 

makeMessage 메소드를 통해 Object를 string type 으로 변경해서 backend 로 보낸 것이다. 

 

refresh 후 nickname 에 safari 라고 입력하면 backend 가 그 메세지를 받은 후 다시 front 로 전송해준 모습을 확인할 수 있다. 

이제 type 에 따라 출력되는 것을 구별해보자. 

728x90
728x90

#1.6 Chat Completed

html 로 메세지를 전송하는 버튼과, 메세지 리스트를 보여줄 수 있는 화면을 작성해보자.

line 13 ~ 16 을 추가해준다.

 

html에서 작성한 ul, form 을 브라우저 쪽에서 변수로 선언해주고,

from 에 대한 변수인 messageForm에 eventListener 를 add 해준다. 

즉 submit 이벤트가 발생했을 때  line 17~21 메소드를 실행한다.

* preventDefault() : submit 되었을 때 defulat 동작은 초기 화면으로 돌아오는 것인데, 해당 함수를 통해 그 default 동작을 막는다. 즉 submit 이 되어도 그 화면을 그대로 유지할 수 있도록 해준다. 

 

 

 다음 서버를 실행하고 브라우저에 hello 를 입력하면 정상 실행됨을 확인할 수 있다. 

 

이제 input.value를 console.log 로 출력하는 대신, socket.send 를 사용해서 

front->back 으로 입력 값을 전송해보자. 

또한 input.value 값을 초기화해줌으로써 submit 후에 입력 창에 문자가 남아있지 않도록 해준다. 

 

서버 재실행 후 브라우저에서 숫자를 입력해보면 backend 에서 그 값을 확인할 수 있다. 

 

 

backend 가 받은 메세지를 그대로 다시 front 로 전송해보자.

line 32를 line 33으로 수정해주면 

아래와 같이 브라우저에서 hi 라고 입력 후 send 버튼을 누르는 즉시 back->front 로 그 메세지를 다시 전송하여 콘솔 창에서 그 메세지를 확인할 수 있다. 

그러나 이 기능은 메세지를 입력한 그 브라우저와 서버 간에 통신을 하는 것이다. 즉 크롬 브라우저와 사파리 브라우저간의 통신이 되지 않느다. 이를 확인하기 위해 사파리에 동일한 주소로 접속을 한 뒤 메세지를 입력하면 이는 사파리에서만 확인 가능하지 크롬에서는 확인이 불가하다.

safari

 

이제 chrome <-> server <-> safari 구조로 chrome ~ safari 사이에 통신할 수 있도록 해보자. 

현재 socket 에 누가 연결이 되었는지 모르므로 fake database를 생성해주자. 

앞으로 누군가 서버와 connection이 되면, 그 연결 정보인 socket 을 sockets 배열에 넣어줄 것이다. 

line 27, 30을 통해서 하나의 socket(브라우저)을 통해 받은 메세지를 다른 socket(브라우저들)들에 전송할 수 있다. 

현재 코드로는 자신의 브라우저에도 메세지를 보내는 방식이지만 추후 이 방식은 개선하도록 한다. 

 

line 38과 같이 연결된 모든 브라우저에 socket 을 통해 메세지를 보내주는 기능을 추가한 뒤 

크롬에서 메세지를 send 하면 safari 에서도 그 메세지가 출력됨을 볼 수 있다. 

 

반대로 safari 에서 입력해도 동일하게 chrome 에 출력된다.

 

728x90
728x90

#1.4 WebSockets Messages

앞서 server.js 에서 handleConnection 이라는 함수를 따로 정의했었는데,

on 함수의 두번째 인자 내에 바로 함수를 생성해보자. (line 26~28)

이 방식으로 하면 line 23~25 방식에 비해, socket이 현재 어떤 상태인지 알기 쉽다. 즉, 연결이 되면 socket이 인자에 들어온다는 것을 직관적으로 이해할 수 있다. 

이제 console.log 대신 "welcome to chat" 이라는 메세지를 보내보자. 

send 메소드는 server의 메소드가 아닌 socket 의 메소드이며, 이는 socket 과의 직접적인 연결을 제공해준다.

 

backend 에서 메세지를 보냈으니, 이를 Frontend에서 받아보자. 가능한 옵션은 네가지가 있고 이 중 open을 입력하자. open은 connection 이 opne 되었을 때를 의미한다.

 

backend 쪽도 로그를 추가해주자. 

 

다음으로 socket에 이벤트 두개를 추가해보자. message와 close 이벤트를 추가한 코드이다. 

message이벤트는 메세지를 받을때마다 아래 내용을 출력한다. 

서버에도 브라우저와 잘 연결되었다는 메세지가 출력된 모습이다. 

브라우저 쪽도 잘 작동한다.

이 중에 직접 받은 data 만 받고 싶다면 위 사진의 line 8 중 message 파라미터를 message.data 로 바꿔보자. 

data 만 출력한 모습이다. 

 

이 상태에서 서버 터미널에 ^C를 입력하여 서버를 kill 하면, 브라우저에서는 서버 연결이 끊겼다는 메세지가 발생한다. 

 

동일하게 서버에서도 close event에 대한 Listener 를 등록해보자. (socket.on () )

 

브라우저에서 연결을 끊기 위해 크롬 창을 닫으면,  아래와 같이 터미널창에 브라우저로부터 연결이 끊겼다는 메세지가 출력된다. 

서버와 브라우저 간 양방향 통신에 대해 알아봤고,

이제 frontend -> backend 로 메세지를 보내보자. (위에서는 backend -> frontend 만 했음)

보내기 전에 10초를 기다리기 위해 timeout 을 설정하자. 

timeout을 설정함으로써 메세지를 순차적으로 보낼 수 있다. 

브라우저가 보낸 메세지를 backend 에서 받기 위해 eventListener를 작성해보자. (line 31~33)

 

이제 서버를 재실행하면,

1. back -> front : hello! 메세지를 보냄. ( 위 사진의 Line 34)

frontend

2. front -> back : front에서 10초 후 메세지를 보내면 backend 터미널에 hello from the server 라는 메세지 출력됨.

그러나 아래와 같은 에러가 떠서

 

강의 아래 있는 코멘트를 보고 수정한 뒤 재실행 하니

정상적으로 출력된것을 확인할 수 있었다.

 

* 메세지를 받을 때, frontend 에서는 addEventListener() 메소드로 받고, backend 에서는 socket.on() 메소드로 받는다. 

728x90
728x90

출처 : 인프런_따라하며 배우는 노드, 리액트 시리즈 — 기본 강의

 

 

이번 시간에는 권한에 따라 페이지 접근 제한을 두기 위해 회원 인증 단계를 구현해보자.

페이지 접근은 다음과 같이 나눌 수 있다. 

 

 

페이지 접근 뿐 아니라 댓글 작성, 파일 전송 및 업로드 등에 관련해서도 인증 과정이 필요하다.

 

들어갈 수 있는 페이지에 대한 관리는 HOC를 쓸 것이다.

 

HOC이란?

-function으로서 다른 컴포넌트를 받아 새로운 컴포넌트를 리턴한다.

 

 

 

Auth 컴포넌트는 backend에 request를 날리고 상태를 가져올 것이다. 

 

우리가 가지고 있는 페이지(LandingPage, LoginPage, RegisterPage)를 모두 Auth 에 넣은 다음 Auth에서 처리를 하는 방식이다.

 

예를 들어 LandingPage를 넣으면 Auth는 Backend에 Request를 날린다. 현재 LandingPage에 들어와있는 사람의 상태 정보(로그인이 되어 있는지 아닌지, Admin인지 아닌지 등)를 Auth로 가져온다. 

 

 

 

Step 1) 클라이언트는 서버쪽으로 요청을 보낸다. 

 

/client/src/components/views/hoc/auth.js 파일을 새로 생성한 뒤 아래와 같이 작성한다.

여기서 서버쪽의 API로 request를 보낼 것이다. 보내기 위해 redux를 사용했다.  dispatch로 action(action이름은 auth)을 날리자. 

 

페이지가 이동할 때마다 dispatch로 계속 서버에 request를 주고 있는 것이다. 

 

파라미터 설명

- SpecificComponent : LandingPage

- option : null (아무나 출입 가능한 페이지) , true(로그인한 유저만 출입 가능), false(로그인한 유저 출입 불가능)

- adminRoute : true(관리자만 들어갈 수 있는 페이지)

import React, {useEffect} from 'react';
import axios from 'axios';
import {useDispatch} from 'react-redux'
import {auth} from '../_actions/user_action'
export default function (SpecificComponent, option, adminRoute = null){


    function AuthenticationCheck(props){

        const dispatch = useDispatch();

        useEffect(() => {
            
            dispatch(auth()).then(response => { //backend에서 처리한 정보들이 response안에 다 들어있음 
                console.log(response)
            })
            
        }, [])
        
         return (
            <SpecificComponent/>
        )
        
    }


    return AuthenticationCheck
}

 

 

Step 2) 서버에서는 미들웨어로 이동 

 

 

서버 쪽에 구현해놓은 API 를 보자.

index.js에 이미 구현해놓았다. 

get request로 요청을 받으면 미들웨어 auth가

여기로 그 요청이 넘어온다.  여기서는 토큰을 사용해서 쿠키 안에 토큰을 집어 넣는다. 그 쿠키를 보고 로그인 한 유저인지 아닌지를 판단하고 그 결과를 리액트에 넘겨준다. 

 

 

 

Step 1)-1 Action 구현

 

그리고 user_action.js에도 아래와 같이 인증 기능을 하는 함수를 추가해준다.

export function auth() {

    const request = Axios.get('/api/users/auth')        //endpoint로 get request , get이니까 login과 다르게 get 이니까 body 파라미터 필요 없음 
        .then(response => response.data)                     


        return {                    //Action 했으니까 이제 Reducer로 보냄
            type: AUTH_USER,
            payload: request
        }
    

}

 

 

Step 1)-2 Reducer 구현

 

action에 따라 결과를 리턴해주는 곳이다. AUTH_USER 케이스를 추가해준다. 

import { 
    LOGIN_USER, REGISTER_USER, AUTH_USER
} from '../_actions/types';


export default function (state={}, action){  //state 는 이전 상태 

    switch(action.type){        //Action에는 여러 타입 존재함. 이 타입에 따라 다르게 반응하도록 작성
        case LOGIN_USER:
            return {...state, loginSuccess: action.payload}       //... : spread operator은 파라미터 state를 그대로 가져온 것으로 빈 상태를 의미 
            break;

        case REGISTER_USER:
            return { ...state, register: action.payload}
            break;

        case AUTH_USER:
        return { ...state, userData: action.payload}
        break;

        default:
            return state;

    }

}

 

 

 

Step 4) 컴포넌트를 Auth HOC에 넣기

 각 컴포넌트를 Auth로 감싸주고 파라미터를 지정해서 넘겨준다. 파라미터 설명은 Step1) 에 기재했다. 

import React from 'react'

import {
  BrowserRouter as Router,
  Switch,
  Route,
  // eslint-disable-next-line
  Link  
} from "react-router-dom"; 



import LandingPage from './components/views/LandingPage/LandingPage';
import LoginPage from './components/views/LoginPage/LoginPage';
import RegisterPage from './components/views/RegisterPage/RegisterPage';
import Auth from './hoc/auth'

function App() {
  return (
    <Router>
      <div>
         
        {/*
          A <Switch> looks through all its children <Route>
          elements and renders the first one whose path
          matches the current URL. Use a <Switch> any time
          you have multiple routes, but you want only one
          of them to render at a time
        */}


{         /* 로그인한 유저는 로그인 페이지에 못들어감 */}
        <Switch>
          <Route exact path="/" component={Auth(LandingPage, null)} />
          <Route exact path="/login" component={Auth(LoginPage, false)} />  
          <Route exact path="/register" component={Auth(RegisterPage, false)} />
        </Switch>
      </div>
    </Router> 
  );
} 

export default App;


 

 

 

Step 5) 기능 확인하기

 

이제 실행해서 결과를 확인해보자.

 

1) 로그인 전 LandingPage

 

 

2)로그인 페이지 

isAuth 가 false이다. 

 

 

3) 로그인 후 LandingPage

 

 

3) 로그아웃 후 로그인 페이지 

 

 

 

 

 

Step 6) 페이지 접근 관리하기

위에 있는 payload.isAuth는 로그인 된 상태이고 아래 있는 payload.isAuth는 로그아웃된 상태이다. 각각 true, false 이므로 이에 맞게 코드를 작성하자. 

 

 

auth.js를 다음과 같이 수정해서 로그인 한 사람은 로그인 페이지나 회원가입 페이지에 접근하지 못하도록 하는 등 페이지 접근을 관리한다. 

import React, {useEffect} from 'react';
import axios from 'axios';
import {useDispatch} from 'react-redux'
import {auth} from '../_actions/user_action'
export default function (SpecificComponent, option, adminRoute = null){


    function AuthenticationCheck(props){

        const dispatch = useDispatch();

        useEffect(() => {
                    //auth는 action이름 
            dispatch(auth()).then(response => { //backend에서 처리한 정보들이 response안에 다 들어있음 
                console.log(response)


                //로그인 하지 않은 상태인데 
                if(!response.payload.isAuth) {  //option이 true이면 즉 로그인 된 유저만 들어갈 수 있어야하면 
                    if(option){
                        props.history.push('/login')
                    }
                } else{ //로그인 된 상태인데 
                    if(adminRoute && !response.payload.isAdmin){
                        props.history.push('/')
                    }
                    else{
                        if(option===false){ //option이 false면 로그인한 유저는 출입 불가능함. 로그인 페이지 or 회원가입 페이지
                            props.history.push('/')
                        }
                    } 
                }

            })
            
        }, [])

        return (
            <SpecificComponent/>
        )
    }


    return AuthenticationCheck
}

 

 

 

 

 

 

참고) 

Page 이동에 사용되는 history는 react-router-dom을 이용해서 쓰고 있는 것임. 

withRouter 로

export default withRouter(LandingPage)

코드 맨 아래에 위와 같이 처리를 해줘야 오류가 안 난다. 

 

 

 

 

 

 

 

 

여기까지 리엑트 노드를 이용한 로그인, 로그아웃, 회원가입 페이지 구현을 해 보았다.

 

직접 코드를 작성하면서 웹사이트 구현을 위한 기초를 다질 수 있었고, 이를 통해 서버 및 클라이언트의 전체적인 동작 원리 및 흐름과 MongoDB 사용법 또한 익힐 수 있었다. 

728x90
728x90

출처 : 인프런_따라하며 배우는 노드, 리액트 시리즈 — 기본 강의

 

Step 1) 시작 페이지 화면 구성

 

LandingPage.js 화면 구성하기 

div 태그 안에 style을 지정해주자.

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

 function LandingPage(){//Functional Component 만들기
     
    
    useEffect(() => {
        axios.get('/api/hello')      //endpoint. getRequest를 server 즉 index.js로 보내질 것
        .then(response => {console.log(response)})   //server 에서 돌아온 response를 콘솔창에 출력해봄
    }, [])
    
    
    return(
         <div style={{
             display: 'flex', justifyContent: 'center', alignItems: 'center',
             width: '100%', height: '100vh'
         }}>
          
          <h2> 시작 페이지 </h2>
          


         </div>
     )
 }

 export default LandingPage

 

 

 

그럼 첫 시작 화면이 아래와 같이 뜬다.

 

 

 

 

Step 2) 로그인 페이지 작성

 

onChange에 함수를 지정해줌으로써 이메일이나 비밀번호를 입력할 때, 실시간으로 state가 변하게 해준다. 

 

 

//client/src/components/views/LoginPage/LoginPage.js


import React, { useState } from 'react'
import Axios from 'axios'
import {useDispatch} from 'react-redux';
import {loginUser} from '../../../_actions/user_action'

function LoginPage(props) {     //파라미터로 props 넣어줘야함! 로그인 완료된 후 처음 화면으로 돌아가게 하기 위함 

    const dispatch = useDispatch();

    const [Email, setEmail] = React.useState(" ")
    const [Password, setPassword] = React.useState(" ")

    const onEmailHandler = (event) => {
        setEmail(event.currentTarget.value)
    }
    
    const onPasswordHandler = (event) => {
        setPassword(event.currentTarget.value)
    }

    const onSubmitHandler = (event) => {
        event.preventDefault(); //리프레시 방지-> 방지해야 이 아래 라인의 코드들 실행 가능 

        // console.log('Email', Email);
        // console.log('Password', Password);


        let body={
            email: Email,
            password: Password
        }

        //디스패치로 액션 취하기
        dispatch(loginUser(body))
        .then(response => {
            if(response.payload.loginSuccess) {
                props.history.push('/')             //리액트에서 페이지 이동하기 위해서는 props.history.push() 이용.
               										// 로그인 완료된 후 처음 화면(루트 페이지-landingpage로)으로 돌악가게 하기 
            } else{
                alert(' Error')
            }
        })


        
    }

    return (
        <div style={{
            display: 'flex', justifyContent: 'center', alignItems: 'center',
            width: '100%', height: '100vh'
        }}>
            
            
            <form style={{display: 'flex', flexDirection: 'column'}}
                onSubmit={onSubmitHandler}
            >

                <label>Email</label>
                <input type="email" value={Email} onChange={onEmailHandler}/>   
                <label>Password</label>
                <input type="password" value={Password} onChange={onPasswordHandler}/>

                <br />
                <button>
                    Login
                </button>
            </form>

        </div>
    )
}

export default LoginPage

 

 

 

Step 3) Action 기능 구현 

Login버튼을 누르면 디스패치가 action을 수행할 수 있도록 기능을 구현한다. 그 기능들은 _action/user_action.js 를 새로 생성해서 아래와 같이 코드를 작성하자. 

- Server 에 보낼 때 Axios.post() 를 이용한다. 

//client/src/_actions/user_action.js

import Axios from 'axios';
import {
    LOGIN_USER
} from './types'
export function loginUser(dataTosubmit) {

    const request = Axios.post('/api/users/login', dataTosubmit)        //서버에 리퀘스트 날리고 
        .then(response => response.data)                     //받은 데이터를 request에 저장


        return {                    //Action 했으니까 이제 Reducer로 보냄
            type: LOGIN_USER,
            payload: request
        }
    

}

 

 

 

Step 4) Reducer 기능 구현 

 

이 다음은 Reducer 차례로, (previousState, action) 을 받아서 nextState를 리턴해주는 역할을 할것이다.

 

 

Action의 여러가지 type들을 쉽게 관리하기 위해 client/src/_actions/types.js파일을 새로 만들어서 아래와 같이 코드를 작성한다.

//action의 타입들만 관리하는 파일
export const LOGIN_USER = "login_user";
 

 

 

 

그리고 client/src/_reducers/user_reducer.js 파일도 아래와 같이 작성해준다.

import { LOGIN_USER } from '../_actions/types';


export default function(state={}, action){  //state 는 이전 상태 

    switch(action.type){        //Action에는 여러 타입 존재함. 이 타입에 따라 다르게 반응하도록 작성
        case LOGIN_USER:
            return {...state, loginSuccess: action.payload}       //... : spread operator은 파라미터 state를 그대로 가져온 것으로 빈 상태를 의미 
            break;

        default:
            return state;

    }

}

 

 

 

로그인 결과 화면 

 

MongoDB에 저장해놓은 데이터에 맞게 로그인 시도를 했는데 로그인에 실패했다.

 

터미널을 보니 아래와 같은 메세지가 떠서 구글링해보았다.

message: "Could not connect to any servers in your MongoDB Atlas cluster. One common reason is that you're trying to access the database from an IP that isn't whitelisted. Make sure your current IP address is on your Atlas cluster's IP whitelist:docs.atlas.mongodb.com/security-whitelist/",

 

 

아래 사이트를 참고해서 Network Access 문제를 해결했다. 처음 MongoDB에 접속했던 곳과 다른 곳에서 접속해서 IP 주소가 달라져서 접근 문제가 있는 것 같았다. 

 

stackoverflow.com/questions/62859081/error-could-not-connect-to-any-servers-in-your-mongodb-atlas-cluster

 

Error : “Could not connect to any servers in your MongoDB Atlas cluster”

i have a issue that i tried to fix it for a long time. i try to connect to mongo atlas cloud from nodejs with mongoose npm. now, its not my first time but i just can't find the answer my mongoose v...

stackoverflow.com

 

그러나 문제는 해결되지 않았고 포트번호를 이미 쓰고 있어서 오류가 난것이었다.

위와 같이 터미널에서 kill을 해주니 정상적으로 로그인이 된 것을 확인할 수 있었다.

 

 

로그인에 성공했고 시작페이지로 돌아갔다.

 

 

 

 

 

728x90

+ Recent posts