티스토리 뷰

컴포넌트와 props, map함수 개념을 복습할 겸 간단한 투두앱을 만들어보았다.

 

완성화면

 

구현 기능

1. 데이터 입력 후 추가하기

2. 추가한 데이터가 리스트에 노출되게 하기

3. 건별로 데이터 삭제하기

 

 

 

레이아웃 구성하기

 

todoApp의 가장 바깥 영역(하얀색) 부분은 App.jsx 이다.

이곳에 입력란, 추가버튼, 할일 리스트를 보여준다.

 

할일 리스트는 TodoBoard.jsx 컴포넌트로 관리한다.

TodoBoard 컴포넌트에서는 반복되는 영역인 TodoItem을 map을 이용하여 출력하도록 한다.

 

반복되는 영역이자, 할일의 건별 데이터는 TodoItem.jsx 컴포넌트로 관리한다.

건별 데이터와 삭제버튼을 출력하도록 한다.

 

 

App.jsx

import { useState } from 'react'
import './App.css'
import TodoBoard from './component/TodoBoard'

function App() {

  //input 입력값
  const [inputValue, setInputValue] = useState("");

  //todoList 전체 값들 : array
  const [todoList, setTodoList] = useState([]);

  //추가버튼 클릭
  const addItem = () => {
    setTodoList([...todoList, inputValue]); // 기존 todoList 복사 후(...todoList) 입력값 뒤에 붙여서 set
   }

  //삭제버튼 클릭
  const delItem = (index) => {
    /*
    삭제버튼 누르는 요소의 인덱스는 index 이다.

    아래 todoList.filter()의 내용은
    todoList 전체 item 중, i != index인 것만 출력한다 는 뜻이다.
    i == index 인 데이터는 삭제되고, 그걸 제외한 나머지 item이 새로운 배열이 되고,
    그 배열을 setTodoList 한다.
    */
    setTodoList( todoList.filter((item, i) => i !== index) );
  }

  return (
      <div className="container">
          <div className="contents">
              <h2>todo List</h2>
              <div className="inputArea">
                  <input className="inputBox" type="text" placeholder="할일을 입력하세요."
                    value={inputValue} onChange={(e)=>{ setInputValue(e.target.value) }}
                  />
                  <button className="addBtn" onClick={ addItem }>💛추가</button>
              </div>
              <TodoBoard todoList={todoList} delItem={delItem}></TodoBoard>
          </div>
      </div>
  )
}

export default App

 

위 코드를 하나씩 살펴보겠다.

 

 

1. input박스 state 선언

input 박스는 사용자가 입력하는 값이다.

변하는 값, 변수이므로, state로 선언해야 한다.

 

const [inputValue, setInputValue] = useState("");

 

input 박스에 입력하는 값을 inputValue라는 state로 선언하였다.

초기값은 빈 문자열("") 로 주었다.

 

<input className="inputBox" type="text" placeholder="할일을 입력하세요."
 value={inputValue} onChange={(e)=>{ setInputValue(e.target.value) }}
/>

 

input 박스에 value={inputValue} 를 주었다.

사용자가 input박스에 값을 입력할 때마다 inputValue에 값이 저장되게 하기 위해 

onChange={ (e) => {setInputValue(e.target.value) }} 이벤트를 주었다.

 

위 함수는 input 박스에 변화가 일어날 때마다 e.target.value의 값으로 setInputValue 하라는 뜻이다.

e.target.value는 사용자가 입력하는 값이다.

 

 

※만약 onChange 이벤트를 주지 않는다면..

화면에서 사용자가 값을 입력해도 input 박스에 값이 표출되지 않는다.

그 이유는, input 박스를 value={inputValue} 로 선언했기 때문이다.

value를 state로 지정했으면, set함수를 통한 state 값 변경 작업이 반드시 이뤄져야 한다.

 

 

2. 투두리스트의 각 데이터는 array로 관리한다.

const [todoList, setTodoList] = useState([]);

input에 값 입력후 추가버튼을 클릭하면 입력값을 배열에 담아서 관리하도록 한다.

todoList라는 state로 선언하였고, 초기값은 빈 배열([])로 주었다.

 

 

