Blogrow Project Day 02 (2)

thisisyjin·2022년 5월 19일
0

Dev Log 🐥

목록 보기
12/23

Blogrow Project

📝 DAY 02 - 220519 (2)

  • 헤더 컴포넌트 생성
  • 로그인 유지
  • 로그아웃

헤더 컴포넌트

  • 헤더 컴포넌트를 작성하기 전에, 반응형 디자인을 위한 Responsive 컴포넌트를 생성.

Responsive 컴포넌트

components/common/Responsive.js

import styled from 'styled-components';

const ResponsiveBlock = styled.div`
  padding-left: 1rem;
  padding-right: 1rem;
  width: 1024px;
  margin: 0 auto;

  @media screen and (max-width: 1024px) {
    width: 768px;
  }
  @media screen and (max-width: 768px) {
    width: 100%;
  }
`;

const Responsive = ({ children, ...rest }) => {
  return <ResponsiveBlock {...rest}>{children}</ResponsiveBlock>;
};

export default Responsive;

Header 컴포넌트

components/common/Header.js 생성

import styled from 'styled-components';
import Responsive from './Responsive';
import Button from './Button';

const HeaderBlock = styled.div`
  position: fixed;
  width: 100%;
  background: #fff;
  box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
`;

const Wrapper = styled(Responsive)`
  height: 4rem;
  display: flex;
  align-items: center;
  justify-content: space-between;

  .logo {
    font-size: 1.2rem;
    font-weight: 800;
    letter-spacing: 0.1em;
  }

  .right {
    display: flex;
    align-items: center;
  }
`;

// 헤더 높이만큼 콘텐츠를 내려야함. (position: fixed이므로)
const Spacer = styled.div`
  height: 4rem;
`;

const Header = () => {
  return (
    <>
      <HeaderBlock>
        <Wrapper>
          <div className="logo">BLOGROW</div>
          <div className="right">
            <Button>로그인</Button>
          </div>
        </Wrapper>
      </HeaderBlock>
      <Spacer />
    </>
  );
};

export default Header;

position: fixed로 헤더를 고정시킴.
-> 컨텐츠 부분을 헤더 높이만큼 내려줘야 함.
-> height: 4rem인 div를 만들어서 끼워넣기.

이제 여기에서 로그인 버튼 클릭시 /login 경로로 이동해야 함.
-> 원래대로라면 Link 컴포넌트로 만들면 됨.

방법 1. Button 컴포넌트에서 withRouter 사용.

  • react-router-dom의 withRouter 함수 사용.

-> 현재 react-router-dom v6로 가면서 withRouter가 없어짐.

대신해서 withLocation을 만들어서 아래와 같이 사용 가능.

import { useLocation } from "react-router-dom";

const withLocation = Component => props => {
    const location = useLocation();
  
    return <Component {...props} location={location} />;
  };

export default withLocation( MyComponent)

방법 2. Link 컴포넌트를 직접 사용함.
-> styled(Link)로 스타일링 똑같이 해주기.

components/common/Button.js 수정

import styled, { css } from 'styled-components';
import { Link } from 'react-router-dom';
import palette from '../../lib/styles/palette';

const buttonStyle = css`
  border: none;
  border-radius: 4px;
  font-size: 1rem;
  font-weight: 700;
  padding: 0.5rem 1rem;
  color: #fff;
  outline: none;
  cursor: pointer;

  background: ${palette.gray[8]};
  &:hover {
    background: ${palette.gray[6]};
  }

  ${(props) =>
    props.fullWidth &&
    css`
      padding-top: 0.75rem;
      padding-bottom: 0.75rem;
      width: 100%;
      font-size: 1.2rem;
    `}

  ${(props) =>
    props.teal &&
    css`
      background: ${palette.teal[7]};
      &:hover {
        background: ${palette.teal[6]};
      }
    `}
`;

const StyledButton = styled.button`
  ${buttonStyle}
`;

const StyledLink = styled(Link)`
  ${buttonStyle}
`;

const Button = (props) => {
  return props.to ? (
    <StyledLink {...props} teal={props.teal ? 1 : 0} />
  ) : (
    <StyledButton {...props} />
  );
};

