gyeong3un2

[React.js, 스트링부트, AWS로 배우는 웹개발 101] 3장 프론트엔드 개발 -2 본문

Full Stack/[React.js, 스프링부트, AWS로 배우는 웹개발 101] 정리

[React.js, 스트링부트, AWS로 배우는 웹개발 101] 3장 프론트엔드 개발 -2

ʕっ•ᴥ•ʔっ 프론트엔드 개발하는 쿼카 2022. 12. 22. 00:35
웹개발 101 - 3장 프론트엔드 개발

[React.js, 스트링부트, AWS로 배우는 웹개발 101] 3장 프론트엔드 개발 -1

[React.js, 스트링부트, AWS로 배우는 웹개발 101] 3장 프론트엔드 개발 -2

[React.js, 스트링부트, AWS로 배우는 웹개발 101] 3장 프론트엔드 개발 -3

3.2  프론트엔드 서비스 개발

 

Todo 리스트

 

첫 번째로 Todo 컴포넌트를 만들어본다. 이 컴포넌트는 간단한 checkbox와 label을 렌더링하는 컴포넌트이다.

// Todo.js: Todo 컴포넌트
import React from "react";

const Todo = () => {
    return (
        <div classNmae = "Todo">
            <input type = "checkbox" id="todo0" name="todo0" value="todo0" />
            <label for = "todo0">Todo 컴포넌트 만들기</label>
        </div>
    );
};

export default Todo;

 

index.js는 App 컴포넌트가 렌더링되고 있다. 따라서 Todo 컴포넌트를 화면에 출력하기 위해 App 컴포넌트의 render 함수에 Todo 컴포넌트를 추가해야 한다.

// App.js
import Todo from './Todo';

function App() {
  return (
    <div className="App">
      <header className="App-header"> 
        <Todo />  
        <Todo />
      </header>
    </div>
  );
}

export default App; // 이를 통해  App이라는 컴포넌트를 
                    // 다른 컴포넌트에서 사용하도록 명시할 수 있다.

 

이 App.js에는 render 함수에 Todo 컴포넌트를 두 개를 추가했기 때문에 아래처럼 두 개의 checkbox와 label이 추가되었습니다.

 

 

Props과 useState Hook

Todo 애플리케이션의 Todo 리스트는 각 Todo마다 다른 타이틀을 가지고 있다. 이를 구현하기 위해선 Todo 컴포넌트에 title을 매개변수로 넘겨야 한다.

 

// Todo.js
import React, { useState } from "react";

const Todo = (props) => {
    const [item, setItem] = useState(props.item);

    return (
        <div className = "Todo">
            <input 
                type = "checkbox" 
                id={item.id} 
                name={item.name}
                checked={item.done} 
            />
            <label id={item.id}>{item.title}</label>
        </div>
    );
};

export default Todo;

리액트의 훅 중 하나인 useState인 함수형 컴포넌트에서 상태 변수를 사용할 수 있도록 해준다. 훅을 이용하면 리액트가 제공하는 기능과 상태변수를 사용할 수 있다.

상태변수란 시간이 지남에 따라 또 컴포넌트의 사용자가 컴포넌트와 상호작용하는 동안 변경되는 변수를 의미한다. Todo는 함수이기에 선언되고 함수가 끝나면 사라진다. 하지만, 앱의 화면은 사용자가 입력하거나 수정하는 내용, 즉 상태를 리액트가 계속 추적하고, 상태가 변할 때마다 함수를 다시 불러서 새 상태가 화면에 출력되도록 다시 렌더링해야 한다. 이때, 리액트에게 이 오브젝트가 상태라고 알려주는 함수가 useState 함수이다. useState 함수에 상태를 추적할 오브젝트를 매개 변수로 넘김으로써 상태를 사용할 수 있다.

 

useState 함수는 배열을 반환한다. 배열의 첫번째 값은 방금 상태로 지정한 오브젝트이다. 두번째 값은 이 상태를 업데이트할 수 있는 함수이다.

 

이 책에선 setItem이라는 함수를 이용해 이후 item의 값을 업데이트할 예정이다.

 

Todo의 props에 item을 넘겨주는 법을 알아보자.

