[주문앱 1탄] UI 만들기

비얌·2023년 4월 20일
6
post-thumbnail
post-custom-banner

🧹 개요

리액트 완벽 가이드라는 강의를 듣다가, 장바구니 기능이 있는 음식 주문 앱을 만들게 되었다. 그게 나에게 너무 어려워서 혼자 만들라면 못 만들 것 같다는 생각이 들었고, 그래서 오히려 해내고 싶다는 생각에 시도해보기로 결심했다😎

장바구니 기능이 메인인 음식 주문 앱은 아래와 같다.

내가 만들 주문 앱도 이와 거의 똑같다! 강사님이 작성한 양질의 코드를 배우고 익힌다는 느낌으로 만들 거라서 나뉜 컴포넌트와 파일 내용도 강사님의 코드와 거의 똑같을 것이다.

다만 그렇게만 하면 재미가 없으므로 원하는 디자인과 컨텐츠로, 또 부가적인 기능을 추가하여 만들어보려고 한다!


나는 그릭요거트를 매우 좋아하므로🥰 그릭요거트 주문 앱을 만들어볼 것이다.



🎨 와이어 프레임

피그마로 만든 와이어 프레임은 아래와 같다. 메인 화면에는 카테고리 별 재료들이 있다. 그리고 각 재료마다 수량을 선택할 수 있는 input창이 있고, 오른쪽에는 담기 버튼이 있다. 이 버튼을 누르면 장바구니에 담은 재료 목록이 들어간다.

장바구니를 눌러보면 이렇게 담은 재료의 이름, 가격, 수량을 확인할 수 있는 모달이 보인다. 그리고 여기서도 -, + 버튼으로 수량을 조절할 수 있다. 수량이 1개일 때 - 버튼을 누르면 해당 재료가 장바구니에서 삭제된다.

하단에는 총 합계가 있고, 닫기주문하기 버튼이 있다. 닫기 버튼이나 모달 밖의 공간을 클릭하면 모달이 닫히도록 할 것이다.



✨ 결과 미리보기

UI는 최종적으로 이렇게 만들어졌다!



🛫 컴포넌트 분리 계획

일단 App.jsx은 두개의 컴포넌트로 나눌 것이다. 여기서 Header 컴포넌트는 장바구니 버튼이 포함되어있고, 그 아래에 재료를 선택할 수 있는 공간을 Toppings 컴포넌트에 넣을 것이다.

// 📃 App.jsx
import './App.css'
import Header from './components/Layout/Header';
import Toppings from './components/Toppings/Toppings';

function App() {
  return (
    <>
      <Header />
      <main>
        <Toppings />
      </main>
    </>
  )
}

export default App;

그리고 컴포넌트 종류를 Cart, Layout, Toppings, UI 파트로 나눠 더 세부적으로 파일을 나누고자 했다.

Cart에는 Cart, CartItem 컴포넌트가 있으며 각각 장바구니를 클릭했을 때 나오는 것이 Cart고(그 안에 모달을 넣을 것) Cart 안에 있는 세부적인 재료 목록들이 CartItem이다.

Layout에는 Header, HeaderCartButton 컴포넌트가 있으며 Header는 장바구니 버튼이 포함된 부분이고 HeaderCartButton은 장바구니 버튼이다.



🛫 컴포넌트 나누기

Header 컴포넌트 만들기

Header 컴포넌트에 GreekZik이라는 매장명과 HeaderCartButton이라는 장바구니 버튼 컴포넌트를 넣었다.

// 📃 Header.jsx
import React from 'react';
import HeaderCartButton from './HeaderCartButton';

const Header = () => {
  return (
    <>
      <header>
        <h1>GreekZik</h1>
        <HeaderCartButton />
      </header>
    </>
  );
};

export default Header;

HeaderCartButton 컴포넌트 만들기

장바구니 버튼인 HeaderCartButton 컴포넌트에는 장바구니 아이콘과 '장바구니'라는 단어, 그리고 담은 재료의 개수를 넣었다.

담은 재료의 개수는 일단 0으로 하드코딩해놓았다.

그리고 CartIcon에는 장바구니 아이콘인 SVG 파일이 담겨있어 오로지 아이콘을 담는 역할만 한다. SVG가 뭔지, 그리고 SVG 파일을 컴포넌트로 만드는 방법은 아래에서 살펴보자

// 📃 HeaderCartButton.jsx
import React from 'react';
import CartIcon from '../Cart/CartIcon';

