Event Listening & Event Handler

일반적인 JS 방식에서 해당 element를 찾아 addEventListener를 만들어줬다면, 리액트에서는 JSX 내에서 특별한 prop을 사용하여 이벤트를 들을 수 있다. on으로 시작하는 속성은 모두 이벤트와 관련된 요소이다. 

 

<button onClick={}>버튼</button>

위와 같이 onClick 속성에 넘겨줘야 하는 값은 함수의 이름이다. 

const clickHandler = () => console.log("clicked!);

흔히 할 수 있는 실수는 다음과 같다. 이 코드는 파싱되는 즉시 함수 실행이 되어버린다. 즉 JSX 코드가 읽히는 순간에 실행될 것이다.

<button onClick={clickHandler()}>버튼</button>

 

우리는 버튼을 클릭할 때에만 해당 함수를 실행하고 싶기 때문에 함수를 실행( () 붙이기) 하는 것이 아니라, 함수의 이름만 넘겨줘야 한다.

<button onClick={clickHandler}>버튼</button>

* 이벤트를 처리해주는 함수 이름은 ~Handler로 끝나는 편이 식별이 쉽다. 개인적으로 이 방식이 깔끔해서 나도 동일하게 작성하려 한다.

 

Component 함수는 어떻게 실행되는가?

이벤트에서 화면에 띄우는 변수를 바꾸더라도 화면은 바뀌지 않는다. 왜 바뀌지 않을까?

Component 자체가 하나의 함수임을 기억한다면 화면이 바뀌지 않는 것은 당연하다. JSX 코드를 리액트가 읽는 순간 그 안에 있는 컴포넌트 함수를 호출한다. 더이상 호출해야 하는 함수가 없을 때까지 리액트는 JSX 코드를 읽어가며 함수를 모두 호출할 것이다. 함수가 모두 호출된 후에는 해당 코드를 브라우저의 DOM instruction으로 번역하여 브라우저로 넘겨준다. 

리액트는 위와 같이 작동하며, 여기에서 유의할 점은 JSX 코드를 읽는 작업을 리액트가 반복하지 않는다는 것이다. 그렇다면 화면을 업데이트하고 싶을 때는 어떻게 해야할까? 

 

여기에서 state라는 리액트에서 사용하는 개념이 등장한다.

 

State 사용하기

특정 데이터가 바뀔 때 리액트에게 해당 부분을 다시 읽어오도록 알려줄 수 있는 속성이 바로 state이다. 이를 사용하려면 react 라이브러리에서 useState라는 요소를 import해야 한다.

 

use로 시작하는 요소들은 React Hook이다. react hook은 반드시 리액트 컴포넌트 함수 내에서만 사용이 되어야 한다. 컴포넌트 함수 내에 다른 함수로 중첩되어서는 안되며, 반드시 컴포넌트 함수 바로 안에서만 위치해야 한다. (예외사항이 있기는 하나 그 부분은 추후에 다루겠다)

 

useState함수는 초기값을 인자로 넘겨줄 수 있으며, state 변수와 그 변수를 수정할 수 있는 함수 2개를 배열로 리턴한다. 따라서 useState의 기본적인 사용 방식은 다음과 같다.

const [value, setValue] = useState(data);

 

state에 값을 할당할 때는 '=' 연산자를 이용하는 것이 아니라, setState 함수를 사용하여 값을 할당한 뒤 리액트에게 해당 값이 수정되었으니 해당 컴포넌트를 재실행하라고 알려준다. 즉, 업데이트가 가능하다는 것이다!

 

useState 뜯어보기

state가 컴포넌트 내에 정의되어 있고 해당 컴포넌트가 여러 번 호출된다면, 각각의 state는 component별로 고유하다. 어떤 컴포넌트의 state 값을 바꾼다고 다른 요소가 영향을 받지 않는다는 것이다. 

 

자 그러면 왜 계속하여 바뀌는 값인 state를 const형식으로 할당하였을까? state를 업데이트할 때 '=' 연산자를 사용하지 않고 set___ 함수를 사용하여 값을 업데이트하기 때문이다. useState로 전달된 초기값은 말 그대로 가장 처음 state를 초기화할 때만 사용되며 그 이후는 계속하여 업데이트된 최신 값만 가져온다. 

 

state와 이벤트를 함께 잘 사용한다면 얼마든지 UI를 업데이트할 수 있을 것이다.

 

Form Input 추가하기

유저로부터 title, amount, date를 받아올 수 있는 form을 추가하였다. 아직은 실제로 받아오는 데이터를 처리하지 못한다. 새로운 expense data기 때문에 NewExpense라는 폴더를 새로 만들어 추가하였다. 

import ExpenseForm from "./ExpenseForm";
import "./NewExpense.css";

const NewExpense = () => {
  return (
    <div className="new-expense">
      <ExpenseForm />
    </div>
  );
};

export default NewExpense;
import "./ExpenseForm.css";

const ExpenseForm = () => {
  return (
    <form>
      <div className="new-expense__controls">
        <div className="new-expense__control">
          <label>Title</label>
          <input type="text" />
        </div>
        <div className="new-expense__control">
          <label>Amount</label>
          <input type="number" min="0.01" step="0.01" />
        </div>
        <div className="new-expense__control">
          <label>Date</label>
          <input type="date" min="2019-01-01" max="2022-12-31" />
        </div>
      </div>
      <div className="new-expense__actions">
        <button type="submit">Add Expense</button>
      </div>
    </form>
  );
};

export default ExpenseForm;

 

위 코드를 App.js에 반영하였다.

import Expenses from "./components/Expenses/Expenses";
import NewExpense from "./components/NewExpense/NewExpense";

const App = () => {
  const expenses = [
    ... // 생략
  ];
  return (
    <div>
      <NewExpense />
      <Expenses data={expenses} />
    </div>
  );
};

export default App;

 

Listening to user input 

input에 들어오는 모든 입력을 처리하기 위해 onInput을 사용해도 괜찮으나, 어떤 input 타입에도 사용할 수 있도록 onChange prop을 사용하여 처리해보자. 

<input type="text" onChange={titleChangeHandler} />

 

위와 같이 onChange 함수가 실행될 때마다 titleChangeHandler 함수가 실행될 것이다. 그러면 유저가 입력한 input 값은 어떻게 받아올까? 이 부분은 일반적인 JS 방식과 동일하다. addEventListener 함수로 2개의 인자를 넘겨줄 때, 이벤트 명과 그 이벤트가 호출될 때마다 실행할 함수를 넘겨줬다. 두번째 함수는 자동으로 event 객체를 받아올 수 있었다! onChange로 전달하는 함수 역시 자동으로 event 객체를 받아올 수 있다.

const titleChangeHandler = (event) => {
	console.log(event.target.value);
}

event를 console.log 해보면 target 객체 안에 입력한 값들이 value 속성에 들어가고 있는 것을 볼 수 있다. 이제 유저가 입력한 값을 받아올 수 있게 되었다!

 

여러 개의 state 관리하기

이제 input값 별로 입력한 요소를 저장할 수 있도록 코드를 수정하자. 각 input별로 하나의 state를 갖도록 했다. 

import { useState } from "react";
import "./ExpenseForm.css";

const ExpenseForm = () => {
  const [enteredTitle, setEnteredTitle] = useState("");
  const [enteredAmount, setEnteredAmount] = useState("");
  const [enteredDate, setEnteredDate] = useState("");

  const titleChangeHandler = (event) => {
    setEnteredTitle(event.target.value);
  };
  const amountChangeHandler = (event) => {
    setEnteredAmount(event.target.value);
  };
  const dateChangeHandler = (event) => {
    setEnteredDate(event.target.value);
  };

  return (
    <form>
      <div className="new-expense__controls">
        <div className="new-expense__control">
          <label>Title</label>
          <input type="text" onChange={titleChangeHandler} />
        </div>
        <div className="new-expense__control">
          <label>Amount</label>
          <input
            type="number"
            min="0.01"
            step="0.01"
            onChange={amountChangeHandler}
          />
        </div>
        <div className="new-expense__control">
          <label>Date</label>
          <input
            type="date"
            min="2019-01-01"
            max="2022-12-31"
            onChange={dateChangeHandler}
          />
        </div>
      </div>
      <div className="new-expense__actions">
        <button type="submit">Add Expense</button>
      </div>
    </form>
  );
};

export default ExpenseForm;

* input값으로 받아오는 요소는 number이든 date이든 string으로 넘어온다. input값을 저장해줄 state를 초기화해줄 때는 빈 문자열로 초기화해줘도 문제가 없다.

 

하나의 state로 모든 input 데이터를 저장하기

title, amount, date를 모두 묶어서 object 형태로 저장하는 방법도 가능하다.

const ExpenseForm = () => {
  const [userInput, setUserInput] = useState({
    enteredTitle: "",
    enteredAmount: "",
    enteredDate: "",
  });

  const titleChangeHandler = (event) => {
    setUserInput({
        ...userInput,
        enteredTitle: event.target.value
    });
  };
  const amountChangeHandler = (event) => {
    setUserInput({
      ...userInput,
      enteredAmount: event.target.value,
    });
  };
  const dateChangeHandler = (event) => {
    setUserInput({
      ...userInput,
      enteredDate: event.target.value,
    });
  };

  return (
    ... // 생략
  );
};

export default ExpenseForm;

각각 개별의 state로 관리해도, 묶어서 하나의 object로 관리해도 아무런 문제가 없다. 개인의 취향이다! 하지만 현재 이 코드는 불완전하다. 왜일까?

 

Previous State에 기반하여 state 업데이트하기

위의 코드는 100% 안전하게 업데이트한다고 보장하지 못한다. 왜냐하면, 리액트는 state update를 스케쥴해두기 때문이다. set함수를 만나자마자 바로 실행하는 것이 아니다. 따라서 이론적으로 동시에 수많은 state를 업데이트하도록 스케쥴해두었다면 잘못된 state를 가져올 수도 있는 것이다. 따라서 가장 최신의 previous state값을 가져오도록 보장하는 방식은 다음과 같다. 

setUserInput((prevState) => {
	return { ...prevState, enteredTitle: event.target.value };
});

set함수로 익명의 arrow fn을 넘겨주면 그 인자로 가장 최신의 state값을 가져오는 것이 보장된다. 따라서 예전 state값에 기반하여 state를 업데이트해야하는 경우, 위와 같은 방식을 사용해야 한다. 

 

다음 단계로 넘어가기 전에, 일단은 여러 개의 state를 사용했던 방식으로 코드를 되돌리고 다시 진행하자.

 

Form Submission 처리하기

button type="submit" 요소에 onClick 이벤트를 주는 것 대신, submit 타입의 버튼이 form 내에서 클릭된 경우 form 태그에 onSumbit 이벤트를 처리하는 것이 조금 더 바람직하다. 

 

submit 이벤트가 실행되는 즉시 페이지가 새로고침되는 것을 볼 수 있다. JS 방식에서 사용했던 것과 동일하게 event의 기본적인 동작을 일어나지 않도록 하는 event.preventDefault() 코드를 추가하자. 그리고 유저의 input값을 받아오는 state 값들을 하나로 묶어주었다.

// .. 생략
const sumbitHandler = (event) => {
    event.preventDefault();

    const expenseData = {
      title: enteredTitle,
      amount: enteredAmount,
      date: new Date(enteredDate),
    };

    console.log(expenseData);
  };
  return (
    <form onSubmit={sumbitHandler}>
 // .. 생략

 

* 여기서 의문인 부분이 "submit되는 순간에 각 state들이 가장 최신의 값이라고 확신할 수 있는가?" 이다. 이 부분은 조금 더 고민해보자.

 

다만 form을 제출한 뒤에 아직 데이터들이 input에 그대로 남아있는 것을 확인할 수 있다. 이 또한 해결하러 가보자.

 

Two-way binding

input 값의 변화만 받아오는 것이 아니라, 새로운 value 값을 input에 넣어줄 수도 있을 것이다. (two-way binding)

submit된 순간에 state값을 모두 빈 문자열로 초기화해주고 input 태그의 속성 중 하나인 value 속성에 각각의 state값을 넣어준다면, submit되는 순간에 모든 input 태그가 초기화된 모습을 확인할 수 있다.

import { useState } from "react";
import "./ExpenseForm.css";

const ExpenseForm = () => {
  const [enteredTitle, setEnteredTitle] = useState("");
  const [enteredAmount, setEnteredAmount] = useState("");
  const [enteredDate, setEnteredDate] = useState("");

  const titleChangeHandler = (event) => {
    setEnteredTitle(event.target.value);
  };
  const amountChangeHandler = (event) => {
    setEnteredAmount(event.target.value);
  };
  const dateChangeHandler = (event) => {
    setEnteredDate(event.target.value);
  };

  const sumbitHandler = (event) => {
    event.preventDefault();

    const expenseData = {
      title: enteredTitle,
      amount: enteredAmount,
      date: new Date(enteredDate),
    };

    setEnteredAmount("");
    setEnteredDate("");
    setEnteredTitle("");
    console.log(expenseData);
  };
  return (
    <form onSubmit={sumbitHandler}>
      <div className="new-expense__controls">
        <div className="new-expense__control">
          <label>Title</label>
          <input
            type="text"
            value={enteredTitle}
            onChange={titleChangeHandler}
          />
        </div>
        <div className="new-expense__control">
          <label>Amount</label>
          <input
            type="number"
            min="0.01"
            step="0.01"
            value={enteredAmount}
            onChange={amountChangeHandler}
          />
        </div>
        <div className="new-expense__control">
          <label>Date</label>
          <input
            type="date"
            min="2019-01-01"
            max="2022-12-31"
            value={enteredDate}
            onChange={dateChangeHandler}
          />
        </div>
      </div>
      <div className="new-expense__actions">
        <button type="submit">Add Expense</button>
      </div>
    </form>
  );
};

export default ExpenseForm;

 

Child component -> Parent component 통신하기 (Bottom-up)

props를 사용하여 parent->child로 값을 전달하는 것은 해봤다. 그렇다면 반대는 어떻게 하면 될까?

 

onChange 함수를 사용하여 값을 호출했듯, 우리도 우리의 component가 custom 함수를 갖도록 설정할 수 있을 것이다. 부모 component에서 함수를 생성하여 props를 통해 넘겨주고, 자식 component가 props.함수명으로 해당 함수를 실행한다면 함수의 호출이 다른 component에서 일어나기 때문에 얼마든지 값을 넘겨줄 수 있을 것이다. 

 

ExpenseForm -> NewExpense로 값을 넘겨주자. (child->parent)

먼저 NewExpense에서 ExpenseForm 컴포넌트에 props로 함수를 넘겨주는 코드를 작성하였다.

import ExpenseForm from "./ExpenseForm";
import "./NewExpense.css";

const NewExpense = () => {
  const saveExpenseDataHandler = (enteredExpenseData) => {
    const expenseData = {
      ...enteredExpenseData,
      id: Math.random().toString(),
    };
    console.log(expenseData);
  };
  return (
    <div className="new-expense">
      <ExpenseForm onSaveExpenseData={saveExpenseDataHandler} />
    </div>
  );
};

export default NewExpense;

props의 명은 함수임을 한눈에 알 수 있게 on~으로 시작하였다.

 

이제 자식인 ExpenseForm 컴포넌트 내에서 부모로 값을 넘겨줘야 되는 순간(submit)에 props의 onSaveExpenseData 함수를 호출하여 넘겨줄 값을 인자로 넣어주면 되겠다!

 

import { useState } from "react";
import "./ExpenseForm.css";

const ExpenseForm = (props) => {
  // ... 생략

  const sumbitHandler = (event) => {
    event.preventDefault();

    const expenseData = {
      title: enteredTitle,
      amount: enteredAmount,
      date: new Date(enteredDate),
    };

    setEnteredAmount("");
    setEnteredDate("");
    setEnteredTitle("");

    props.onSaveExpenseData(expenseData);
  };
  return (
    // ... 생략
};

export default ExpenseForm;

 

submit을 해보면 부모에서 함수가 실행되어, 값이 잘 넘어온 것을 확인할 수 있다.

console.log가 실행된 위치가 부모 컴포넌트

 

똑같은 방식으로 NewExpense -> App 컴포넌트로 값을 전달하자. 이제 App 컴포넌트 내에 하드코딩된 expenses 배열에 전달받은 expenseData를 추가해주기만 하면 된다. 일단은 전달까지만 해보자.

 

// ... 생략
  const addExpenseHandler = (expense) => {
    console.log(expense);
  };
  return (
    <div>
      <NewExpense onAddExpense={addExpenseHandler} />
      <Expenses data={expenses} />
    </div>
  );
 };

export default App;
import ExpenseForm from "./ExpenseForm";
import "./NewExpense.css";

const NewExpense = (props) => {
  const saveExpenseDataHandler = (enteredExpenseData) => {
    const expenseData = {
      ...enteredExpenseData,
      id: Math.random().toString(),
    };
    props.onAddExpense(expenseData);
  };
  return (
    <div className="new-expense">
      <ExpenseForm onSaveExpenseData={saveExpenseDataHandler} />
    </div>
  );
};

export default NewExpense;

submit 버튼을 눌러보면 값이 App 컴포넌트까지 잘 넘어온 것을 확인할 수 있다 :)

App.js에서 실행된 console.log

부모 컴포넌트 <-> 자식 컴포넌트 통신은 매우매우매우 중요한 부분이다!!! 잊지말자! props를 통해 값을 넘겨주거나, props를 통해 함수의 포인터를 넘겨주어 통신을 할 수 있다. 

 

Lifting State up

현재 component 구조를 간단하게 표현해보면 다음과 같다.

component tree

데이터가 생성되는 컴포넌트와 데이터가 실제로 사용되는 컴포넌트가 다르다. 두 컴포넌트는 직접적인 연결이 되어있지 않으므로, 가장 최소한으로 연결된 부모를 통해 데이터를 주고 받을 것이다. 

 

filter 기능 추가하기

유저가 선택한 연도의 데이터만 보여줄 수 있도록 필터 기능을 추가해보려 한다. 아직 UI 상으로 보여지는 것까지는 구현하지 않고, 실제로 선택된 데이터만 컴포넌트를 통해 넘어오는 것을 연습하자.

 

Expenses 컴포넌트에 ExpensesFilter 컴포넌트를 추가했다. 해당 컴포넌트는 단순히 유저가 드롭다운방식을 통해 원하는 연도를 select 할 수 있게 했다.

import { useState } from "react";
import Card from "../UI/Card";
import ExpenseItem from "./ExpenseItem";
import "./Expenses.css";
import ExpensesFilter from "./ExpensesFilter";

const Expenses = (props) => {
  const [filteredYear, setFilteredYear] = useState("2022");
  const enteredYearHandler = (enteredYear) => {
    setFilteredYear(enteredYear);
  };
  return (
    <div>
      <Card className="expenses">
        <ExpensesFilter
          selected={filteredYear}
          onEnteredYear={enteredYearHandler}
        />
        {props.data.map((data) => (
          <ExpenseItem
            title={data.title}
            amount={data.amount}
            date={data.date}
          />
        ))}
      </Card>
    </div>
  );
};

export default Expenses;
import "./ExpensesFilter.css";

const ExpensesFilter = (props) => {
  const selectChangeHandler = (event) => {
    props.onEnteredYear(event.target.value);
  };
  return (
    <div className="expenses-filter">
      <div className="expenses-filter__control">
        <label>Filter by year</label>
        <select value={props.selected} onChange={selectChangeHandler}>
          <option value="2022">2022</option>
          <option value="2021">2021</option>
          <option value="2020">2020</option>
          <option value="2019">2019</option>
        </select>
      </div>
    </div>
  );
};

export default ExpensesFilter;

 

Expenses 컴포넌트가 filter된 year에 의해 바뀌어야 하므로 해당 컴포넌트에 선택된 year값을 저장할 수 있는 state를 만들어주었다. 그리고 초기값을 ExpensesFilter로 props를 통해 넘겨 select 태그에 반영되도록 하였다. 

 

Stateful vs Stateless component

state를 관리하는 컴포넌트를 Stateful 컴포넌트라 칭한다. 몇개의 컴포넌트만 state를 관리할 것이고 나머지 Stateless 컴포넌트로 그 값들을 넘겨줄 것이다. 

 

 

여기까지 정리한 부분을 깃허브에 업로드해두었다.

https://github.com/cheonyeji/react_recap_ch3/tree/chapter2

 

GitHub - cheonyeji/react_recap_ch3

Contribute to cheonyeji/react_recap_ch3 development by creating an account on GitHub.

github.com

 

React Basics 게시물에서 이어지는 내용이다.

 

실제로 미니 프로젝트를 하나 만들어보자!

App.js, index.js, index.css가 들어있는 src 폴더 내에 components라는 이름으로 모든 컴포넌트를 저장할 폴더를 만들었다. App.js는 가장 최상단에 위치한 요소(root)이며, 해당 App.js 안에 여러 컴포넌트들이 중첩될 것이니까 폴더 depth를 달리 주었다.

 

리액트에서는 각 컴포넌트의 파일명을 대문자로 시작하며, camelcase 방식을 따르는 것이 보편적이라고 한다. 따라하자! 컴포넌트가 무엇인지 App.js를 통해 살펴보니, 별 거 없다. 하나의 JS 함수다! 그리고 각 컴포넌트는 일반 html 태그와 구분하기 위해 무조건 대문자로 시작해야 한다. 

function ExpenseItem() {
  return <h2>Expense item!</h2>;
}

export default ExpenseItem;

이 요소를 루트 컴포넌트, App.js에 추가한다.

import ExpenseItem from "./components/ExpenseItem";

function App() {
  return (
    <div>
      <h2>Let's get started!</h2>
      <ExpenseItem />
    </div>
  );
}

export default App;

 

리액트에서 주의해야할 점은 리턴하는 요소가 하나의 root 요소여야 한다는 것이다. 나란히 나란히 div 태그를 붙여서 리턴하면 vscode가 엄청나게 눈치를 줄 것이다. ExpenseItem 컴포넌트에 기본적으로 들어가야 될 요소들을 넣어보자.

function ExpenseItem() {
  return (
    <div>
      <div>March 7th 2022</div>
      <div>
        <h2>Birthday Cake</h2>
        <div>$10.7</div>
      </div>
    </div>
  );
}

export default ExpenseItem;

위 코드를 저장하면 아주,,, 못생긴 화면이 반겨준다. 자, Styling 하러 가보자!

 

React Styling

리액트에서 CSS 방식은 크게 다르지 않다. 그냥 CSS 파일 만들어서 필요한 내용들 작성해주면 된다. 그리고 css 파일을 import해주면 끝이다! 일반적인 JS에서 CSS를 적용해줄 때 class 속성을 부여했지만, JSX 문법에서는 className이라는 속성을 사용하여 클래스 명을 부여한다. 

 

import "./ExpenseItem.css";

function ExpenseItem() {
  return (
    <div className="expense-item">
      <div>March 7th 2022</div>
      <div className="expense-item__description">
        <h2>Birthday Cake</h2>
        <div className="expense-item__price">$10.7</div>
      </div>
    </div>
  );
}

export default ExpenseItem;

* css 작업은 일단 넘어가겠다..! 일단은 리액트 문법에 익숙해지는 걸 일순위로

 

Dynamic Data 표현하기

현재 방식은 ExpenseItem이라는 컴포넌트가 재사용성이 좋다고는 결코 말할 수 없다. 매번 새로운 아이템이 생길 때마다 하드코딩을 해야되는 상황이다. 그러면, dynamic하게 data가 들어오는 경우 어떻게 컴포넌트로 넘길 수 있을까? 어떻게하면 컴포넌트를 원 목적대로 재사용이 가능하도록 작성할 수 있을까? 

 

하드코딩된 데이터를 모두 변수에 넣어, 컴포넌트 함수는 단순히 전달받은 데이터를 띄우는 blueprint 역할을 하게 하면 될 것이다. 다음과 같이 말이다.

import "./ExpenseItem.css";

function ExpenseItem() {
  const expenseData = new Date(2022, 2, 7);
  const expenseTitle = "Birthday Cake";
  const expenseAmount = 10.7;

  return (
    <div className="expense-item">
      <div>{expenseData.toISOString()}</div>
      <div className="expense-item__description">
        <h2>{expenseTitle}</h2>
        <div className="expense-item__price">${expenseAmount}</div>
      </div>
    </div>
  );
}

export default ExpenseItem;

 

하지만 여전히, 화면에 띄우고자 하는 원본 데이터 자체가 컴포넌트 안에 들어있는 상태이다. 즉, 재사용성은 보장되지 않은 상태이다. 그러면 컴포넌트에 데이터를 전달하려면 어떻게 해야할까?

 

Props로 컴포넌트 간 데이터 전달하기

리액트에서 다른 컴포넌트에 저장된 데이터를 단순히 갖다 쓰는 것은 불가능하다. 따라서 우리는 컴포넌트에 속성값으로 데이터를 넘겨준 뒤, 해당 컴포넌트에서 전달받은 속성을 통해 그 데이터를 꺼내쓸 수 있도록 할 것이다. html 요소는 얼마든지 속성을 가질 수 있다. 리액트 컴포넌트 역시, props를 통해 값을 전달받을 수 있다!

 

컴포넌트는 하나의 객체(props)를 전달받는다. props 안에 속성으로 넘겨준 값들이 들어가 있다. 속성을 키로 받아오기 때문에, 컴포넌트로 넘긴 속성과 props를 통해 접근하는 키 값은 반드시 일치해야 한다.

 

이제 App.js에서 ExpenseItem 컴포넌트로 데이터를 전달하는 상황을 가정하자. 원래는 서버에서 값을 받아오는 식으로 진행되겠지만, 여기에서는 더미 데이터를 사용했다.

import ExpenseItem from "./components/ExpenseItem";

function App() {
  const expenses = [
    {
      id: "e1",
      title: "Toilet Paper",
      amount: 94.12,
      date: new Date(2020, 7, 14),
    },
    { id: "e2", title: "New TV", amount: 799.49, date: new Date(2021, 2, 12) },
    {
      id: "e3",
      title: "Car Insurance",
      amount: 294.67,
      date: new Date(2021, 2, 28),
    },
    {
      id: "e4",
      title: "New Desk (Wooden)",
      amount: 450,
      date: new Date(2021, 5, 12),
    },
  ];
  return (
    <div>
      <h2>Let's get started!</h2>
      <ExpenseItem
        title={expenses[0].title}
        amount={expenses[0].amount}
        date={expenses[0].date}
      />
      <ExpenseItem
        title={expenses[1].title}
        amount={expenses[1].amount}
        date={expenses[1].date}
      />
      <ExpenseItem
        title={expenses[2].title}
        amount={expenses[2].amount}
        date={expenses[2].date}
      />
      <ExpenseItem
        title={expenses[3].title}
        amount={expenses[3].amount}
        date={expenses[3].date}
      />
    </div>
  );
}

export default App;

* 물론 여기서 배열로 접근하는 부분은 최적화 가능한 부분이다. 일단은 넘어가자. 

 

이렇게 App 컴포넌트에서 ExpenseItem 컴포넌트로 값을 전달해줬음에도 화면에는 차이가 없다. 아직 ExpenseItem 컴포넌트는 전달받은 값으로 띄우고 있지 않기 때문에, props를 사용해서 전달받은 데이터를 렌더링하도록 고쳐보자.

 

import "./ExpenseItem.css";

function ExpenseItem(props) {
  return (
    <div className="expense-item">
      <div>{props.date.toISOString()}</div>
      <div className="expense-item__description">
        <h2>{props.title}</h2>
        <div className="expense-item__price">${props.amount}</div>
      </div>
    </div>
  );
}

export default ExpenseItem;

* 여기에서 destructoring 사용하여 props를 { date, title, amount} 꺼내줘도 된다. 그치만 일단은 props 개념을 익히기 위해 굳이굳이 키워드를 사용하여 객체에서 키값으로 접근해보자.

 

JS 로직을 적용해보자

현재 날짜가 아주 읽기가 어렵게 되어있다. Month - Year - Day의 포맷으로 바꿔보자. 이 부분은 JS에서 제공하는 기본 기능으로 충분히 해결이 가능하다. 다만 JSX 내에서 JS 로직을 직접 적용하지는 말고, JSX 밖에서 변수에 값을 넣어준 뒤 JSX 코드 자체는 깔끔하게 유지하는 방향으로 코딩하자.

 

import "./ExpenseItem.css";

function ExpenseItem(props) {
    const month = props.date.toLocaleString('en-US', {month: 'long'});
    const day = props.date.toLocaleString('en-US', {day : "2-digit"});
    const year = props.date.getFullYear();

  return (
    <div className="expense-item">
      <div>
        <div>{month}</div>
        <div>{year}</div>
        <div>{day}</div>
      </div>
      <div className="expense-item__description">
        <h2>{props.title}</h2>
        <div className="expense-item__price">${props.amount}</div>
      </div>
    </div>
  );
}

export default ExpenseItem;

 

슬슬 component를 쪼개볼까?

컴포넌트에 이것저것 기능을 추가하다 보면, 어쩔 수 없이 컴포넌트가 점점 무거워지는 모습을 볼 수 있다. 당연한 과정이다! 자, 이제 각각 하나의 기능만 수행하도록 컴포넌트 안의 요소를 다른 컴포넌트로 쪼개보자.

 

방금 다룬 date 정보만 해도 하나의 컴포넌트로 쪼개기 참 좋아보인다. 자 쪼개보자. 

쪼개는 것을 어려워할 필요는 없다. 안에 들어있던 로직, JSX 코드를 그대로 새 컴포넌트로 옮긴 뒤 원 코드 영역에 해당 컴포넌트만 적어주면 된다!

import "./ExpenseDate.css";

function ExpenseDate(props) {
  const month = props.date.toLocaleString("en-US", { month: "long" });
  const day = props.date.toLocaleString("en-US", { day: "2-digit" });
  const year = props.date.getFullYear();

  return (
    <div className="expense-date">
      <div className="expense-date__month">{month}</div>
      <div className="expense-date__year">{year}</div>
      <div className="expense-date__day">{day}</div>
    </div>
  );
}

export default ExpenseDate;

* CSS 파일은 생략

 

ExpenseItem 컴포넌트에서 ExpenseDate 컴포넌트를 사용할 때는 잊지말고 props로 date 정보를 넘겨줘야 된다. 이제 App -> ExpenseItem -> ExpenseDate 로 데이터가 총 3번 전달된 상황이다. 

 

import ExpenseDate from "./ExpenseDate";
import "./ExpenseItem.css";

function ExpenseItem(props) {
  return (
    <div className="expense-item">
      <ExpenseDate date={props.date} />
      <div className="expense-item__description">
        <h2>{props.title}</h2>
        <div className="expense-item__price">${props.amount}</div>
      </div>
    </div>
  );
}

export default ExpenseItem;

 

App.js 를 더 간결하게 만들어보자

App 컴포넌트 내에 ExpenseItem이 계속해서 생겨나는 것에 약간 부담이 있으니 코드를 쪼개보자! [0] [1] [2] [3] 으로 접근하기가 너무 싫어서 강의에 나와있는 코드랑은 약간 방향을 틀었다.

 

import Expenses from "./components/Expenses";

function App() {
  const expenses = [
    {
      id: "e1",
      title: "Toilet Paper",
      amount: 94.12,
      date: new Date(2020, 7, 14),
    },
    { id: "e2", title: "New TV", amount: 799.49, date: new Date(2021, 2, 12) },
    {
      id: "e3",
      title: "Car Insurance",
      amount: 294.67,
      date: new Date(2021, 2, 28),
    },
    {
      id: "e4",
      title: "New Desk (Wooden)",
      amount: 450,
      date: new Date(2021, 5, 12),
    },
  ];
  return (
    <div>
      <h2>Let's get started!</h2>
      <Expenses data={expenses} />
    </div>
  );
}

export default App;
import ExpenseItem from "./ExpenseItem";
import "./Expenses.css";

function Expenses(props) {
  return (
    <div className="expenses">
      {props.data.map((data) => (
        <ExpenseItem title={data.title} amount={data.amount} date={data.date} />
      ))}
    </div>
  );
}

export default Expenses;

 

Composition에 대하여 (children props)

현재 레이아웃 상으로 css가 겹치는 부분이 존재한다. 동글동글 테두리 부분이 겹친다. 이런 식으로 겹치는 부분을 단순히 감싸주기만 하는 wrapper component로 쪼개려면 사용해보지 않았던 개념이 등장한다. 

 

이 부분은 공식문서를 참고하면 더 좋을 듯 하다! 

https://ko.reactjs.org/docs/composition-vs-inheritance.html

 

합성 (Composition) vs 상속 (Inheritance) – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

Card라는 이름으로 각각의 요소를 감싸주는 Wrapper 컴포넌트를 만들어보자. 

function Card() {
  return <div className="card"></div>;
}

export default Card;
import Card from "./Card";
import ExpenseDate from "./ExpenseDate";
import "./ExpenseItem.css";

function ExpenseItem(props) {
  return (
    <Card className="expense-item">
      <ExpenseDate date={props.date} />
      <div className="expense-item__description">
        <h2>{props.title}</h2>
        <div className="expense-item__price">${props.amount}</div>
      </div>
    </Card>
  );
}

export default ExpenseItem;

위와 같이 겹치는 css를 포함하는 card 컴포넌트를 wrapper로 전달했더니 화면에 아무것도 표시가 되지 않는다. <> </> 사이의 요소를 전달받지 못해서 이런 문제가 발생했다. 따라서 <> 태그와 </> 태그 사이의 요소를 받아올 수 있도록 기본적으로 전달받은 props의 children 속성을 사용한다! (children은 예약어)

 

코드를 열심히 수정해서 card 컴포넌트 내부에 값을 제대로 넣어줬음에도 깨지는 에러가 발생한다.

function Card(props) {
  return <div className="card">{props.children}</div>;
}

export default Card;

레이아웃이 박살났어요

 

자 이 문제는 expense-item 클래스명이 card로 제대로 들어가지 않아 생긴 문제다. 즉, children 뿐만 아니라 원래 들어있던 클래스명도 같이 가져와야 한다는 것이다. className 이라는 속성을 통해 접근할 수 있다. (className 역시 예약어)

import "./Card.css";

function Card(props) {
  const classes = "card " + props.className;
  return <div className={classes}>{props.children}</div>;
}

export default Card;

 

이제 ExpenseItem과 Expenses로 가서 Card 컴포넌트로 감싸주면 원래 UI와 동일하게 구현된다. composition 개념 잘 잡고 가자!

import Card from "./Card";
import ExpenseDate from "./ExpenseDate";
import "./ExpenseItem.css";

function ExpenseItem(props) {
  return (
    <Card className="expense-item">
      <ExpenseDate date={props.date} />
      <div className="expense-item__description">
        <h2>{props.title}</h2>
        <div className="expense-item__price">${props.amount}</div>
      </div>
    </Card>
  );
}

export default ExpenseItem;
import Card from "./Card";
import ExpenseItem from "./ExpenseItem";
import "./Expenses.css";

function Expenses(props) {
  return (
    <Card className="expenses">
      {props.data.map((data) => (
        <ExpenseItem title={data.title} amount={data.amount} date={data.date} />
      ))}
    </Card>
  );
}

export default Expenses;

 

완성~

 

다만 여전히 데이터는 static하고 유저와 상호작용하는 부분이 없다. 더 나아가보자!

 


JSX 좀 더 뜯어보기

리액트 구 버전에서는 모든 컴포넌트 상단에 react 라이브러리를 import 해줬어야 했다. 그 이유를 살펴보자면 다음과 같다. JSX 코드를 사용하지 않는 방식으로 코드를 바꿔보자.

return React.createElement({'div', {}, 
	React.createElement('h2', {}, "Let's get started!"),
	React.createElement(Expenses, {data:expenses} )
);
  return (
    <div>
      <h2>Let's get started!</h2>
      <Expenses data={expenses} />
    </div>
  );

위와 아래는 같은 코드다. JSX 코드를 위와 같은 코드로 변환해주기 위해 과거에는 React를 직접 import 해줬어야 했다.

createElement에 전달되는 3개의 인자는 태그, 전달값, 태그안의 값들(여러개가 될 수 있음)이다. 따라서 항상 기억하자! JSX 코드가 제대로 작동되도록 하는 데에는 import React from 'react'가 생략되었으나 기본적으로 들어가 있기 때문에 가능한 것이다.

 

또한 위 코드를 보면 자연스럽게 왜 JSX가 최종적으로 리턴할 수 있는 태그가 한개인 이유를 알 수 있다. 

 

 

Component 파일들 정리하기

점점 컴포넌트가 늘어날수록 정리의 필요성은 커질 것이다. 각각의 기능과 관련된 컴포넌트를 그 기능별로 묶어주고, 전체적으로 통용되는 UI는 UI로 묶어주면 더욱 보기 수월할 것이다! 한번 실제로 정리해보자. 원래는 components 폴더 안에 함께 들어있던 코드들을 다음과 같이 정리해주었다.

Expenses 폴더 / UI 폴더로 나눠 정리

물론 정리 방식은 개인의 자유다 :) 그렇지만 하나의 큰 폴더 안에 모든 컴포넌트를 몽땅 넣어놓는 극악무도한 짓은 하지 말자.

 

Alternative Function Syntax

function 문법 대신 arrow fn을 사용해도 괜찮다. 정말로 개인의 선택이기 때문에 편한 방식을 택하면 된다~! 하지만 나는 arrow function으로 코드 전부 바꿔줄거다 ㅎㅎ

 

여기까지 작업한 파일은 깃헙에 업로드했다!

https://github.com/cheonyeji/react_recap_ch3

 

GitHub - cheonyeji/react_recap_ch3

Contribute to cheonyeji/react_recap_ch3 development by creating an account on GitHub.

github.com

 

유데미 강의를 듣고 복습차 정리해놓은 내용이다.

이번 기회에 리액트 강의 다 듣고 개념을 제대로 잡고 프로젝트로 들어가봐야겠다.

매번 절반까지만 듣고 말았지 뭐야 ... 

 

React => Component

가장 핵심 개념이다. 리액트는 재사용성을 고려하여 모든 요소를 각각의 component로 쪼갠 뒤 갖다 쓰는 방식으로 재사용성을 높였다. 불필요한 코드를 반복할 필요도 없으며, 각각의 기능을 쪼개 하나의 component가 각각 하나의 기능만을 수행하게 하여 관리하기 수월하게 만들었다.

 

component는 HTML + JS (+ CSS?)로 이루어진다. 

 

Create React App

쉽고 빠르게 리액트 프로젝트를 시작할 수 있는 툴이다. 그나저나 오랜만에 cra 썼더니 오래된 버전이라고 지우고 다시 설치하라고 떴다... 얌전히 npm uninstall -g create-react-app 해주고 npm install -g create-react-app 해줬다. 

 

React project 뜯어보기

리액트는 어쨌거나 JS 파일이다. 그러나 cra를 통해 생성된 index.js 파일을 뜯어보면 기존 js 문법에서는 허용되지 않는 부분들을 볼 수 있다. 가령 css 파일을 import 한다던지? <App /> 라던지

React는 브라우저로 전달해주기 전에 코드를 브라우저가 이해할 수 있게 잘~ 통역해서 보내준다. 

 

index.js 파일이 가장 먼저 실행되는 파일인데 한번 더 자세히 살펴보자. 

import ReactDOM from 'react-dom/client';

import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

react, react-dom 패키지를 통해 리액트 라이브러리가 실행된다. ReactDOM 패키지는 어떤 React dom object를 export를 해주고 그 요소를 index.js로 가져오고(import) 있다. 가져온 ReactDOM 객체의 createRoot 함수를 실행하는데 리액트로 개발하는 요소의 가장 root단에 존재하는 요소이다. 이 리액트 UI가 어디에 실행될지, index.html 파일 내에서 찾아볼 수 있다. 리액트는 single page application이므로, public 폴더 내의 index.html 파일 내에 <div id="root"> </div> 영역 내에 우리의 리액트 코드가 삽입될 것이다. 

해당 객체를 root라는 이름으로 저장한 뒤, 해당 div 내에 어떤 요소가 렌더링되어야할지 전달한다. 현재 코드 상으로는 < App /> 요소가 전달되고 있다. 이 App이라는 요소가 어디서 왔는지 살펴봤더니 App.js 파일에서 export된 요소로 보인다. 하지만 문법이 생소하다. 이 문법은 일반적인 JS 문법은 아니며, JSX라는 JS를 조금 더 확장한 문법이다.

 

App은 하나의 component이다. App.js 파일을 살펴보자.

function App() {
  return (
    <div>
      <h2>Let's get started!</h2>
    </div>
  );
}

export default App;

 

App.js는 하나의 함수이며 무언가를 리턴하고 있다. HTML 코드를 JS파일 내에서 리턴하는 방식은 일반적인 JS 에서 보지 못했던 JSX 문법이다. 

 

JSX란?

JSX란 JavaScript에 XML을 추가한 것으로, 브라우저로 전달되기 전에 브라우저가 이해하기 쉽게 한번 변환되어 전달된다. 실제로 우리의 react 프로젝트를 개발자도구에서 살펴보면 우리가 작성한 적 없는 코드들을 볼 수 있다. 

 

How React Works

기존 JS는 Id나 class이름이나 document에서 요소를 가져온 뒤 createElement로 요소를 추가하고, 추가된 요소를 append해주는 방식으로 html코드를 삽입했다. 이렇게 하나하나 어떻게 실행해야될지 명령하는 언어를 imperative(명령형) 언어라고 한다. 하지만 이 방식은 요소를 추가하거나 지울 때마다 꽤 수고롭다. 그에 비해 리액트는 일반 html코드를 적듯이 적어주기만 하면 된다. 리액트 컴포넌트는 custom html 요소나 마찬가지다.

 

'React > 개념' 카테고리의 다른 글

JS 복습하기  (0) 2022.07.20
React : create-react-app  (0) 2021.05.19
React의 메인 개념 : component  (0) 2021.05.19

리액트 프로젝트를 시작하는 가장 쉬운 방법은 create-react-app을 사용한 방법이리라 생각된다.

 

계속하여 최선 버전으로 업데이트되도록, npx 명령어를 사용하여 생성한다.

// npx create-react-app *myapp
npx create-react-app test

*myapp 자리에 원하는 프로젝트 명을 적고 실행하면, 입력한 프로젝트 명으로 폴더가 하나 만들어져 있다.

해당 폴더를 실행하면, localhost:3000 으로 가장 기본적인 react 페이지가 뜬다.

 

여기서 이제 src폴더 내에서 App.js index.js index.css 를 제외한 파일은 다 지우고 프로젝트를 시작했다. 당장은 필요없기도 하고, 많은 리액트 강의에서도 다 지우더라!

 

index.js 파일을 열어보면 다음과 같다. (버전별로 차이는 있지만 신경X)

import ReactDOM from 'react-dom';

import './index.css';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

 

리액트 자체는 js의 확장판이기 때문에, 기본적으로 js 문법을 따라간다. 다만 기본 js에서는 지원하지 않는 문법이 위 파일에서 2개 보인다.

 

import './index.css' <- 보통 js 파일에서는 위와 같이 import하지 않는다. 

<App /> <- js 코드 내의 HTML 코드이다. (JSX)

 

위와 같이 React가 도입한 문법은, npm run 명령어를 통해 실행되어, 개발자가 원하는 방향대로 화면에 뿌려진다. 원래대로라면 작동하지 않을 문법이 react로 인해, 브라우저로 전달되기 전 transform 되어 원하는 모습을 보여주는 것이다. 

 

그리고 또 하나 기억해야 할 것은, index.js 파일은 가장 먼저 실행되는 파일이라는 것이다. 

 

index.js가 최종적으로 실행하는 것은 다음이다.

ReactDOM.render(<App />, document.getElementById('root'));

App이라는 것은 위에서 App.js를 import했으므로, 이전에 소개했던 component이다. 

(third-party library나 js파일을 import할 때는 뒤의 확장자를 빼고 import한다)

 

document.getElementById('root')

리액트는 기본적으로 single-page-application이다. 즉, 하나의 페이지 안에서 전부 동작한다는 것이다. 유저에게 보여지는 파일은 여러개의 html로 이루어진 것 같지만, 실질적으로 리액트는 하나의 페이지만을 갖고 있다. 라우팅과 같은 부차적인 기능을 통해 다른 html로 이동하는 듯한 착각을 심어주는 것이다. 

따라서 root id를 가진 document는 어디 있느냐하면, public폴더 내의 index.html을 보면 찾을 수 있다.

(public 폴더는 잘 접근할 일은 없지만, index.html이 이곳에 있다)

 

따라서 우리는 App이라는 이름의 component를 root div 내에서 실행하는 것이다. 

(App.js는 다른 component와 달리, index.js에서 실행되는 root component이다. 추후 여러개의 component가 등장하더라도, App.js 내에서 nested 될 것이다)

 

우선 가장 기본적인 문장을 띄우도록 App.js를 작성해보았다.

function App() {
	return (
    	<div>
        	<h2>Hello, world!<h2>
        </div>
    );
}

export default App;

js 내에서 html 코드를 return하는 형식은 처음 보면 굉장히 낯선 광경이다. 이 문법은 react에 도입된 문법으로, JSX라 불린다. 이는 react가 npm start로 실행되면서 브라우저에 전달될 때는 transform하여 전될되는데, 구체적으로 어떻게 변하는지 보고싶다면 해당 리액트 프로젝트를 시작한 후 개발자 도구의 source 파일을 보면 된다. 위에서 적은 js와는 사뭇 다른 코드가 보일 것인데, 이는 리액트가 JSX를 transform하여 브라우저에 전달한 결과이다. 

 

기본적인 js와 달라 혼란스러울 수 있지만, 개발자에게 더 편한 환경을 제공해주는 것이므로 금방 익숙해지는 것이 좋다!

 


기본적인 js 문법과의 차이를 한번은 짚고 넘어가는 것이 좋을 것 같아 추가한다.

function App() {
	return(
		<div>
    		<h2>Hello world!</h2>
        	<p>my cat is rockstar</p>
    	</div>
    );
}

위의 App.js 파일에서 p 태그 하나를 추가하고 싶으면 기본 리액트 문법은 위와 같이 작성하면 된다. 하지만 기본 js 문법에 따르면 다음과 같이 작성해야한다. (imperative approach로, js에게 명료하게 하나하나 어떻게 해야할지 전달)

function App() {
	const paragraph = document.createElement('p');
    paragraph.textContent = 'my cat is rockstar';
    document.getElementById('root').append(paragraph);
	return(
		<div>
    		<h2>Hello world!</h2>
    	</div>
    );
}

 

단순히 코드만 봐도, 리액트가 좀 더 짧고 간결하다. 

'React > 개념' 카테고리의 다른 글

React Basics  (0) 2022.07.20
JS 복습하기  (0) 2022.07.20
React의 메인 개념 : component  (0) 2021.05.19

서론

React 자체를 너무 어렵게 접근할 필요는 없다. 리액트는 UI를 보다 쉽게 개발할 수 있도록 도와주는 자바스크립트 라이브러리다. 물론 Html, css, js로도 UI를 만들 수 있으나, 리액트를 사용하면 component라는 개념 덕분에 코드가 훨씬 깔끔하고 구현 난이도가 내려간다. 

 

Component란 무엇인가?

쉽게 말하자면, UI 상에서 다시 사용할 수 있는 building block이다. 단순히 표시해줘야하는 데이터만 바뀌고 UI는 그대로라면, 굳이 여러번 동일한 코드를 반복해서 작성할 필요 없이, 하나의 block을 만들고, 그 안의 데이터만 넘겨주는 방식이다.

Component 자체는 쉽게 말하자면, Html+js (+ css) 덩어리다. (css를 괄호처리한 이유는 리액트 상에서는 크게 중요한 개념이 아니라서다.) 각각의 UI 상에서 모든 걸 쪼개서 component로 구성할 수도 있다. 따라서 개발자는 UI를 구성하는 모든 요소를 쪼개서 component로 만든 뒤, 최종적으로 어떻게 UI를 그릴지 React에게 구성도를 던져주기만 하면 된다. (재사용성에 너무 집착할 필요는 없다.) 

 

Component 단위의 구성은 개발자에게 Reusability & Separation of Concerns를 제공한다.

불필요한 코드의 반복도 없고, 한 곳에서 지나치게 많은 요소를 관리할 필요도 없다. 각각의 component는 자신이 수행해야 하는 하나의 목표만 집중하면 되는 것이다. 

'React > 개념' 카테고리의 다른 글

React Basics  (0) 2022.07.20
JS 복습하기  (0) 2022.07.20
React : create-react-app  (0) 2021.05.19

로그인, 로그아웃 여부에 따라 Header 조건부 렌더링하기 (1)

- component가 모두 접근할 수 있도록 context를 사용한다. 

 

context.js에서 createContext import 해준 뒤 다음과 같이 기본적으로 context와 provider 작성을 해준다.

 

// UserContext : Application 내의 데이터 저장소
const UserContext = React.createContext();

const UserContextProvider = ({children}) => {
	<UserContext.Provider>{children}</UserContext.Provider>
}

export default UserContextProvider;

 

여기에서 추가적으로 값을 전달해야할 것이며, state도 갖고 있어야 한다. 

+ Recent posts