[우유부단] 작업내용정리

hyo·2023년 5월 4일
1
post-thumbnail

우유부단

시작하며

정리가 늦었지만 우유부단 프로젝트에서 내가 작업한 내용을 조금 디테일하게 정리해서 포스팅해보려고한다.


1. 레이아웃

Layout컴포넌트를 만들어 모든 페이지에 공통으로 적용될 헤더바, 푸터를 만드는 작업이었다.
NextJS를 사용한 공통컴포넌트를 입히는 작업이었다.

pages 폴더내에 _app.js, _document.js를 만들고 내부에 작성하였다. _app.js & document.js 관련 포스팅

우선 레이아웃 말그대로 공통으로 사용될 컴포넌트 이므로
Layout컴포넌트를 만들고 _app.tsx에 import한 뒤

//pages/_app.tsx의 App 함수 내부
<Layout>
  <Component {...pageProps} />
</Layout>

위 처럼 감싸주면 모든 페이지에서 공통으로 Layout컴포넌트가 입혀져서 나온다.
주의사항은 _document.tsx에 공통컴포넌트 또는 CSS 를 넣는 실수는 하지 말자.

pages/_app.tsx 결과물
import { GlobalStyles } from '../styles/globalStyle';
import type { AppProps } from 'next/app';
import Layout from '../components/layout';
import Head from 'next/head';
import '../fonts/style.css';
import { ThemeProvider } from 'styled-components';
import { theme } from '../styles/theme';

//redux
import { store } from '../redux/store';
import { Provider } from 'react-redux';

export default function App({ Component, pageProps }: AppProps) {

  return (
    <>
      <Head>
        <title>우유부단</title>
      </Head>
      <Provider store={store}>
        <Layout>
          <GlobalStyles />
          <ThemeProvider theme={theme}>
            <Component {...pageProps} />
          </ThemeProvider>
        </Layout>
      </Provider>
    </>
  );
}

이제 라우팅 설정인데 NextJS의 라우팅 방식은 포스팅을 해두었기 때문에 참고해보자. NextJS 라우팅방식 관련 포스팅

어찌됏건 NextJS는 기본 React처럼 Routes, Route를 사용할 필요 없이 _app.tsx에 AppProps설정 하나만 해주면
pages 폴더내부에만 있으면 자동으로 Route설정이 된다.

import type { AppProps } from 'next/app'; 
import Layout from '../components/layout';
import Head from 'next/head';

export default function App({ Component, pageProps }: AppProps) { 

  return (
    <>
      <Head>
        <title>우유부단</title>
      </Head>
        <Layout>
            <Component {...pageProps} />
        </Layout>
    </>
  );
}

위처럼 해주고 이동하고 싶은 컴포넌트에 href로 설정해주면 된다.

예시: 어떤 버튼을 누르면 pages에 signup폴더의 index.tsx로 이동하고 싶다? 그 버튼 태그의 속성에 href='/signup'이라 설정만 하면 됨.

<button href='/signup'>회원가입 바로가기</button>


2. 회원가입

우선 pages폴더 내부에 signup 컴포넌트를 만들고 index.tsx , style.ts 파일을 만들었다.
그리고 components 폴더에 SignUp컴포넌트를 만들고
pages/signup파일에 import 해주고 컴포넌트를 분리시켰다.

//pages/signup/index.tsx
import * as S from './style';
import SignUp from '../../components/SignUp';

const SignUpPage = () => {
  return (
    <S.SignUpPageContainer>
      <SignUp />
    </S.SignUpPageContainer>
  );
};

export default SignUpPage;

우선 컴포넌트 세팅은 위처럼 하였다.

회원가입에 필요한 입력태그와 유효성검사 그리고 데이터 전송관리하기에도 편한 React-Hook-Form 라이브러리를 사용하였다.

React-Hook-form 설치

npm install react-hook-form

설치 후 세팅

// components/SignUp/index.tsx
import { useForm } from 'react-hook-form';