3. 추가버튼 이벤트

const addItem = () => {
  setTodoList([...todoList, inputValue]);
}

추가버튼을 클릭하면 기존의 todoList를 복사 후,

그 뒤에 inputValue 값을 붙여서 setTodoList를 하도록 했다.

 

<button className="addBtn" onClick={ addItem }>💛추가</button>

추가버튼에 onClick={ addItem } 를 주었다.

 

 

 

4. 삭제버튼 이벤트

삭제버튼은 TodoItem.jsx에 있는 건데 왜 App.jsx 에 작성하는지 의아할 수 있다.

간단하게 설명하자면, 리액트의 state 관리 원칙과 단방향 데이터 흐름 때문이다.

 

리액트는 데이터가 부모에서 자식으로 흐르는 단방향 데이터 흐름을 따른다.

즉, 부모 컴포넌트가 상태(state)를 관리하고, 하위 컴포넌트는 이 상태를 props를 통해 받아서 렌더링 해야 한다.

 

그렇기 때문에 삭제버튼 이벤트 delItem 은 App.jsx에 작성하는 것이고

App.jsx -> TodoBoard.jsx -> TodoItem.jsx 로 차근차근 넘겨줄 것이다. (props를 이용해서!) 

 

  const delItem = (index) => { //삭제버튼 누르는 요소의 인덱스를 index 라 한다.
    setTodoList( todoList.filter((item, i) => i !== index) );
  }

삭제버튼을 누르는 요소의 인덱스를 index라 하고, delItem의 파라미터로 넘긴다. 

함수 내에서는 todoList.filter((item, i) => i !== index) 를 한다.

todoList 배열 전체를 돌면서 요소 하나하나를 item이라 하고, 이 item의 인덱스를 i라 한다.

 

filter는 조건을 만족하는 원소들만 추출하여 새로운 배열을 만든다.

즉, i !== index 인 원소들만 추출해서(i==index 원소는 제외함) 새로운 배열을 만들고 setTodoList 한다.

 

 

그리고 delItem 함수는 TodoItem에서 이용할 것이기 때문에, TodoBoard로 넘겨준다. 

<TodoBoard todoList={todoList} delItem={delItem}></TodoBoard>

 

 

 

 

TodoBoard.jsx

import React from 'react'
import TodoItem from './TodoItem'

function TodoBoard(props) {

    return(
        <div>
            {props.todoList.map((item, index) => (
            <TodoItem item={item} key={index} index={index} delItem={props.delItem}/>)
            )}
        </div>
    )
}

export default TodoBoard

 

App에서 넘겨받은 todoList를 map을 이용해서 TodoItem 리스트를 출력한다.

각 TodoItem 마다 item, key, index, 그리고 App.jsx에서 넘겨받은 delItem 함수를 props로 넘겨준다.

 

index={index}는 TodoItem.jsx 에서 삭제기능 구현할 때 사용할 것이다.

삭제버튼을 클릭하면, 해당 데이터가 몇번째 데이터인지 알기 위해 사용하는 임시id 라고 생각하면 된다.

 

key는 리액트에서 리스트 항목을 효율적으로 추적하고 업데이트하기 위한 값으로, 반드시 고유한 값을 지정해야 한다.

여기서는 데이터의 인덱스번호를 key 값으로 주었다. (보통은 id값을 key로 준다.)

 

 

※배열의 index 값을 key로 사용했는데 괜찮을까?

배열의 형태가 ["abcd", "efg", "hijk"] 와 같이 문자열만 담기도록 했기 때문에 id가 없다.

그래서 id를 대신하기 위해 index를 key로 준 것이다.

key값에 index를 주는건 권고되지 않는 사항이나, 항목에 고유한 id가 없으므로 임시로 사용했다.

 

만약 배열이 [ {id="1", data="abcd"}, {id="2", data="efg"} ] 와 같이 객체를 담도록 설계되었다면,

id 값을 key로 주면 된다.

 

 

※삭제 구현을 할 때, index={index}값을 이용한다고 했는데, 그럼 key값의 용도는 무엇일까?

주지 않아도 기능은 동작할 수 있으나, key가 없는 경우에 콘솔에 다음과 같은 에러가 뜬다.