// App.js
import './App.css';
import Todo from './Todo';
import React, { useState } from "react";

function App() {
  const [item, setItem] = useState({
    id: "0",
    title: "Hello World 1",
    done: true }
  );

  return (
    <div className="App">
      <header className="App"> 
        <Todo item={item}/>
      </header>
    </div>
  );
}

export default App; // 이를 통해  App이라는 컴포넌트를 
                    // 다른 컴포넌트에서 사용하도록 명시할 수 있다.

App.js에서 useState를 이용해 item을 초기화해준다. 이후 <Todo item = {<변수>}를 이용해 props로 매개변수로 넘길 수 있다.

 

 

  • Todo를 하나 더 만들어 item을 하나 더 넘기자. item의 title은 "Hello World2"이고, done은 false이다.
  • Todo를 두 개 연속으로 늘어 놓는 대신, 배열과 반복문을 이용해보자.
// App.js
import "./App.css";
import Todo from './Todo';
import React, { useState } from "react";

function App() {
  const [items, setItems] = useState([
    { id: "0", title: "Hello World 1", done: true },
    { id: "1", title: "Hello World 2", done: false }
  ]);

  let todoItems = items.length > 0 && items.map((item) => <Todo item={item} key={item.id}/>);

  return (
    <div className="App">
      { todoItems }
    </div>
  );
}

export default App; // 이를 통해  App이라는 컴포넌트를 
                    // 다른   컴포넌트에서 사용하도록 명시할 수 있다.

 

수정 후 브라우저를 확인하면 다음과 같이 리스트가 출력된다.

Todo 하나 더 만들기

 

material-ui를 이용한 디자인

 

material-ui 패키지를 이용해 UI를 변경해보자. 이 패키지는 UI를 위한 다양한 컴포넌트를 제공한다. 이때, material-ui를 사용하기 위해 material-ui의 컴포넌트를 적절한 곳에 추가하는 것이다. 컴포넌트의 기능은 컴포넌트의 이름으로 유추할 수 있다.

 

material-ui 컴포넌트 적용 후 UI 모습

 

 

Todo 추가

 

이 절에선 Todo를 추가하기 위한 UI와 백엔드 콜을 대신할 가짜 함수를 작성한다. 그리고 이 과정을 통해 이벤트 핸들러 함수를 구현하는 방법과 핸들러 함수를 UI에 연결하는 방법을 배운다. 

 

이를 위해 src 아래에 AddTodo.js를 생성하고 UI 부분의 코드를 구현한다.

 

// AddTodo.js
import React, { useState } from "react";

import { Button, Grid, TextField } from "@mui/material";

const AddTodo = (props) => {
    // 사용자의 입력을 저장할 오브젝트
    const [item, setItem] = useState({title: ""});

    return(
        <Grid container style={{ marginTop: 20}}>
            <Grid xs={11} md={11} item style={{paddingRight: 16}}>
                <TextField placeholder="Add Todo here" fullWidth />
            </Grid>
            <Grid xs={1} md={1} item>
                <Button fullWidth style={{height: '100%'}} color="secondary"  varient="outlined">
                    +
                </Button>
            </Grid>
        </Grid>
    );
}

export default AddTodo;
  • useState를 이용해 사용자의 입력을 저장할 임시 오브젝트 item을 지정한다.
  • Grid, TextField, Button을 이용해 사용자의 입력을 받을 인풋 필드와 버튼을 생성한다.

이를 화면에 올리기 위해 App.js에 AddTodo 컴포넌트를 추가한다.

 

// App.js
import AddTodo from "./AddTodo";
import {Container, ...} from "@mui/material";

...

function App() {
	...
    
	return <div className="App">
    <Container maxWidth="md">
      <AddTodo />
      <div className="TodoList">{todoItems}</div>
    </Container>
    </div>;
}

 

AddItem 핸들러 추가

앞서 만든 버튼은 기능이 없다. 이 기능을 추가하기 위해선 버튼에 핸들러 함수를 연결해야 한다.

 

각 함수가 실행되는 시점과 순서

 