const SignUp = () => {
  
  const {
    register,
    handleSubmit, // form태그 내부 submit type의 button을 눌렀을때 실행시킬 함수로 인자 두개를 받을 수 있다. 첫째는 올바른 실행 두번째인자는 에러났을때 실행
    watch, // watch()를 쓰면 모든 input의 값을 객체형태로 받아온다. watch('email')은 register 지정이름이 email인 input에 입력된 text를 String형태로 불러옴.
    setValue, // setValue('email', '')라 쓰면 email input에 '' 삽입. 즉, 첫번째 인자엔 register이름, 두번째 인자엔 집어넣을 값.
    formState: { isSubmitting, errors },
  } = useForm({
    mode: 'onChange', // mode: onChange 를 써줘야 아래 errors도 확인(출력) 가능!
  });
  
  return (
    <SignUpContainer>
      <form onSubmit={handleSubmit(onValid, onInValid)}
        <input
                type="email" // input type
                placeholder="이메일을 입력해주세요."
                {...register('email', { // register를 사용하여 지금 input의 고유 이름을 지정해줄 수 있다. 'email'
                  required: '아이디 필수입력', // 입력창에 아무것도 쓰지 않았을때 필수로 입력하라는 errors.email?.message에 넣어질 문자열
                  pattern: { // 유효성 검사
                    value: EMAIL_REGEX, // Email 정규식 불러온 변수
                    message: '이메일형식이 올바르지 않습니다.', // pattern내부 value의 조건에 맞지 않을때 errors.email?.message에 들어갈 문자열
                  },
                })}
              />
        <button>회원가입완료</button> // type을 지정해주지 않으면 기본적으로 submit 타입이다.
      </form>
    </SignUpContainer>
  )
}

위처럼 기본적인 react-hook-form방식을 사용하여 구현하였다.

email중복체크와 nickname 중복체크 기능도 구현 하였는데,
hooks 폴더를 따로 만들어 button type을 button으로 주고 email 입력란에 입력을 했고 유효성을 통과한 에러메시지가 없는 문자열이라면 중복체크함수로 인자를 전달하는 방식으로 구현하였다.

<button
    type="button"
    onClick={() => {
      if (watch('email') && !errors.email?.message) {
         overLapEmailApi(watch('email'), setEmailMsg);
      }
    }}>
   중복체크
</button>


3. 로그인

로그인도 위의 회원가입과 마찬가지로 React-hook-form을 사용하여 진행하였다.
그리고 소셜로그인 기능도 구현해보았다.
그래서 로그인에 대한 정리는 소셜로그인에 대한 내용을 정리해보겠다.
소셜로그인은 OAuth2.0방식으로 진행해보았다.

OAuth2.0 관련 포스팅

OAuth방식중 하나인

위의 방식으로 구현해보았는데 정리해보면 이렇다.

1. 사용자가 로그인페이지에서 카카오로그인을 누른다.
2. client(프론트엔드)는 인증서버로 로그인 인가코드를 달라고 요청하고 주소창에 url형식으로 받는다.
3. 받은 url중 뒤에 code='' 뭐시기가 있는데 code의 값을 local Server(백엔드 서버)로 전달한다.
4. 백엔드는 받은 인가코드를 인증서버에 전달하며 AccessToken을 요청한다.
5. 인증서버는 인가코드를 확인 후 백엔드로 AccessToken을 보낸다.
6. 백엔드는 받은 AccessToken을 리소스서버에 전달하며 유저정보를 요청한다.
7. 리소스서버는 AccessToken을 확인 후 유저정보를 백엔드로 전달한다.
8. 백엔드는 유저정보와 AccessToken을 프론트엔드(Client)로 전달한다.
9. 사용자가 본다!

위 방식과 완전히 같진 않지만 내용은 같게 사용하였다.

코드로 보면

// components/Auth/index.tsx
import { useRouter } from 'next/router'; // NextJS에서 라우팅해주는 매서드

const Auth = () => {
  const router = useRouter();
  
  return (
    <div onClick={() => {
       axios.get(`${api}/kakao/oauth`)
        .then((res: AxiosResponse) => {
            router.push(res.data); // 백엔드로부터 인가코드를 받아올 페이지 즉, 리다이렉트URL을 받아서 화면 이동시킴. res.data가 리다이렉트 URL이다. 리다이렉트URL은 미리 백엔드와 말을 맞춰 여기로 해달라고 요청하여서 원하는 URL이 옴.
         })
    }}>
      <KakaoLoginImg />
    </div>
  )
}

위처럼 우선 설정을 하고 인가코드를 받을 리다이렉트 페이지를 만들어둬야한다.

// pages/auth/kakaoredirect/index.tsx

import * as S from './style';
import kakaoAuth from '../../../apis/oauth/kakaoLogin';
import { useEffect } from 'react';
import LoadingSpinner from '../../../assets/loadingspinner.gif';
import Image from 'next/image';
import { useRouter } from 'next/router';
import axios, { AxiosResponse, AxiosError } from 'axios';
import LocalStorage from '../../../constants/localstorage';