Warning: Each child in a list should have a unique "key" prop.

input 창에 데이터를 넣고 추가버튼을 클릭했더니 발생한 오류

오류가 뜬 경위는 다음과 같다.

input창에 데이터 작성 -> 추가버튼 클릭 -> array 배열에 담김

-> 이 array를 화면에 렌더링 시킬 때 map함수 사용 -> map에 key 값이 없네? -> 오류 발생! 

 

 

key는 리액트에서 리스트를 렌더링할 때 내부에서 활용하는 "내부식별자" 로 사용되므로,

map을 통한 리스트 출력을 구현할 때는 반드시 key 값을 주도록 하자.

그리고 key는 내부식별자이기 때문에 특정 객체에서 직접 접근할 수 없다. (이에 대한 설명은 TodoItem.jsx에서 하겠다.)

 

 

 

TodoItem.jsx

import React from 'react'

function TodoItem(props){

    //console.log(props.key) //undefined

    return(
        <div>
            <div className="ol item">{props.item}</div>
            <button className="or" onClick={ () => props.delItem(props.index) }>삭제</button>
            <br/>
        </div>
    )
}

export default TodoItem

 

console.log(props.key) 를 찍어보면 undefinded 라고 뜬다.

key는 내부 식별자이기 때문에 위와 같이 props 객체에서 직접 접근할 수 없다.

 

TodoBoard에서 TodoItem으로 props를 던질 때, key={index}와 index={index}를 함께 던졌던 것도 이러한 이유에서이다.

props.key 할 수 없기 때문에, 그 역할을 대신하여 props.index로 접근할 것이다.

 

{props.item}로 배열값을 출력하고,

삭제버튼에는 onClick시 delItem이 실행되도록 이벤트를 줬다.

delItem에 삭제할 요소가 몇번째인지를 알기 위해 props.index 값을 넘겨주었다.

 

 

App.css

/*가장 바깥 배경*/
body{
    background-color: aliceblue;
}

/*하얀색 영역*/
.container{
    /*기본 디자인*/
    margin: 0 auto;
    margin-top: 4rem;
    width: 35rem;
    min-height: 250px;
    background-color: #ffffff;
    box-shadow: 0 3px 5px rgba(0,0,0,0.16);
    border-radius: 10px;

    /* contents 안의 item이 추가되면 container가 늘어나야함 */
    display:flex;   /* 이걸주면 할일리스트가 추가되면서 하얀영역의 높이도 자동 늘어남*/
    flex-direction: column; /*이걸주면 하얀영역에 입력한 글자가 하얀영역의 가로길이를 넘어가려 할 때 아래로 내려줌  */
}

/*하얀색 영역의 모든 요소를 전체적으로 안쪽으로 들여지게함*/
.contents{
    padding: 3rem;
}

/*input박스와 추가버튼을 감싸는 div, 아래 리스트와의 간격 주기 위함*/
.inputArea{
    margin-bottom: 50px;
}

/*input 박스*/
.inputBox{
    border: 1px solid #eee;
    box-shadow: 0 3px 5px rgba(0,0,0,0.1);
    width:85%;
    height:18px;
}

/*추가 버튼*/
.addBtn{
    border: 1px solid #eee;
    border-radius: 0.5rem;
    background-color: #0e3a7b;
    color:white;
    cursor:pointer;
}

/*** 입력영역 끝 ***/

.item{
    width:85%;
    min-height:30px; /*글자가 길어지면 min-height를 줘서 영역 길어지게*/

    /*아주긴단어를입력했을때 줄바꿈 일어나게하기 */
    word-wrap: break-word;
    overflow-wrap: break-word;
    white-space: pre-wrap;
}

/*왼쪽정렬*/
.ol{
    float:left;
}

/*오른쪽정렬*/
.or{
    float:right;
}

 

css는 그냥 간단하게 작성해보았다. 

 

 

 

 

코딩알려주는 누나 유튜브, 구글, 챗지피티 등을 활용하여 구현해보았다.

삭제기능을 구현하면서 리액트의 단방향 데이터 흐름과 상태 관리에 대해 알아볼 수 있어서 좋은 시간이 되었다!

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/08   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함