기능을 구현하기 위해선 세가지 이벤트 핸들러를 구현해야 한다.

 

  • onInputChange : 사용자가 인풋필드에 키를 하나 입력할 때마다 실행되며 인풋필드에 담긴 문자열을 자바스크립트 오브젝트에 저장한다.
  • onButtonClick : 사용자가 + 버튼을 마우스로 클릭할 때 실행되며 onInputChange에서 저장하고 있던 문자열에 리스트에 추가한다.
  • enterEventKeyHandler : 사용자가 인풋필드 상에서 Enter 또는 Return 키를 눌렀을 때 실행되며 기능은 onButtonClick과 같다.

 

컴포넌트 state에 추가할 Todo 기억하기

사용자가 인풋필드에 입력하는 정보를 컴포넌트 내부에서 임시로 저장하기 위해 useState로 상태 변수 item을 초기화했다. 사용자가 인풋필드에 정보를 입력하기 시작하면, 그 정보는 TextField 컴포넌트로 전달된다. TextField는 onChange를 props로 받는데, 이 함수는 사용자가 TextField에서 키보드를 한 번 누를 때마다 실행된다. 따라서 onChange에 핸들러 함수 onInputChange를 연결해 사용자가 입력하는 키보드 값을 item에 저장할 수 있다.

 

// AddTodo.js
...

const AddTodo = (props) => {
	...

    // onInputChange 함수 작성
    const onInputChange = (e) => {
        setItem({title: e.target.value});
        console.log(item);
    };

    // onInputChange 함수 TextField에 연결
    // TextField의 onChange props로 넘기는 작업을 해주면, TextField에 사용자 입력이 들어올 때마다 함수가 실행된다.
    return(
        <Grid container style={{ marginTop: 20}}>
            <Grid xs={11} md={11} item style={{paddingRight: 16}}>
                <TextField placeholder="Add Todo here" fullWidth 
                onChange={onInputChange} value={item.title} />
            </Grid>
            
            ...
        </Grid>
    );
}

export default AddTodo;

 

onInputChange() 함수에서는 Event e를 매개변수로 받는다. 이는 어떤 사건이 일어났을 때의 상태와 그 사건에 대한 정보를 담고 있다. 이 함수에서는 사용자가 키보드를 두드릴때마다 TextField 컴포넌트에서 사건이 발생하는 것을 말한다. 이때, 가장 중요한 정보는 어떤 키 값을 입력했느냐이다.

TextField 컴포넌트는 해당 TextField에서 어떤 이벤트 발생할 때마다, 이벤트가 일어났을 때 실행해야 하는 함수, onChange()를 실행하고 매개변수로 Event e를 넘긴다.

이런 함수를 이벤트 핸들러 함수라고 한다.

 

여기선 이벤트 핸들러로 onInputChange() 함수를 구현하고 이를 TextField의 이벤트 핸들러로 onInputChange() 함수를 연결해주는 것이다. e.target.value를 item의 title로 지정한 후 setItem을 통해 item을 새로 업데이트해 사용자의 todo 아이템을 임시로 저장할 수 있다.

 

onInputChange 함수 테스트

위 사진을 보면 알 수 있듯이, TextField에 키보드 입력 시 onInputChange가 실행되면서 로그가 남는 것을 확인할 수 있다.

 

Add 함수 작성

이제 + 버튼을 눌렀을 때, Todo 아이템을 추가할 함수를 작성하자. 

 

버튼 클릭시 리스트가 업데이트되는 과정

AddTodo 컴포넌트는 상위 컴포넌트의 items에 접근할 수 없다. 하지만 상위 컴포넌트인 App 컴포넌트는 items를 관리하기 때문에 접근할 수 있다. 따라서 새 item을 리스트에 추가하는 함수는 App 컴포넌트에 작성한다. 

 

App 컴포넌트에  addItem 함수를 추가하고, 이 함수를 AddTodo의 프로퍼티로 넘겨 AddTodo에서 사용한다.

 

// App.js, add 함수 추가
function App() {
  ...

  const addItem = (item) => {
    item.id = "ID-" + items.length; // key를 위한 id
    item.done = false;              // done 초기화
    
    //업데이트는 반드시 setItems로 하고 새 배열을 만들어야 한다.
    setItems([...items, item]);	
    console.log("items: ", items);
  };

  ...
  
  return <div className="App">
    <Container maxWidth="md">
      <AddTodo addItem={addItem}/>
      <div className="TodoList">{todoItems}</div>
    </Container>
    </div>;
}

