[sleact] 2. 로그인, 회원가입 기능

Ju Yerina·2021년 6월 27일
1

Sleact frontend

목록 보기
2/2

CRA 없이 React와 Typescript를 이용하여 slack 을 만들어보자!

1. 회원가입 페이지 만들기

  • 중복 코드 제거나 성능 최적화의 경우는 일단 작성하고 나중에 하는 편.
  • 화면깜빡임은 화면을 다시 그리는 것은 X 일부분만 검사하여 바꿀 수도 있음

2. 커스텀 훅 만들기

  • form 입력에 관한 내용을 custom hook 을 이용해서 중복 제거 할 수 있다.
  • 구조 분해할당을 이용해서 중간 변수 선언을 건너뛸 수 있음.
    • const [password, , setPassword] = useInput('')
  • 타입스크립트는 타입 추론을 잘하지만, 매개변수의 경우는 추론이 힘드니 미리 선언 해줘야함.
    • 제너릭을 사용해서 어떤 값이 들어올 지 모를 때 제네릭을 받는 모두가 같은 type을 받을 수 있게 처리함

제네릭????

  • 타입을 변수처럼 사용해서 어떤 값을 넣을 지 모를 때 알아서 처리할 수 있게 설정해줌.
  • any와 달리 제네릭을 사용하는 모두가 같은 type을 받을 수 있다는 장점.

useInput.ts :

import { useState, useCallback, SetStateAction, Dispatch } from "react";

const useInput = <T = string>(initialData: T): [T, (e: any) => void, Dispatch<SetStateAction<T>>] => {
  const [value, setValue] = useState(initialData);
  const handler = useCallback((e)=>{
    setValue(e.currentTarget.value);
  },[]);

  return [value, handler, setValue];
};

export default useInput;
  • <T = string>(initialData: T) : T가 기본적으로 string이 들어오게 default 처리
  • : [T, (e: any) => void, Dispatch<SetStateAction<T>>] : 어떤 값을 return 할 지 type 체크
  • Dispatch<SetStateAction<T>> React에서 정의해줌
  • 가독성 처리를 위해 return type은 따로 변수처리해서 분리할 수도 있음.
type ReturnTypes<T = any> = [T, (e: any) => void, Dispatch<SetStateAction<T>>];

const useInput = <T = any>(initialData: T): ReturnTypes => {};
  • 타입 추론 에러가 났다면 any 대신 ChangeEvent<HTMLInputElement>, e.target.value 대신 e.target.value ad unknown as T를 넣으면 해결

signUp.tsx :

import React, {useCallback, useState} from "react";
import {Header, Form, Label, Input, Button, LinkContainer, Error} from "./styles";
import useInput from "@hooks/useInput";


const SignUp = () => {
  const [email, handleChangeEmail, setEmail] = useInput('');
  const [nickname, handleChangeNickname, setNickname] = useInput('');
  const [password, ,setPassword] = useInput('');
  const [passwordCheck, ,setPasswordCheck] = useInput('');
    
  const handleChangePassword = useCallback((e)=>{
    setPassword(e.target.value);
    setMismatchError(e.target.value !== passwordCheck); 
  },[passwordCheck]);
    
  const handleChangePasswordCheck = useCallback((e)=>{
    setPasswordCheck(e.target.value);
    setMismatchError(e.target.value !== password); // 기존 password 랑 같은지 비교하기
  },[passwordCheck]);
    
   

  return (
    <div id="container">
      <Header>Slack</Header>
      <Form onSubmit={handleSubmit}>
        <Label id="email-label">
          <span>이메일 주소</span>
          <div>
            <Input
              type="email"
              name="email"
              value={email}
              onChange={handleChangeEmail} />
          </div>
        </Label>
        <Label id="nickname-label">
          <span>닉네임</span>
          <Input
            type="text"
            name="nickname"
            value={nickname}
            onChange={handleChangeNickname} />
        </Label>
        <Label id="password">
          <span>비밀번호</span>
          <Input
            type="password"
            name="password"
            value={password}
            onChange={handleChangePassword} />
        </Label>
        <Label id="password-check-label">
          <span>비밀번호 확인</span>
          <div>
            <Input
              type="password"
              id="password-check"
              name="password-check"
              value={passwordCheck}
              onChange={handleChangePasswordCheck}
            />
          </div>
        </Label>
    </div>
  );
};