const HeaderCartButton = () => {
  return (
    <button>
      <span>
        <CartIcon />
      </span>
      <span>장바구니</span>
      <span>0</span>
    </button>
  );
};

export default HeaderCartButton;

CartIcon 컴포넌트 만들기

장바구니 SVG 파일을 CartIcon라는 컴포넌트로 만들 것이다.

SVG란?

SVG(Scalable Vector Graphics)는 웹 친화적인 벡터 파일 포맷입니다. JPEG와 같은 픽셀 기반의 래스터 파일과 달리, 벡터 파일(래스터vs벡터)은 그리드 위의 점과 선을 기반으로 하는 수학 공식을 통해 이미지를 저장합니다. 따라서 SVG와 같은 벡터 파일은 품질을 그대로 유지하면서 크기를 마음대로 조정할 수 있으므로 로고와 복잡한 온라인 그래픽에 아주 적합합니다.
(출처)

👉 즉, SVG 파일은 백터 파일이라는 특성으로 인해 이미지를 확대하거나 축소해도 해상도가 저하되지 않는다.


SVG 파일을 컴포넌트로 만드는 방법은 아래와 같다. svg 태그와 속성을 넣고, 그 안에 path를 적어준다.

SVG 파일을 처음 써보며 저 path가 이 파일이 저장되어 있는 주소인줄 알았는데, 이게 도형을 그리는 명령이고(예: 마지막에 있는 z는 도형 그리는 것을 마침) 이러한 명령에 따라 도형이 만들어진다는 것을 알게 되어 굉장히 신기했다.

// 📃 CartIcon.jsx
const CartIcon = () => {
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      viewBox='0 0 20 20'
      fill='currentColor'
    >
      <path d='M3 1a1 1 0 000 2h1.22l.305 1.222a.997.997 
      0 00.01.042l1.358 5.43-.893.892C3.74 11.846 4.632 14 
      6.414 14H15a1 1 0 000-2H6.414l1-1H14a1 1 0 00.894-.553l3-6A1 
      1 0 0017 3H6.28l-.31-1.243A1 1 0 005 1H3zM16 16.5a1.5 1.5 0 
      11-3 0 1.5 1.5 0 013 0zM6.5 18a1.5 1.5 0 100-3 1.5 1.5 0 000 3z' />
    </svg>
  );
};

export default CartIcon;

Toppings 컴포넌트 만들기

Toppigs 컴포넌트를 만들어볼 것이다. Toppings 컴포넌트는 ToppingsSummary 컴포넌트와 AvailableToppings 컴포넌트로 나뉜다.

// 📃 Toppings.jsx
import React from 'react';
import ToppingsSummary from './ToppingsSummary';
import AvailableToppings from './AvailableToppings';

const Toppings = () => {
  return (
    <>
      <ToppingsSummary />
      <AvailableToppings />
    </>
  );
};

export default Toppings;

Card 컴포넌트 만들기

UI 폴더에 Card 컴포넌트를 만들 것이다. 이 Card 컴포넌트는 모서리가 둥근 모양의 UI를 제공하는 컴포넌트로, 주문할 수 있는 재료들이 있는 AvailableToppings 컴포넌트 안의 내용을 이 Card로 감쌀 것이다.

클래스네임에 이곳에서 지정하는 card와 prop으로 넘겨받는 className을 동시에 사용할 수 있도록 'className={`${classes.card} ${props.className}`}'속성을 준다.

// 📃 Card.jsx
import React from 'react';
import classes from './Card.module.css';

const Card = (props) => {
  return (
    <div className={`${classes.card} ${props.className}`}>
      {props.children}
    </div>
  );
};

export default Card;

css에서 card에 패딩과 그림자를 주고 모서리를 둥글게 하고 배경색은 흰색으로 설정한다.

/* 📃 Card.module.css */
.card {
  padding: 1rem;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
  border-radius: 14px;
  background-color: white;
}

ToppingsSummary 컴포넌트 만들기

ToppinsSummary 컴포넌트에는 이 가게에 대한 설명이 들어갈 것이다. 그리고 Card 컴포넌트로 감싸서 만들어둔 모서리가 둥근 모양의 Card 안에 들어가게 한다.

// 📃 ToppingsSummary.jsx
import React from 'react';
import Card from '../UI/Card';

const ToppingsSummary = () => {
  return (
    <section>
      <Card>
        <p>최상급의 우유로 만든 꾸덕한 그릭요거트, 최고급 토핑을 제공하는 'GreekZik'입니다.</p>
        <p>완벽히 커스텀 된 당신만의 그릭요거트를 즐기세요!</p>
      </Card>
    </section>
  );
};