const kakaoredirect = () => {
  const router = useRouter();

  const api = process.env.NEXT_PUBLIC_SERVER_URL;
  const nowUrl = router.query; // router.query매서드는 현재 주소창의 url을 뺀 뒷부분을 출력해준다. 즉, code='' 의 code의 값 출력
  useEffect(() => {
    axios
      .get(`${api}/kakao/callback`, {
        params: nowUrl, // params로 백엔드에 인가코드를 전달
      })
      .then((res: AxiosResponse) => { // 인가코드를 백엔드로 전달하고 백엔드에서 인증서버로부터 AccessToken을 받고 리소스서버에 유저정보를 요청에 전달받고 요청이 잘 이루어졌을때! 아래 코드 실행 
        const kakao_access: any = res.headers.authorization?.split(' ')[1];
        const kakao_refresh: any = res.headers.refreshtoken;
        LocalStorage.setItem('accesstoken', kakao_access);
        LocalStorage.setItem('refreshtoken', kakao_refresh);
        router.push('/mypage');
      })
      .catch((err: AxiosError) => { // 요청실패했을때!
        console.log('err : ', err.message);
      });
  }, [nowUrl]);

  return (
    <S.KakaoRedirectContainer>
      <Image src={LoadingSpinner} alt="gif" />
    </S.KakaoRedirectContainer>
  );
};

export default kakaoredirect;

위처럼 OAuth2.0의 이해와 백엔드와의 협업으로 소셜로그인을 구현해볼 수 있었다.


4. 마이페이지

마이페이지는 사용자의 게시글, 댓글, 투표목록을 카테고리별 리스트별로 확인하고 개인정보수정을 할 수 있게 해주는 페이지다.

그리고 로그인 할때 전달받은 AccessToken을 이용하여 가장 많은 HTTP요청이 이루어지는 페이지이기도 하다.

로그인 할 때 전달받은 AccessToken을 윈도우 저장객체인 localStorage에 저장하여 마이페이지에서 요청을 할때 localStorage에서 AccessToken을 가져와 요청header에 Authorization: Bearer AccessToken 이런식으로 넣어 요청을 보내야 한다.

여기서 CSR(Client Side Rendering)을 사용할 땐 문제가 없었지만, 이번 프로젝트는 NextJS를 사용한 SSR(Server Side Rendering) 방식이기 때문에 서버에서 렌더링이 이루어진다.
문제는 서버에서 window 저장객체인 localStorage를 읽을 수 없다는 것이다.
이 문제가 마이페이지를 만들때 가장 인상적이어서 포스팅을 하였다.
NextJS에서 window저장객체 문제 포스팅

// constants/localstorage.ts
class LocalStorage {
  constructor() {}

  static setItem(key: string, value: string) {
    if (typeof window !== 'undefined') {
      localStorage.setItem(key, value);
    }
  }
  // getItem 은 return 을 넣어줘야함! setItem, removeItem이랑 다르게 값을 보여주는녀석!
  static getItem(key: string) {
    if (typeof window !== 'undefined') {
      return localStorage.getItem(key);
    }
    return null; // window객체 localStorage, sessionStorage는 값이 없을때 null
  }

  static removeItem(key: string) {
    if (typeof window !== 'undefined') {
      localStorage.removeItem(key);
    }
  }
}

export default LocalStorage;
// components/Mypage/index.tsx 내부 요청 코드

useEffect(() => {
    axios
      .get(`${api}/members/find`, {
        headers: {
          Authorization:
            LocalStorage.getItem('accesstoken') !== null
              ? `Bearer ${LocalStorage.getItem('accesstoken')}`
              : SessionStorage.getItem('accesstoken') !== null
              ? `Bearer ${SessionStorage.getItem('accesstoken')}`
              : null,
        },
      })
      .then((res: AxiosResponse) => {
        
      })
      .catch((err: AxiosError) => {
        console.log('mypage 정보요청 err : ', err.message);
      });
  }, []);

요청방식은 위 포스팅대로 해결을 하였다.


개인정보수정 페이지

마이페이지에서 개인정보수정버튼을 누르면 비밀번호 확인을 하고 개인정보수정 페이지로 넘어가는 방식으로 구현하였다.

비밀번호확인은 마찬가지로 React-Hook-Form 라이브러리를 사용하여 만들었다.

비밀번호확인이 성공하면 개인정보수정페이지로 라우팅이된다.

닉네임수정, 이미지수정, 비밀번호수정 버튼을 누르면 수정할 수 있게 입력창이 나온다.

이부분은 로컬상태관리 매서드인 useState()를 사용하여 구현하였고, 마찬가지로 입력태그는 React-Hook-Form을 사용하여 구현하였다.

이만 조금 디테일한 우유부단의 작업 내용 포스팅을 마치겠다.
profile
개발 재밌다

0개의 댓글