export default App;
  • setItems(Items)가 아닌 setItems([...items])처럼 새 배열을 만들어 주는 이유
    리액트는 레퍼런스를 기준으로 재렌더링한다. 배열의 레퍼런스는 배열에 값을 추가하더라도 변하지 않는다. 따라서 리액트는 이 배열에 아무 변화도 없었다고 판단하고 다시 렌더링하지 않는다. 이를 해결하기 위해 우리는 배열에 새 item을 추가할 때마다 새 배열을 만들어준다.

AddTodo 컴포넌트에서 add함수를 props로 넘겨받아 onButtonClick에서 사용하자.

 

// AddTodo.js, add 함수 사용
const AddTodo = (props) => {
    // 사용자의 입력을 저장할 오브젝트
    const [item, setItem] = useState({title: ""});
    const addItem = props.addItem;

    // onButtonClick 함수 작성
    const onButtonClick = () => {
        addItem(item); //addItem 함수 사용
        setItem({title: ""});
    }

    ...

    return(
        <Grid container style={{ marginTop: 20}}>
            ...
            
            <Grid xs={1} md={1} item>
                <Button fullWidth style={{height: '100%'}} color="secondary"  varient="outlined"
                onClick={onButtonClick}>
                    +
                </Button>
            </Grid>
        </Grid>
    );
}

 

엔터키 입력시 아이템 추가

Enter 키는 버튼 클릭과 동일한 기능을 하기 때문에 버튼 클릭 함수를 재사용하자.

 

// AddTodo.js, 엔터키 처리를 위한 핸들러 작성
const AddTodo = (props) => {
    ...

    // enterKeyEventHandler 함수 작성
    const enterKeyEventHandler = (e) => {
        if (e.key == 'Enter'){
            onButtonClick();
        }
    }

    return(
        <Grid container style={{ marginTop: 20}}>
            <Grid xs={11} md={11} item style={{paddingRight: 16}}>
                <TextField placeholder="Add Todo here" fullWidth 
                onChange={onInputChange} onKeyPress={enterKeyEventHandler} value={item.title} />
            </Grid>
            
            ...
        </Grid>
    );
}

enterKeyEventHandler() 함수는 키보드의 키 이벤트 발생시 항상 실행된다. 이때, Enter 키가 눌려서 이벤트가 발생하는 경우, onButtonClick()을 실행한다.

 

onKeyPress()는 키보드의 키를 누를 때마다 실행되는 이벤트 핸들러이다.

 

 

Todo 삭제

이 기능을 구현하려면 각 리스트 아이템의 오른쪽에 삭제 아이콘을 추가해야 한다. 그리고 사용자가 이 아이콘을 눌렀을 때, 해당 아이템을 삭제하는 기능을 추가해야 한다.

 

삭제 아이콘은 material-ui에서 제공하는 ListItemSecondaryAction과 IconButton 컴포넌트를 이용한다.

 

// Todo.js, ListItemSecondaryAction과 IconButton 사용 및 material-ui 컴포넌트 import
import React, { useState } from "react";
import { ListItem, ListItemText, InputBase, Checkbox, ListItemSecondaryAction, IconButton } from "@mui/material";
import DeleteOutlined from "@mui/icons-material/DeleteOutlined";

const Todo = (props) => {
    const [item, setItem] = useState(props.item);

    return (
        <ListItem>
            <Checkbox checked={item.done}/>
            <ListItemText>
                <InputBase
                    inputProps={{"aria-label": "naked"}}
                    type="text"
                    id={item.id}
                    name={item.id}
                    value={item.title}
                    multiline={true}
                    fullWidth={true}
                />
            </ListItemText>
            <ListItemSecondaryAction>
                <IconButton aria-label="Delete Todo">
                    <DeleteOutlined />
                </IconButton>
            </ListItemSecondaryAction>
        </ListItem>
    );
};

export default Todo;

 

