2020.12.31 react-blog 01.login_UI_redux

01 작업환경 준비하기

0. 프로젝트 생성 
    > yarn create react-app blog-frontend
    
1. 설정파일 만들기 
    root/.prettierrc
        {
            "singleQuote": true,
            "semi": true,
            "useTabs": false,
            "tabWidth": 2,
            "trailingComma": "all",
            "printWidth": 80
        }

    root/jsconfig.json
        {
            "compilerOptions": {
                "target": "es6"
            }
        }

2. 라우터 적용 
  1) 라이브러리 설치
    > yarn add react-router-dom 
  2) 라우트 관련 컴포넌트 생성
    src/pages/LoginPage.js 로그인, RegisterPage.js 회원가입, WritePage.js 글쓰기, PostPage.js 글상세|읽기, PostListPage.js 글목록
  3) 엔트리 파일 index.js에 있는 APP을 BrowserRouter로 감싸기 
    src/index.js 
  4) 라우트 경로 지정
    src/App.js 
    * <Route component={PostListPage} path={['/@:username', '/']} exact />
      = <Route component={PostListPage} path="/" exact />
        <Route component={PostListPage} path="@:username" exact />
      = 경로/@이름 으로 접근가능 

3. 스타일 설정 
  1) 라이브러리 설치
    > yarn add styled-components
  2) 색상 팔레트 파일 생성  
    src/lib/styles/palette.js (<- https://bit.ly/mypalette)
  3) 재사용 용, 버튼 컴포넌트 만들기 
    src/components/commen/Button.js
        ...
        const Button = (props) => <StyledButton {...props} />;
  4) 글로벌 스타일 수정 
    src/index.css      

4. 리덕스 적용 
  1) 라이브러리 설치
    > yarn add redux react-redux redux-actions immer redux-devtools-extension
   2) 모듈 생성 
   src/modules/auth.js
   3) 루트 리듀서
   src/modules/index.js
   4) 엔트리 파일 index.js에 스토어 생성, Provider를 통해 리액트에 리덕스 적용 
   src/index.js       

02 로그인 및 리덕스 연동

1. UI 준비하기
  1) 스냅챗 세팅 https://snippet-generator.app/
  2) Auth 템플릿 완성하기 
  3) Auth 폼 완성하기 

2. 리덕스로 폼 상태관리하기 
  1) 모듈 작성  
   - AuthForm.js :
      import produce from 'immer'
      changeField=createAction() 
      initalizeForm=createAction() 
      initalState
      dauth=handleActions() 
  2) useDispatch와 useSelector 함수를 통해 컴포넌트(connect 대신, hooks)와 리덕스 연동 
   - LoginForm.js, 
     LoginPage.js : <AuthForm type="login" /> -> <LoginForm />
   - RegisterForm.js
     RegisterPage.js : <AuthForm type="register" /> -> <RegisterForm />
  2) 모듈 수정 
   - AuthForm.js : type, 컨테이너 props인 form, onChange, onSubmit 적용

3. API 연동하기 
  yarn add axios redux-saga

  1) API 클라이언트에 공통된 설정을 쉽게 넣을 수 있도록 별도의 axios 인스턴스를 만든다 
    > src/lib/api/client.js

        import axios from 'axios';
        const client = axios.create();

        /** 글로벌 설정 예시 
        // API 주소를 다른 곳으로 사용함
        client.defaults.baseURL = 'https://external-api-server.com';

        // 헤더 설정
        client.defaults.headers.common['Authorization'] = 'Bearer a1b2c3d4';

        // 인터셉터 설정
        axios.intercepter.response.use(
          (response) => {
            //요청 성공시 할 작업 내용 위치
            return response;
          },
          (error) => {
            //요청 실패시 할 작업 내용 위치
            return Promise.reject(error);
          },
        );
        */

        export default client;

  2) 프록시 설정 
    - 백엔드 서버 4000포트, 리액트 개발 서버는 3000포트 
    - CORS Cross Origin Request 오류
      -> 원인 : 별도 설정없이 API를 호출하는 경우 발생하는 에러 
      -> 대안 : 다른 주소에서도 API를 호출할 수 있도록 서버 코드 수정
              but 실서버에서는 같은 둘 다 호스트에서 제공할테니..   
              - 임시로-> 웹팩 개발 서버에서 지원하는 프록시Proxy 기능 사용
                = CRA의 경우, 
                  > src/package.json 파일에 "proxy":"http://localhost:4000/" 추가 
                  -> client.get('/api/posts')하면 
                    웹팩 개발 서버가 
                    프록시 역할을 해서 http://localhost:4000/api/posts에 대신 요청 후 결과물 반환해줌 
  3) api 함수 작성 
    > src/lib/api/auth.js 
        import client from './client';

        //로그인
        export const login = ({ username, password }) =>
          client.post('api/auth/login', { username, password });

        //회원가입
        export const register = ({ username, password }) =>
          client.post('api/auth/register', { username, password });

        //로그인 상태확인
        export const check = () => client.get('api/auth/check');
        
  4) api 요청 상태관리 - redux-saga 활용 
    (1) loading 리덕스 모듈작성  
      > src/modules/loading.js 
    (2) 루트 리듀서 작성    
      > src/modules/index.js 
    (3) createRequestSaga 함수 설정
      > lib/createRequestSaga.js : 사사 생성, 리팩토링(반복코드 간소화) 
      > src/modules/auth.js : 액션생성함수와 리듀서 구현 
      > src/modules/index.js : rootSaga 생성 
      > src/index.js : redux-saga 적용 