export default SignUp;

3. 서버에 데이터 보내기 & CORS, proxy

  • 미들웨어를 사용했던 이유 : 비동기 로직와 컴포넌트 코드 분리
    • 로직 분리가 필요없는 이유 - 컴포넌트 하나에만 사용하는 비동기 로직 (회원가입 혹은 로그인 같은 거)
    • 리덕스 단점인 코드 길어짐이 있기 때문에 안쓰고 axios로 로직 추가할 것임

1) 요청 보내기

// 비동기 api 요청
axios.post('http://loalhost:3095/api/users', {
    email, nickname, password
})
    .then(()=>{}) // 성공 
    .catch(()=>{}) // 실패
    .finally(()=>{}) // 성공 or 실패 둘 다 처리
  • network 창에서 요청이 두개씩 날라간 이유? api 를 보낸 주소와 받는 port가 서로 다르면 method OPTION을 통해 한번 더 보냄.

2) CORS 이슈

  • back-end app.js에서 cors 허용 처리

    app.use(
        cors({
          origin: true,
          credentials: true,
        })
      );
  • webpack devServer 옵션에서 proxy 설정하기

devServer: {
    historyApiFallback: true, // react router
    port: 3090,
    publicPath: '/dist/',
    proxy: {
      '/api/': { // front-end 에서 api로 보내는 요청은
        target: 'http://localhost:3095', // 이런 주소로 보내겠다 변경한다는 뜻
        changeOrigin: true,
      },
    },
  },
axios.post("/api/users", {}); // url에 localhost 적어둔 거 제거하기
  • /api/users 가 백엔드 서버인 http://localhost:3095 로 우회되면서 cors 이슈 해결.
    • network 탭에서도 요청 두 번 들어간 거 같은 도메인으로 인식하면서 한번으로 줄어들게 됨

3) 에러 메시지 & 성공 메시지 표시

const [signUpSuccess, setSignUpSuccess] = useState(false);
const [signUpError, setSignUpError] = useState('');

// 비동기 api 요청
const handleSubmit = useCallback((e) => {
    e.preventDefault();
    console.log(email, nickname, password, passwordCheck);
    if (!mismatchError && nickname) {
        
      setSignUpError(''); // 요청 보내기 전 미리 초기화
      setSignUpSuccess(false);

      // 비동기 api 요청
      axios.post("/api/users", {
        email, nickname, password
      })
        .then((response) => {
          console.log(response);
          setSignUpSuccess(true);
        })
        .catch((error) => {
          console.log(error.response);
          setSignUpError(error.response.data);
        })
        .finally(() => {
        });
    }
  }, [email, nickname, password, passwordCheck]);

// return (
{signUpSuccess && <Success>회원가입되었습니다! 로그인해주세요.</Success>}
{signUpError !== '' && <Error>{signUpError}</Error>}}
// )
  • 요청을 보내기 직전 loading 단계에서 error와 success 관련 state를 미리 초기화 시키는 편이 좋음. (요청 여러개 날릴 때 남아있던 결과가 두번째 요청에 남아있을 수 있음)

4. 로그인 기능

  • 페이지 이동 시 SPA에서 <a href="">를 사용하면 새로고침 되므로 'react-router-dom'의 <Link> 를 사용.
  • 로그인 시 로그인 했다는 정보 저장을 위한 상태 관리 필요(전역 데이터 관리)

1) SWR

SWR???

서버로부터 받아온 데이터를 가져와서 컴포넌트에게 전달하는 기능

  • 전역으로 상태를 관리하여 사용할 수 있음. (ex. 로그인 후 내 정보 관리 )

  • react-query / GraphQL 사용 시 Apollo 가 같은 역할 함.

설치

npm i swr

사용

const {data, error, isValidating, mutate} = useSWR(key, fetcher, options);

login.tsx :

const {data, error} = useSWR('http://localhost:3095/api/users', fetcher);
// 첫번째 파라미터에는 요청 보낼 주소
// 두번째 파라미터에는 요청 받고 난 다음 처리 함수이름

utils/fetcher.ts :

import axios from 'axios';

const fetcher = (url: string) => {
      axios.get(url).then((res)=> res.data).catch((err)=> err.data);
};