export default Button;
  • css`` 안에 스타일을 작성한 후 buttonStyle이라 저장함.

  • StyledButton은 styled.button 이고
    StyledLink는 styled(Link)로 작성.

  • 삼항연산자 -> Button 컴포넌트의 props로 to가 존재하면 - StyledLink
    / 그렇지 않으면 StyledButton 렌더링함.

  • 만약 Button의 props로 'teal'이 있다면 적용. (true,false를 1,0으로 작성함.)

a 태그는 boolean 값이 임의의 props로 들어가는 것을 허용하지 않으므로, 1과 0으로 구분해야 한다.

잘 적용됨!

✅ 참고

  • 웹 접근성을 위해서라도 방법 2인 styled(Link)를 이용하는 것을 권장함!

로그인 상태 나타내기

  • 로그인 성공시 Header 컴포넌트에서 로그인중인 상태를 보여주고, 새로고침 해도 유지되도록.

Header에 리덕스 연결

  • 컨테이너 컴포넌트를 생성해야 함. (스토어와 연결하기 위해)

container/common/HeaderContainer.js 생성.

import { useSelector } from 'react-redux';
import Header from '../../components/common/Header';

const HeaderContainer = () => {
  const { user } = useSelector(({ user }) => ({ user: user.user }));
  return <Header user={user} />;
};

export default HeaderContainer;

Header 컴포넌트 수정

  • 로그인 상태이면 계정명(username)이 보이게 하고,
    '로그인' 대신 '로그아웃'이 보이도록.
import styled from 'styled-components';
import Responsive from './Responsive';
import Button from './Button';
import palette from '../../lib/styles/palette';
import { Link } from 'react-router-dom';

const HeaderBlock = styled.div`
  position: fixed;
  width: 100%;
  background: #fff;
  box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
`;

const Wrapper = styled(Responsive)`
  height: 4rem;
  display: flex;
  align-items: center;
  justify-content: space-between;

  .logo {
    font-size: 1.2rem;
    font-weight: 800;
    letter-spacing: 0.25em;
    cursor: pointer;

    &:hover {
      color: ${palette.teal[7]};
    }
  }

  .right {
    display: flex;
    align-items: center;
  }
`;

const Spacer = styled.div`
  height: 4rem;
`;

// 🔻 username 나타내게
const UserInfo = styled.div`
  font-weight: 800;
  margin-right: 1rem;
`;

const Header = ({ user }) => {
  return (
    <>
      <HeaderBlock>
        <Wrapper>
          <Link to="/" className="logo">
            BLOGROW
          </Link>
          // 🔻 user이 null인지 아닌지애 따라 렌더링 다르게
          {user ? (
            <div className="right">
              <UserInfo>{user.username}</UserInfo>
              <Button>로그아웃</Button>
            </div>
          ) : (
            <div className="right">
              <Button to="/login" teal>
                로그인
              </Button>
            </div>
          )}
        </Wrapper>
      </HeaderBlock>
      <Spacer />
    </>
  );
};

export default Header;

PostListPage 렌더링 대체

pages/PostListPage.js 수정

  • Header 대신 HeaderContainer을 렌더링해줌.
import HeaderContainer from '../containers/common/HeaderContainer';

const PostListPage = () => {
  return (
    <div>
      <HeaderContainer />
      <div>this is contents!</div>
    </div>
  );
};

export default PostListPage;

  • 로그인시 위와 같이 계정명 + 로그아웃 버튼이 나오면 성공!
  • 그러나 새로고침시 다시 상태가 초기화됨.

🔻 참고 - state (user.username)


로그인 상태 유지하기

  • 브라우저의 내장된 스토리지인 LocalStorage를 이용해보자!

Login 컨테이너 수정

containers/auth/LoginForm.js 수정

...
useEffect(() => {
    if (user) {
      navigate('/');
      // 🔻 localStorage.setItem
      try {
        localStorage.setItem('user', JSON.stringify(user));
      } catch (e) {
        console.log('local storage error');
      }
    }
  }, [navigate, user]);

❔ 왜 JSON.stringify를 해주는가?

  • JSON.stringify() : 배열이나 객체 등 모든것을 다 string으로 바꿔줌.
    user 객체를 string 형식으로 저장하기 위함.
    -> 내 이전 블로그 글을 참조!