이때, 우리는 InputBase에 id와 name가 존재하는지 반드시 확인해야 한다. 만약 두 아이템이 같은 title을 가지고 있다면, 어떤 아이템을 삭제해야 하는지 구분할 수 없다. 따라서 item에 고유한 id를 주고 이를 이용해 구분한다. 이 부분은 이후 백엔드와 연결하며 백엔드의 id로 대체될 예정이다.

 

 

이제 삭제 버튼을 눌렀을 때, 실행할 deleteItem() 함수를 작성할 차례이다. 이는 addItem()과 같은 방법으로, 전체 Todo 리스트는 App.js에서 관리하기 때문에 deleteItem() 함수는 App.js에 작성해야 한다.

 

// App.js, Todo의 delete에 연결
function App() {
  ...

  const deleteItem = (item) => {
    // 삭제할 아이템을 찾는다.
    const newItems = items.filter(e => e.id != item.id);

    //삭제할 아이템을 제외한 아이템을 다시 배열에 저장한다.
    setItems([...newItems]);
  }

  let todoItems = items.length > 0 && (
    <Paper style={{ margin:16 }}>
      <List>
        {items.map((item) => (
          <Todo item={item} key={item.id} deleteItem={deleteItem}/>
        ))}
      </List>
    </Paper>
  );

  ...
}

 

이 함수가 하는 일은 기존 items에서 매개변수로 넘어온 item을 제외한 새 items(newItems 변수)를 items 변수에 저장하는 것이다. 매개변수로 넘어온 item을 제외하기 위해 filter 함수를 사용하고, id를 비교해 item과 id가 같은 경우 제외하는 로직을 작성했다.

 

 

Todo 컴포넌트에 deleteItem을 Todo 컴포넌트에서 사용하는 부분을 작성하자.

// Todo.js, Todo.js에서 deleteItem() 함수 연결/추가
const Todo = (props) => {
    ...
    const deleteItem = props.deleteItem;

    // deleteEventHandler 작성
    const deleteEventHandler = () => {
        deleteItem(item);
    };

    return (
        <ListItem>
            ...
            
            <ListItemSecondaryAction>
                <IconButton aria-label="Delete Todo" onClick={deleteEventHandler}>
                    <DeleteOutlined />
                </IconButton>
            </ListItemSecondaryAction>
        </ListItem>
    );
};

 

 

 

Todo 수정

Todo 수정에는 두 가지 경우가 있다. 이는 체크박스에 체크하는 경우 또는 타이틀을 변경하고 싶은 경우이다.

 

  • 체크박스를 체크하는 경우는 비교적 간단하다. 체크박스 클릭 시, item.done의 값을 변경하면 된다.
  • 타이틀을 변경하고 싶은 경우는 조금 복잡하다. 사용자가 아이템의 title 부분을 클릭하면 자동으로 수정 가능한 상태가 되게끔 만들려고 한다. 그리고 사용자가 Enter 키를 누르면 수정 내용을 저장한다.

 

두 가지 요구 사항을 정리하면 다음과 같다.

  1. Todo 컴포넌트에 readOnly 플래그가 있어, readOnly가 true인 경우 아이템 수정이 불가능하고 false인 경우 아이템을 수정할 수 있다.
  2. 사용자가 어떤 아이템의 title을 클릭하면 해당 인풋필드는 수정 가능한 상태, 즉 readOnly가 false인 상태가 된다.
  3. 사용자가 Enter 또는 Return 키를 누르면 readOnly가 true인 상태로 전환한다.
  4. 체크박스 클릭 시 item.done 값을 전환한다. 기존 상태가 true 였으면 false로, false 였으면 true로 전환한다.

 

ReadOnly 모드

 

ReadOnly False

material-ui의 InputBase 컴포넌트에는 이미 readOnly라는 props가 있다. 따라서 Todo 컴포넌트에서 생성한 readOnly를 넘겨주기만 하면 된다. turnOffReadOnly()가 실행돼 readOnly가 false로 바뀌면 InputBase 컴포넌트가 인풋필드를 수정 가능한 상태로 변경해준다.

 