03 회원 가입

  1) 기본기능 구현 
    > src/containers/auth/RegisterForm.js 
  2) 사용자의 상태를 담을 user 리덕스 모듈 
    > src/modules/user.js
  3) root리듀서에 등록 
    > src/modules/index.js 
  4) 회원 가입 성공후 check를 호출-> 현재 사용자의 로그인 상태 확인 
    > src/containers/auth/RegisterForm.js   
  5) 회원 가입 성공후 홈 화면으로 이동 : withRouter로 컴포넌트 감싸기 
    > src/containers/auth/RegisterForm.js   

04 로그인

  1) 기본 기능 구현, 성공 후 홈으로 이동 
    > src/containers/auth/LoginForm.js   
  2) 회원인증 에러처리 : 요청이 실패했을때 에러 메시지 제공
    (1) 에러발생 dom, style 작성, props로 error를 받아왔을때, 이를 반영한 랜더링 구현
    > src/components/auth/AuthForm.js  
    (2) 로그인 시, 상황별 에러 메시지 표시 
    > src/containers/auth/LoginForm.js
    (2) 회원가입 시, 상황별 에러 메시지 표시 
    > src/containers/auth/RegisterForm.js   
      - 이름,패스워드,확인 란이 비어있을때
      - 패스워드와 확인이 불일치할때 
      - 이름이 중복될때 
  *) 해더 컴퍼넌트 만들기 전에(반응형 대응)
    > src/components/common/Responsive.js 
  3) 헤더 컴포넌트 생성 -> 로그인 후 새로고침해도 로그인이 유지됨 
    > src/components/common/Header.js 
    (1) 상시 페이지 상단에 떠 있도록 fixed , 겹치지 않도록 Spacer 컴포넌트로 헤더 크기만큼의 공간확보
    (2) PostListPage에서 랜더링 
    (3) Button 클릭 -> 페이지 이동 : Link 컴포넌트를 직접 사용 (withRouter보다 웹접근성)
    > src/components/common/Header.js 
    > src/components/common/Button.js 
  4) 로그인 상태 정보제공
    > src/containers/common/HeaderContainer.js
    > src/containers/common/Header.js
  5) 로그인 상태 유지하기 
    > 브라우저에 내장되어 있는 localStorage 활용 
    > src/containers/auth/LoginForm.js 수정
          ...
          useEffect(() => {
            if (user) {
              history.push('/');
              try {
                localStorage.setItem('user', JSON.stringify(user));
              } catch (e) {
                console.log('localStorage is not working');
              }
            }
          }, [history, user]);
          return(...);
          ...
    > src/containers/auth/RegisterForm.js 수정 
        ..동일하게 적용 
    > src/index.js 
        localStorage를 사용하여 로그인 상태 유지
  6) 로그인 점증 실패 시 초기화
    > src/modules/user.js

05 로그아웃

    > src/lib/api/auth.js : 로그아웃 함수 추가 
    > src/modules/user.js : 로그아웃 액션 추가 
                            -> 액션이 디스패치되면 api 호출 후 로컬스토리지 초기화 
    > src/containers/common/HeaderContainer.js : 로그아웃 액션생성 함수를 디스패치하는 
                                                 함수를 만들어서 Header component에 전달 
    > src/components/common/Header.js : 로그아웃 호출 

06 글쓰기

1 외부 라이브러리 설치 
    > yarn add quill