Register 컨테이너 수정

containers/auth/RegisterForm.js 수정

  const navigate = useNavigate();
  useEffect(() => {
    if (user) {
      navigate('/'); // 홈으로 이동

      try {
        localStorage.setItem('user', JSON.stringify(user));
      } catch (e) {
        console.log('local storage error');
      }
    }
  }, [navigate, user]);
  • LoginForm과 마찬가지로 같은 코드를 넣어줌.

리액트 앱이 맨 처음 렌더링 될때 localStorage에서 값을 불러와 리덕스 스토어 안에 넣도록 구현해줘야 함.
->componenetDidMount 또는 useEffect()로 한다며, 첫 렌더링 직후 실행되므로 깜빡임 현상이 나타남

따라서, entry 폴더인 index.js에서 처리해주는 것이 좋음.
-> index.js 에서 사용자 정보를 불러오도록 처리.
-> user 모듈tempsetUser / check 모듈을 불러옴

index.js 수정

  • 깜빡임 현상을 없애기 위해.
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from 'redux-saga';
import rootReducer, { rootSaga } from './modules';
import { tempSetUser, check } from './modules/user';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(sagaMiddleware)),
);

// 🔻 localStorage에 'user'가 있으면 tempSetUser, check 액션생성함수를 dispatch 해주는 함수. 
function loadUser() {
  try {
    const user = localStorage.getItem('user');
    if (!user) return; // 로그인 상태가 아닐 시

    store.dispatch(tempSetUser(JSON.parse(user))); // 객체로 다시 바꾼 후 payload로 넣어줌
    store.dispatch(check());
  } catch (e) {
    console.log('local storge error');
  }
}

sagaMiddleware.run(rootSaga);
// 🔻 함수 호출. (반드시 sagaMiddleware.run 이후에 호출해야 함)
loadUser();

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>,
);

loadUser 함수는 반드시 sagaMiddleware.run() 이후에 호출해야 한다.
-> 먼저 호출시, CHECK 액션 디스패치시 saga에서 제대로 처리하지 않기 때문.

⚡️ 과정

  • CHECK 액션 디스패치
  • saga를 통해 client.get(/api/check) -> axios API 요청
  • 만약 check API 호출 실패시 - user state를 초기화하고, localStorage도 비워줘야 함.

로그인 검증 실패시 정보 초기화

modules/user.js 수정

...

// saga 생성
const checkSaga = createRequestSaga(CHECK, authAPI.check); //  request = client.get('/api/auth/check')

// 🔻검증 실패시 정보 초기화
function checkFailureSaga() {
  try {
    localStorage.removeItem('user');
  } catch (e) {
    console.log('local storage error');
  }
}

export function* userSaga() {
  yield takeLatest(CHECK, checkSaga);
  // 🔻 CHECK_FAILURE 액션이 발생할 시 checkFailureSaga가 실행되도록.
  yield takeLatest(CHECK_FAILURE, checkFailureSaga);
} // userSaga를 rootSaga로 합쳐줌

checkFailureSaga 함수에서는 yield를 사용하지 않는 일반 함수이므로, function* 으로 만들어주지 않아도 됨.

test

쿠키 초기화

개발자도구 - Application 탭에서 [모든 쿠키 삭제] 아이콘을 눌러주면 된다.

-> 쿠키 삭제 후 새로고침을 하면, 로그인 상태가 초기화 되어있다.

어플리케이션 탭에서 로컬스토리지를 보면 아무 값도 없이 비어있다!


로그아웃 기능 구현

    1. 로그아웃 API 호출 (auth 모듈에 logout 함수 추가)
    1. localStorage.removeItem 해주기.

로그아웃 함수 추가

lib/api/auth.js 수정

// axios 요청
import client from './client';

export const login = ({ username, password }) =>
  client.post('/api/auth/login', { username, password });
// Path, payload 를 인자로 받음.

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

export const check = () => client.get('/api/auth/check');

// 🔻 logout 함수 추가
export const logout = () => client.post('/api/auth/logout');

user 모듈 수정

  • LOGOUT 이라는 액션 생성.
    -> 디스패치되면 API 호출localStorage.removeItem() 해주기.