export default ToppingsSummary;

AvailableToppings 컴포넌트 만들기

AvailableToppings 컴포넌트 또한 Card로 감싸고, 그 안에 일단 ToppingItem 컴포넌트 한 개를 넣어준다. 나중에 map()으로 여러 개의 ToppingItem을 넣어줄 것이다.

// 📃 AvailableToppings.jsx
import React from 'react';
import Card from '../UI/Card';
import ToppingItem from './ToppingItem/ToppingItem';

const AvailableToppings = () => {
  return (
    <section>
      <Card>
        <ToppingItem />
      </Card>
    </section>
  );
};

export default AvailableToppings;

ToppingItem 컴포넌트 만들기

ToppingItem 컴포넌트에는 주문할 수 있는 하나의 재료에 대한 내용(이름, 가격, 수량, 담기버튼)이 들어가 있다. 수량과 담기버튼은 ToppingItemForm 컴포넌트를 만들어 이곳에 분리하기로 했다. 그리고 재사용을 위해 Input이라는 UI용 컴포넌트도 만들기로 했다.

ToppingItem 컴포넌트는 아래와 같이 재료 이름, 가격, 그리고 ToppingItemForm으로 이루어져 있다.

// 📃 ToppingItem.jsx
import React from 'react';
import ToppingItemForm from './ToppingItemForm';

const ToppingItem = () => {
  return (
    <>
      <div>
        <h3>무화과</h3>
        <div>1,000</div>
      </div>
      <div>
        <ToppingItemForm />
      </div>
    </>
  );
};

export default ToppingItem;

ToppingItemForm 컴포넌트 만들기

ToppingItemForm 컴포넌트에는 label과 input태그, 그리고 담기 버튼이 있다. 여기서 입력을 받는 부분은 Input 컴포넌트이며, prop으로 id, type, min, max, step, defaultValue를 전달했다.

// 📃 ToppingItemForm.jsx
import React from 'react';
import Input from '../../UI/Input';

const ToppingItemForm = () => {
  return (
    <form>
      <Input 
        label="수량" 
        input={{
          id: 1,
          type: 'number',
          min: '1',
          max: '10',
          step: '1',
          defaultValue: '1',
        }} 
      />
      <button>+ 담기</button>
    </form>
  );
};

export default ToppingItemForm;

Input 컴포넌트 만들기

UI용으로 Input 컴포넌트를 만들었다. 기본적으로 label과 input창이 있다.

  • label에서 htmlFor은 리액트 버전의 for이다.
  • {...props.input}은 Input에서 내려받은 prop.input 객체의 모든 키, 값 쌍을 가져온 것이다.
// 📃 Input.jsx
import React from 'react';

const Input = props => {
  return (
    <div>
      <label htmlFor={props.input.id} >{props.label}</label>
      {/* props.input이 가지는 모든 속성 키,값 쌍을 가져옴 */}
      <input {...props.input} />
    </div>
  );
});

export default Input;

💥 객체 prop 전체를 속성으로 받기

input이라는 객체 형태의 prop을 Input 컴포넌트에서 input 컴포넌트로 넘긴다(객체 형태의 input은 객체 형태의 속성이고, Input 안에 있는 input은 내장된 컴포넌트로 다르다)

이렇게 스프레드 연산자를 사용해 객체 내의 속성을 input에 전달하는 부분이 잘 이해가 안가 실험을 해보기로 했다.

// 📃 Input.jsx
// Input에서 input으로 input객체를 넘긴다.
<Input 
	label="수량" 
	input={{
          id: 1,
          type: 'number',
          min: '1',
          max: '10',
          step: '1',
          defaultValue: '1',
		}} 
/>

 <input {...props.input} />

아래에서 볼 수 있듯이 Input 컴포넌트를 쓰지 않고 label과 input만으로 입력창을 만들었다. input의 속성으로는 label="수량" id='1' type='number' min='1' max='10' step='1' defaultValue='1'을 넣었다.

이는 input={ id: 1, type: 'number', min: '1', max: '10', step: '1', defaultValue: '1', }라는 객체를 ...props.input으로 받아서 만든 입력창과 동일함을 알 수 있다.

<label htmlFor='2'>수량</label>
<input label="수량" id='1' type='number' min='1' max='10' step='1' defaultValue='1' />

객체의 프로퍼티는 a: b 형태로 되어있고 컴포넌트의 prop은 a={b} 형태로 되어 있어서 {...props.input}을 했을 때 속성이 잘 넘어갈지 궁금했는데, 동일하게 넘어간다는 걸 확인했다.



