관리 메뉴

거니의 velog

(7) 프론트엔드 프로젝트 : 시작 및 회원 인증 구현 7 본문

React_프론트엔드 프로젝트

(7) 프론트엔드 프로젝트 : 시작 및 회원 인증 구현 7

Unlimited00 2024. 2. 23. 12:12

3. 헤더 컴포넌트 생성 및 로그인 유지

(1) 헤더 컴포넌트 만들기

* 헤더 컴포넌트를 만들기 전에 Responsive 라는 컴포넌트를 작성할 것이다. 반응형 디자인을 할 때 더 편하게 작업하기 위해서이다. Responsive 컴포넌트는 추후 다양한 컴포넌트에서 사용할 수 있게 때문에 common 디렉터리로 분류한다.

import React from 'react';
import styled from 'styled-components';

const ResponsiveBlock = styled.div`
  padding-left: 1rem;
  padding-right: 1rem;
  width: 1024px;
  margin: 0 auto; /* 중앙 정렬 */

  /* 브라우저 크기에 따라 가로 사이즈 변경 */
  @media (max-width: 1024px) {
    width: 768px;
  }
  @media (max-width: 768px) {
    width: 100%;
  }
`;

const Responsive = ({ children, ...rest }) => {
  // style, className, onClick, onMouseMove 등의 props를 사용할 수 있도록
  // ...rest를 사용하여 ResponsiveBlock에게 전달
  return <ResponsiveBlock {...rest}>{children}</ResponsiveBlock>;
};

export default Responsive;

* 이제 Header 컴포넌트를 만들어 보자. 이 컴포넌트도 포스트 페이지, 포스트 목록 페이지에서 사용되기 때문에 common 디렉터리에 작성한다.

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

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

/**
 * Responsive 컴포넌트의 속성에 스타일을 추가해서 새로운 컴포넌트 생성
 */
const Wrapper = styled(Responsive)`
  height: 4rem;
  display: flex;
  align-items: center;
  justify-content: space-between; /* 자식 엘리먼트 사이에 여백을 최대로 설정 */
  .logo {
    font-size: 1.125rem;
    font-weight: 800;
    letter-spacing: 2px;
  }
  .right {
    display: flex;
    align-items: center;
  }
`;

/**
 * 헤더가 fixed로 되어 있기 때문에 페이지의 컨텐츠가 4rem 아래 나타나도록 해주는 컴포넌트
 */
const Spacer = styled.div`
  height: 4rem;
`;

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

export default Header;

* 헤더 컴포넌트가 언제나 페이지 상단에 떠 있도록 position 값을 fixed로 설정했다. 그런데 position을 fixed로 설정하면 헤더 컴포넌트 하단에 나오는 콘텐츠가 헤더의 위치와 겹치게 된다. 그러므로 Spacer라는 컴포넌트를 만들어서 헤더 크기만큼 공간을 차지하도록 했다.

* 이제 이 컴포넌트를 PostListPage에서 렌더링해 보자.

[pages/PostListPage.js]

import React from 'react';
import Header from '../components/common/Header';

const PostListPage = () => {
  return (
    <div>
      <Header />
      <div>안녕하세요.</div>
    </div>
  );
};

export default PostListPage;

* 다음과 같은 결과가 나타나는가?

- http://localhost:3000/

Header 컴포넌트

* 여기서 로그인 버튼을 누르면 /login 페이지로 이동해야 하는데, 버튼 컴포넌트를 Link 처럼 작동시키는 두 가지 방법이 있다.

* 첫 번째 방법은 Button 컴포넌트에서 withRouter를 사용하는 것이다.

[components/common/Button.js]

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

const StyledButton = styled.button`
  border: none;
  border-radius: 4px;
  font-size: 1rem;
  font-weight: bold;
  padding: 0.25rem 1rem;
  color: white;
  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.125rem;
    `}

  ${(props) =>
    props.cyan &&
    css`
      background: ${palette.cyan[5]};
      &:hover {
        background: ${palette.cyan[4]};
      }
    `}
`;

const Button = ({ to, history, ...rest }) => {
  const onClick = (e) => {
    // to가 있다면 to로 페이지 이동
    if (to) {
      history.push(to);
    }
    if (rest.onClick) {
      rest.onClick(e);
    }
  };
  return <StyledButton {...rest} onClick={onClick} />;
};

export default withRouter(Button);

* 이렇게 history를 사용하여 to 값이 있을 경우 페이지를 이동하도록 구현한 뒤, Button 컴포넌트를 사용할 때 to 값을 props로 넣어 주면 마치 Link 컴포넌트처럼 작동한다.

[components/common/Header.js]

import React from 'react';
import styled from 'styled-components';
import Responsive from './Responsive';
import Button from './Button';
import { Link } from 'react-router-dom';

(...)

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

export default Header;

* REACTERS 라고 나타나는 로고에도 링크를 달고, Button 컴포넌트에도 링크를 달았다. 이제 헤더 컴포넌트에서 로그인 버튼을 눌렀을 때 로그인 페이지로 잘 이동하는지 확인해 보자.

* Button에서 페이지를 이동시키는 두 번째 방법은 withRouter를 사용하는 대신 Link 컴포넌트를 직접 사용하는 것이다.

[components/common/Button.js]

import React from 'react';
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: bold;
  padding: 0.25rem 1rem;
  color: white;
  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.125rem;
    `}

  ${(props) =>
    props.cyan &&
    css`
      background: ${palette.cyan[5]};
      &:hover {
        background: ${palette.cyan[4]};
      }
    `}
`;

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

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

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

export default Button;

* 위 코드에서는 StyledLink 라는 컴포넌트를 새로 만들었다. StyledButton과 똑같은 스타일을 사용하므로, 기존에 사용하던 스타일을 ButtonStyle이라는 값에 담아서 재사용했다. 그리고 Button 컴포넌트 내부에서 props.to 값에 따라 StyledLink를 사용할 지, StyledButton을 사용할지 정하도록 설정했다.

* StyledLink를 사용하는 과정에서는 props.cyan 값을 숫자 1과 0으로 변환해 주었다. 이렇게 한 이유는 styled() 함수로 감싸서 만든 컴포넌트의 경우에는 임의 props가 필터링되지 않기 때문이다(styled.button으로 만든 컴포넌트의 경우에는 cyan과 같은 임의 props가 자동으로 필터링되어 스타일을 만드는 용도로만 사용되고, 실제 button 엘리먼트에게 속성이 전달되지 않는다). 필터링이 되지 않으면 cyan={true}라는 값이 Link에서 사용하는 a 태그에 그대로 전달되는데, a 태그는 boolean 값이 임의 props로 설정되는 것을 허용하지 않는다. 숫자/문자열만 허용하기 때문에 삼항 연산자를 사용하여 boolean 을 숫자로 변환해 준 것이다.

* 이렇게 withRouter를 사용하는 방법과 Link 컴포넌트를 사용하는 방법을 배워 보았다. 두 가지 방법 중에서 Link 컴포넌트를 사용하는 것을 권장한다. 사용자가 느끼기에는 비슷하지만, 웹 접근성으로 따지면 Link 컴포넌트를 사용하는 것이 더 옳은 방식이다. Link 컴포넌트는 a 태그를 사용하기 때문이다. HTML 태그는 용도대로 사용하는 것이 좋다. 또한, Link 컴포넌트를 기반으로 구현하면 버튼에 마우스를 올렸을 때 브라우저 하단에 이동할 주소가 나타난다는 차이점도 있다.

하단에 뜨는 주소를 확인하자.