// Todo.js: ReadOnly False, readOnly 상태 변수와 turnOffReadOnly() 함수 추가 후, 연결
const Todo = (props) => {
	...
    // readOnly 상태 변수 추가
    const [readOnly, setReadOnly] = useState(true);
    
    // turnOffReadOnly 함수 작성, title 클릭 시 실행할 함수
    const turnOffReadOnly = () => {
        setReadOnly(false);
    }

    return (
        <ListItem>
            <Checkbox checked={item.done}/>
            <ListItemText>
                <InputBase
                    inputProps={{"aria-label": "naked", readOnly: readOnly}}
                    onClick={turnOffReadOnly}
                    type="text"
                    
                    ...
                />
            </ListItemText>
            
            ...
        </ListItem>
    );
};

 

ReadOnly True

ReadOnly False와는 반대로 엔터키를 누르면 readOnly 모드를 종료, 즉 readOnly를 다시 true로 바꾸는 함수를 작성하자. Enter 키 함수는 AddTodo의 enterKeyEventHandler와 거의 비슷하다.

 

// Todo.js: ReadOnly True, turnOnReadOnly() 함수 작성
const Todo = (props) => {
    ...
    
    // turnOnReadOnly 함수 작성, 엔터키 누르면 readOnly 모드 종료
    const turnOnReadOnly =(e) => {
        if (e.key == "Enter"){
            setReadOnly(true);
        }
    }

    return (
        <ListItem>
            <Checkbox checked={item.done}/>
            <ListItemText>
                <InputBase
                    inputProps={{"aria-label": "naked", readOnly: readOnly}}
                    onClick={turnOffReadOnly}
                    onKeyDown={turnOnReadOnly}
                    
                    ...
                />
            </ListItemText>
            
            ...
        </ListItem>
    );
};

 

onKeyDown은 무엇인가..?

 

Item 수정 함수

 

커서가 깜빡인다고 해서 수정이 가능한 것은 아니다. App.js에서 editItem() 함수를 작성해 Todo.js의 프로퍼티로 넘겨줘야 한다. Todo.js에서는 사용자가 키보드의 키를 입력할 때마다 item의 title을 새 값으로 변경해야 한다.

 

items의 내부 값을 변경하기 때문에 새 배열로 초기화해 화면을 재렌더링한다.

// App.js, editItem() 함수 작성과 Todo 컴포넌트에 editItem 추가
function App() {
  ...
  
  // editItem() 함수 작성
  const editItem = () => {
    setItems([...items]);
  };

  let todoItems = items.length > 0 && (
    <Paper style={{ margin:16 }}>
      <List>
        {items.map((item) => (
          // Todo 컴포넌트에 editItem 추가
          <Todo item={item} key={item.id} deleteItem={deleteItem} editItem={editItem}/>
        ))}
      </List>
    </Paper>
  );

  ...
}

 

 

사용자의 키 입력에 따라 title을 변경해주는 editEventHandler() 함수를 구현하고, 이 함수를 InputBase의 onChange props로 넘긴다.

// Todo.js, editEventHandler() 함수와 inputBase에 editEventHandler 추가
const Todo = (props) => {
    ...
    const editItem = props.editItem;

    // editEventHandler 작성
    const editEventHandler = (e) => {
        item.title = e.target.value;
        editItem();
    };
    
    ...

    return (
        <ListItem>
            <Checkbox checked={item.done}/>
            <ListItemText>
                <InputBase
                    inputProps={{"aria-label": "naked", readOnly: readOnly}}
                    onClick={turnOffReadOnly}
                    onKeyDown={turnOnReadOnly}
                    onChange={editEventHandler}
                    
                    ...
                />
            </ListItemText>
            
            ...
        </ListItem>
    );
};

 

수정 테스팅

 

Checkbox 업데이트

checkbox 업데이트를 위해 checkboxEventHandler() 함수를 구현한다. 이 함수는 체크박스에 체크가 된 경우 true를, 아닌 경우 false를 저장한다.

 

// Todo.js, checkboxEventHandler() 함수 구현 후, Checkbox onChange에 연결
const Todo = (props) => {
    ...

    const checkboxEventHandler = (e) => {
        item.done = e.target.checked;
        editItem();
    }

	...
    
    return (
        <ListItem>
            <Checkbox checked={item.done} onChange={checkboxEventHandler}/>
            
            ...
        </ListItem>
    );
};