export default fetcher;
  • fetcher는 따로 untils 파일로 분리하여 여러곳에서 사용 가능하도록 함

  • fetcher 함수의 매개변수인 urluseSWR에 의해 자동으로 넘어감. fetcher 내에서 반환되는 data가 useSWR의 data 변수에 담기게 됨.

  • error처리는 알아서 두번째 변수에 담김.

SWR 장점

  • SWR은 브라우저 내 다른 탭으로 이동할 때 자동으로 요청을 보내주기 때문에 실시간으로 데이터를 유지해야 하는 기능을 가진 화면에서 항상 최신 데이터를 보여줄 수 있음.
  • errorRetryInterval 정상 요청임에도 서버 에러가 날 경우, 스스로 서버에 interval 로 재요청 보낼 수 있음.
  • loadingTimeout을 이용해서 로딩 시 타임아웃이 걸렸을 때, 메시지 표시하게 처리할 수도 있음.

withCredentials

현재 로그인 정보 전달은 보통 cookie를 이용함. but,,, 백엔드와 프론트 포트가 다르면 cookie 전달 불가능

  • 쿠키 전달 문제를 해결하기 위한 axios config 설정, 두번째 파라미터에 추가하기
axios.get(url, {
	withCredentials: true // true로 설정하면 백엔드에서 생성한 쿠키 전달받을 수 있음
}).then((res)=> res.data).catch((err)=> err.data);

2) SWR 설정

revalidate

SWR은 몇 초간 주기적으로 서버에 요청을 보내게 됨. 잦은 요청을 방지하기 위해 revalidate() 함수를 실행.

  • callback 함수처럼 사용함
const {data, error, revalidate} = useSWR('http://localhost:3095/api/users', fetcher);

axios.get(url, {
	withCredentials: true // true로 설정하면 백엔드에서 생성한 쿠키 전달받을 수 있음
}).then((res)=> revalidate() ).catch((err)=> err.data);
  • revalidate 정의 후 성공했을 때 실행

dedupingInterval

  • 주기적으로 실행시키지만 원하는 interval 시간을 지정하여 그 기간 내에서는 캐시를 불러오게 함.
const {data, error, revalidate} = useSWR('http://localhost:3095/api/users', fetcher, {dedupingInterval: 1000000}); // 1000초 뒤에 재호출

5. 워크스페이스 기능 & 로그아웃 처리

login.tsx :

const {data, error, revalidate} = useSWR('http://localhost:3095/api/users', fetcher); 

// 비동기 api 요청
axios.post("http://localhost:3095/api/users/login", {
    email, password
})
    .then((response) => {
    revalidate(); // 로그인 성공 시 get 재요청
})
    .catch((error) => {
    console.log(error.response);
    setLoginError(error.response?.data?.statusCode === 401);
                  })
    .finally(() => {
});
}, [email, nickname, password]);

// 로그인 성공 후 data 받아왔다면 리렌더링 되면서 channel 로 리다이렉트 처리
if(data){
    return <Redirect to={"/workspace/channel"} />
}

Workspace.tsx :

import React, { FC, useCallback } from "react";
import useSWR from "swr/esm";
import fetcher from "@utils/fetcher";
import axios from "axios";
import { Redirect } from "react-router-dom";

const Workspace: FC = ({ children }) => {
  const { data, error, revalidate } = useSWR("http://localhost:3095/api/users", fetcher);

  const handleLogout = useCallback(() => {
    axios.post("http://localhost:3095/api/users/logout", null, { withCredentials: true })
      .then(() => {
        revalidate(); // data null 처리
      });
  }, []);

  // 로그인 데이터 없을 때 redirect 처리
  // 이건 항상 hooks 보다 아래에 위치해야함...!!
  if(!data){
    return <Redirect to={"/login"}/>
  }
    
  return (
    <div>
      <button onClick={handleLogout}>로그아웃</button>
      {children}
    </div>
  );
};

export default Workspace;

로그인 처리하기 전까지 datafalse처리 되므로 상태에 따른 분기 처리 가능.

data나 error 값이 변경되는 순간 컴포넌트 리렌더링 되므로 화면 전환을 시킬 수 있음.

profile
1. 대충 2. 빨리 3. 잘 코딩하는 것이 나의 목표

0개의 댓글

관련 채용 정보