💥 CSS 시행착오

CSS는 강의 파일에서 가져오기로 했다.

CSS 파일을 똑같이 가져오긴 했지만, 강의록의 컴포넌트와 나의 컴포넌트가 완전히 같은 것이 아니어서 고쳐야 했던 부분을 기록해보려고 한다.

1. position: fixed를 쓸 때 컨텐츠가 가려지는 문제

상단의 Header를 고정하기 위해 position: fixed를 주면 원래 아래에 있던 부분이 위로 올라가서 헤더에 의해 가려지는 문제가 생긴다.

이때는 padding-top 등으로 위에 빈 공간을 넣어주면 된다.

참고: CSS의 fixed position으로 메뉴바 상단 고정

2. 요소가 잘리는 문제

css를 복사 붙여넣기해서 만들었을 때, 아래와 같이 장바구니 버튼이 잘린다는 문제점이 있었다.

css를 수정하며 계속 해결하려고 했는데, 결국 UI를 완성했을 때까지 해결하지 못했다. 그러다가 두가지 방법으로 해결하게 되었는데, 이 방법을 기록해보려고 한다.

1. widthpadding을 조절한다(좋은 방법이 아님)

아래의 개발자도구를 보면 알 수 있겠지만, width를 100vw에서 90vw로 수정하고, padding을 양쪽으로 10vw만큼 넣어준다. 이렇게 하면 장바구니가 짤리지 않고 모두 보인다.

2. box-sizing: border-box를 넣어준다

1번의 방법이 됐던 이유는 오른쪽의 padding이 짤렸기 때문이다. 그 이유는 box-sizing속성 때문인데, 아무런 속성을 주지 않았을 때 box-sizing: content-box가 기본적으로 적용되며 이를 box-sizing: border-box로 바꿔주어야 한다. box-sizing: border-boxbox-sizing: content-box의 차이점은 mdn 문서에서 상세히 설명하고 있다. box-sizing: content-box는 전체 크기에 padding을 포함하지 않는다. 그래서 padding을 전체 크기에 포함한다는 border-box 속성을 넣어줬고, 해결되었다.

여담으로, App 컴포넌트가 있는 가장 바깥의 css 파일에 아래처럼 추가했는데, 아무것도 변경되지 않았다. 그래서 시행착오를 겪었는데 이 css 파일을 jsx 파일에 임포트하지 않아서 변경되지 않은 것이었다..😇

* {
  box-sizing: border-box;
}

3. css가 화면에 바로 반영되지 않는 문제

css를 고쳐도 크롬 브라우저에서 바로 반영되지 않는 문제가 있었다. 이때는 강력 새로고침을 하거나 캐시 비우기를 하면 된다.

  1. F12를 눌러 개발자도구를 연다
  2. 새로고침 아이콘 위에서 마우스 오른쪽 버튼을 클릭한다
  3. 마지막의 캐시 비우기 및 강력 새로고침을 클릭한다

이렇게 하면 IDE에서 편집한 css가 바로 반영된 것을 볼 수 있다.



✨ 결과

와이어프레임대로 기능 없이 UI만을 만들어보았다!

아래가 기존에 만든 와이어프레임인데, 화면 사이즈를 일반 Desktop으로 선택했는데 생각보다 너비가 좁은 것을 발견했다. 실제 내 PC나 노트북에서 볼 때는 훨씬 너비가 넓다. 그리고 요소와 글자의 크기도 너무 크게 잡은 것 같아 줄였다.

또, 여기에는 생과일, 견과류/건과일, 수제청/시럽 등 카테고리가 있는데 이게 아직 없다. 어려워 보여서 이건 따로 포스팅을 할 계획이다.



🐹 회고

엄청 오래걸릴 거라고 생각했는데, 하루+한나절 정도 한 것 같다. 생각보다 빨리 끝나서 기분이 좋다. 물론.. 블로그 쓰는데 추가적으로 하루정도 걸렸다ㅎㅎ

다음 포스팅에서는 강의에서 배우지 않은 걸 만들 건데, 바로 토핑 카테고리이다. 할 수 있을지 잘 모르겠지만😂 꼭 해보고 싶다. 그릭요거트 토핑에 카테고리가 없을 수는 없기 때문이다... 검색을 열심히 해서 만들어보자!

profile
🐹강화하고 싶은 기억을 기록하고 공유하자🐹
post-custom-banner

0개의 댓글