본문 바로가기

Tech/React

프론트 입문 리액트 삽질기 6 - Material UI 입혀보기

반응형

Material UI

지난번 만든 간단한 카드 페이지에 Material UI를 입혀보려 합니다. 프론트에서 비주얼은 중요하니깐요!

 

Material-UI: A popular React UI framework

React components for faster and easier web development. Build your own design system, or start with Material Design.

material-ui.com

 

react 프로젝트에서 터미널을 열어 다음 명령어를 입력합니다.

npm install @material-ui/core

일단 제일 보기 싫었던 button과 input부터 손봅니다.

import { useState } from 'react';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';

function CardForm({ onCreate = (v) => console.log(v) }) {
    const [cardName, setName] = useState("")
    const [description, setDescription] = useState("")

    const handleSubmit = (e) => {
        e.preventDefault()
        onCreate({ name: cardName, description: description })
        setName("")
        setDescription("")
    }

    return (
        <form onSubmit={handleSubmit} style={{
            padding: '8px',
            margin: '8px'
        }}>
            <TextField label="Outlined" variant="outlined" value={cardName} name="cardName" placeholder="name" onChange={e => setName(e.target.value)} />
            <TextField label="Outlined" variant="outlined" value={description} name="cardDescription" placeholder="description" onChange={e => setDescription(e.target.value)} />
            <Button variant="contained" type="submit"> Add </Button>
        </form>
    );
}

export default CardForm;

동작도 잘 되고, 변경된 스타일도 잘 적용이 되기는 하는데... 나온 ui가 생각보다 깔끔하게 떨어지질 않습니다.

엉성...

그러던 중 material ui 홈페이지의 components 페이지의 코드를 보다 보니 makeStyles 라는 hook이 눈에 띕니다.

 

@material-ui/styles - Material-UI

You can use Material-UI's styling solution in your app, whether or not you are using Material-UI components.

material-ui.com

Material UI의 styles는 사용자 정의 스타일을 직관적으로 정의할수 있도록 makeStyles hook을 제공합니다.

makeStyles hook은 JSX 내에 덕지덕지 지저분할 css 파일을 한 데 모아주는 역할(?)을 수행할 수 있습니다.

다음과 같이 코드를 변경해줍니다.

import { useState } from 'react';
import { makeStyles, Button, TextField, FormGroup } from '@material-ui/core';

const useStyles = makeStyles((theme) => ({
    formGroup: {
        '& .MuiTextField-root': {
            margin: theme.spacing(1),
            width: '25ch',
          },
        alignItems: "center",
        display: 'flex',
        flexDirection: 'row'
      }
  }));

function CardForm({ onCreate = (v) => console.log(v) }) {
    const [cardName, setName] = useState("")
    const [description, setDescription] = useState("")

    const classes = useStyles();

    const handleSubmit = (e) => {
        e.preventDefault()
        onCreate({ name: cardName, description: description })
        setName("")
        setDescription("")
    }

    return (
        <FormGroup className={classes.formGroup} onSubmit={handleSubmit}>
            <TextField id="standard-required" label="name" value={cardName} name="cardName" onChange={e => setName(e.target.value)} />
            <TextField id="standard-required" label="description" value={description} name="cardDescription" onChange={e => setDescription(e.target.value)} />
            <Button size="large" variant="contained" type="submit"> Add </Button>
        </FormGroup>
    );
}

export default CardForm;

FormGroup을 사용한 TextField, Button 정렬

이제 상단의 ReactArrayExample도 Bar 형식으로 교체합니다.

import { Fragment, useState } from 'react';
import './App.css';
import CardForm from './CardForm';
import CardInfoListView from './CardInfoListView';
import {AppBar, Toolbar, Typography} from '@material-ui/core';

function App() {
  const [cardId, setCardId] = useState(0)
  const [cardList, setCardList] = useState([])

  const handleOnCreate = (cardInfo) => {
    setCardList(cardList.concat({ id: cardId, name: cardInfo.name, description: cardInfo.description }))
    setCardId(c => c + 1)
  }

  const handleOnUpdate = (modifiedCard) => {
    setCardList(cardList.map(card => (card.id === modifiedCard.id) ? modifiedCard : card))
  }

  const handleOnDelete = (cardId) => {
    setCardList(cardList.filter(card => card.id !== cardId))
  }

  return (
    <Fragment>
        <AppBar position="sticky">
          <Toolbar>
            <Typography variant="h6">React Sample Card App</Typography>
          </Toolbar>
        </AppBar>
        <CardForm onCreate={(cardInfo) => handleOnCreate(cardInfo)} />
        <CardInfoListView cardList={cardList} handleOnUpdate={handleOnUpdate} handleOnDelete={handleOnDelete} />
    </Fragment>
  );
}

export default App;

 

이 때, Appbar의 position을 sticky로 주지 않으면 Appbar와 아래 내용이 겹쳐 보이니 주의하셔야 합니다.

CardInfoListView는 변경사항이 없으며, CardInfo는 다음과 같이 수정합니다.

import { Fragment, useEffect, useState } from "react";
import { Box, makeStyles, ButtonGroup, Button, TextField } from "@material-ui/core";

const useStyles = makeStyles((theme) => ({
    root: {
        '& .MuiTextField-root': {
            margin: theme.spacing(1),
            width: '25ch',
          },
        alignItems: "center",
        display: 'flex',
        flexDirection: 'row'
      }
  }));

function CardInfo({ info = { id: 0, name: "N/A", description: "N/A" }, handleOnUpdate, handleOnDelete }) {
    const [editing, setEditing] = useState(false)
    const [name, setName] = useState(info.name)
    const [description, setDescription] = useState(info.description)

    const classes = useStyles();

    useEffect(() => {
        console.log("CardInfo component created.")
        return () => console.log("cardInfo component destroyed.")
    }, [])

    return (
        <Box borderRadius={16} borderColor="gray.500" border={1} style={{
            padding: '8px',
            margin: '8px'
        }}>
            {
                editing
                    ?
                    <form className={classes.root} onSubmit={e => { e.preventDefault(); setEditing(false); handleOnUpdate({ id: info.id, name: name, description: description }) }} >
                        <TextField id="standard-required" label="name" name="cardName" value={name} onChange={e => setName(e.target.value)} />
                        <TextField id="standard-required" label="description" value={description} name="cardDescription" onChange={e => setDescription(e.target.value)} />
                        <ButtonGroup size="large">
                            <Button type="submit" color="primary"> Submit </Button>
                            <Button color="secondary" onClick={e => setEditing(false)}> Cancel </Button>
                        </ButtonGroup>
                    </form>
                    :
                    <Box className={classes.root}>
                        <TextField id="standard-required" label="name" name="cardName" value={info.name} disabled/>
                        <TextField id="standard-required" label="description" value={info.description} name="cardDescription" disabled/>
                        <ButtonGroup size="large">
                            <Button color="primary" onClick={e => setEditing(true)}> Edit </Button>
                            <Button color="secondary" onClick={e => handleOnDelete(info.id)}> Delete </Button>
                        </ButtonGroup>
                    </Box>
            }
        </Box>
    );
}

export default CardInfo

 

동작하는 페이지는 다음과 같습니다.

예뻐졌다!

반응형