modules/user.js

import { createAction, handleActions } from 'redux-actions';
import { takeLatest, call } from 'redux-saga/effects';
import * as authAPI from '../lib/api/auth';
import createRequestSaga, {
  createRequestActionTypes,
} from '../lib/createRequestSaga';

const TEMP_SET_USER = 'user/TEMP_SET_USER';
const [CHECK, CHECK_SUCCESS, CHECK_FAILURE] =
  createRequestActionTypes('user/CHECK');
const LOGOUT = 'user/LOGOUT';

export const tempSetUser = createAction(TEMP_SET_USER, (user) => user);
export const check = createAction(CHECK);
export const logout = createAction(LOGOUT);

// saga 생성
const checkSaga = createRequestSaga(CHECK, authAPI.check); //  request = client.get('/api/auth/check')

// 검증 실패시 정보 초기화
function checkFailureSaga() {
  try {
    localStorage.removeItem('user');
  } catch (e) {
    console.log('local storage error');
  }
}

// 🔻 사가 생성. (API 호출이므로, yield 필요)
function* logoutSaga() {
  try {
    yield call(authAPI.logout); // logoutAPI 호출
    localStorage.removeItem('user');
  } catch (e) {
    console.log(e);
  }
}

export function* userSaga() {
  yield takeLatest(CHECK, checkSaga);
  yield takeLatest(CHECK_FAILURE, checkFailureSaga);
  // 🔻 LOGOUT 디스패치시 logoutSaga 함수 실행
  yield takeLatest(LOGOUT, logoutSaga);
} // userSaga를 rootSaga로 합쳐줌

const initialState = {
  user: null,
  checkError: null,
};

export default handleActions(
  {
    [TEMP_SET_USER]: (state, { payload: user }) => ({
      ...state,
      user,
    }),
    [CHECK_SUCCESS]: (state, { payload: user }) => ({
      ...state,
      user,
      checkError: null,
    }),
    [CHECK_FAILURE]: (state, { payload: error }) => ({
      ...state,
      user: null,
      checkError: error,
    }),
    // 🔻 state.user도 지워줌
    [LOGOUT]: (state) => ({
      ...state,
      user: null,
    }),
  },
  initialState,
);
  • redux-saga의 call 함수 이용 (call = 첫인자에 두번째 인자를 전달하여 호출)
    -> API 호출시 yield call(api명)

✅ 참고 - call()과 put()의 차이
1. call = API 요청 등
yield call(request, action.payload) // request에 action.payload를 전달하여 호출
yield call(API.logout) // logout API 호출

  1. put = 액션 디스패치
    yield put(startLoading(type)) // 액션생성함수(페이로드) 가 인자로 들어감.
    yield put({ type: SUCCESS, payload: response.data, }) // 페이로드(객체)만 적어줌

onLogout 함수 전달

containers/common/HeaderContainer.js 수정

  • 컨테이너 컴포넌트에서 onLogout 이라는 함수를 만들어서 Header의 props로 전달해줌.
import { useDispatch, useSelector } from 'react-redux';
import Header from '../../components/common/Header';
import { logout } from '../../modules/user';

const HeaderContainer = () => {
  const { user } = useSelector(({ user }) => ({ user: user.user })); // null 또는 username이 전달됨
  const dispatch = useDispatch();

  const onLogout = () => {
    dispatch(logout());
  };
  return <Header user={user} onLogout={onLogout} />;
};

export default HeaderContainer;

Header 컴포넌트

props로 받아온 onLogout함수를 onClick에 연결시킴.

...

const Header = ({ user, onLogout }) => {
  return (
    <>
      <HeaderBlock>
        <Wrapper>
          <Link to="/" className="logo">
            BLOGROW
          </Link>
          {user ? (
            <div className="right">
              <UserInfo>{user.username}</UserInfo>
              <Button onClick={onLogout}>로그아웃</Button>
            </div>
          ) : (
            <div className="right">
              <Button to="/login" teal>
                로그인
              </Button>
            </div>
          )}
        </Wrapper>
      </HeaderBlock>
      <Spacer />
    </>
  );
};

export default Header;
profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글