2 외부 라이브러리(에디터) 적용 컴포넌트 구성 
    > src/components/write/Editor.js 
3 글쓰기 페이지에 에디터 렌더링 
    > src/pages/writePage.js 
4 하단 컴포넌트(태그, 취소, 완료버튼) 
    1) UI작성 
        > src/components/write/TagBox.js
            - 렌더링 최적화 : 하나만 바뀌어도 전체 렌더링되는 현상 방지 
                -> TagItem, TagList 컴포넌트 분리 
                -> React.memo 적용 
            - input이 바뀔 때, 태그 목록이 바뀔때만 리렌더링 처리 
    2) 글쓰기 페이지에 하단 컴포넌트 렌더링 
        > src/pages/writePage.js 
5 태그 추가,제거 기능 구현 
        > src/components/write/TagBox.js
            - Hooks(useState, useCallback) 사용 
6 포스트 작성 및 취소 컴포턴트 
    1) UI작성
        > src/components/write/WriteActionButtons.js
    2) 글쓰기 페이지에 하단 컴포넌트 렌더링 
        > src/pages/writePage.js 

7 리덕스로 글쓰기 '상태' 관리하기 
    *글쓰기에 관련된 상태들이 모두 리덕스에서 관리 
    1) write 리덕스 모듈 작성 
        > src/modules/write.js 
    2) 루트 리듀서에 포함시키기 
        > src/modules/index.js 
    3) 컨테이너 컴포넌트 만들기
        (1) Editor 컨테이너 컴포넌트 만들기
            > src/containers/write/EditorContainer.js 
            (1-1) writePage에서 기존 Editor를 EditorContainer로 교체 
                > src/pages/writePage.js
            (1-2) Editor 컴포넌트 속성에 (각각의 객션) 추가 
                > src/components/write/Editor.js 
        (2) TagBox 컨테이너 컴포넌트 만들기
            > src/containers/write/TagBoxContainer.js 
            (2-1) writePage에서 기존 TagBox TagBoxContainer 로 교체 
                > src/pages/writePage.js
            (3-2) TagBox 컴포넌트 속성에 (각각의 객션) 추가 
                > src/components/write/TagBox.js 

        (2-1) WriteActionButtons 컨테이너 컴포넌트 만들기
            > src/containers/write/WriteActionButtonsContainer.js 

8 글쓰기 API 연동하기 
    (1) 회원인증 api는 auth.js, 포스트 api는 post.js 
        > src/lib/posts.js 
            import client from './client';
            export const writePost = ({ title, body, tags }) =>
                client.post('/api/posts', { title, body, tags });
    (2) write 리덕스 모듈에서 포스트 api를 호출하는 리덕스 액션과 writeSaga 사가 준비 
        > src/modules/write.js
    (3) 리덕스 모듈인 writeSaga를 rootSaga에 등록 
        > src/modules/index.js
    (4) WriteActionButtonsContainer.js 컨테이너 생성 
        > src/containers/write/WriteActionButtonsContainer.js 
         - 포스트 등록 버튼 -> 현재 리덕스 스토어 안의 값 사용 
                => 새 포스트 작성 
         - 취소 등록 버튼 -> history 객체를 사용 
                => withRouter로 컴포넌트를 미리 감싸 준 다음에 컨테이너를 만들어 줌 
         - 포스트 작성 성공 -> 서버에서 응답한 포스트 정보의 _id와 username 값을 참조
                => 포스트를 읽을 수 있는 경로 생성 
                => history.push를 사용하여 해당 경로로 이동 
    (5) writePage에서 기존 WriteActionButtons 컴포넌트를 WriteActionButtonsContainer로 대체

07 포스트 읽기(조회하기)

1 UI 준비
    1) PostViewer 컴포넌트 - UI : 포스트 제목, 작성자 계정명, 작성된 시간, 태그, 제목, 내용 
      > src/components/post/PostViewer.js 
    2) 페이지 연동 
      > src/pages/PostPage.js
2 API 연동
    1) post 라이브러리에 readPost api 추가 
      > src/lib/api/posts.js 
    2) post 리덕스 모듈 
      > src/moudules/post.js 
    3) PostViewer 컴포넌트를 위한 컨테이너 컴포넌트 
      > src/containers/PostViewerContainer.js 
    4) PostPage에서 기존 PostViewer 컴포넌트를 PostViewerContainer로 교체 
      > src/pages/PostPage.js
    5) 이제, PostViewer 컴포넌트에 적용된 props를 사용 
      > src/components/post/PostViewer.js