Dynamic하게 style 반영하기

inline으로 css를 반영하는 것은 기존 css 요소를 모두 덮어쓸 수 있을 뿐만 아니라, 코드의 중복이 일어날 확률이 높으므로 지양하는 것이 좋다. 즉 className에 접근하여 상태에 따라 'invalid' 라는 클래스명을 추가/제거해주면서 동적으로 css를 추가하는 것이 바람직할 것이다.

 

form-control 클래스명에 invalid가 추가되는 경우, 유저가 아무것도 입력하지 않았을 때 경고 처리를 해주기 위해 다음과 같이 CSS를 작성했다.

.form-control.invalid input {
  border-color: red;
  background: #ffd7d7;
}

.form-control.invalid label {
  color: red;
}

 

유저가 제대로 된 값을 입력하기 시작한 경우 다시 isValid state를 true로, 제출 버튼 클릭 시 아무것도 입력하지 않은 경우 isValid state를 false로 바꿔주는 로직을 추가했다. 또한 form-control 클래스명을 `` (백틱) 으로 전달하여 ${} 문법을 통해 내부에 JS 코드를 작성할 수 있도록 했다.

 

import React, { useState } from "react";

import Button from "../../UI/Button/Button";
import "./CourseInput.css";

const CourseInput = (props) => {
  const [enteredValue, setEnteredValue] = useState("");
  const [isValid, setIsValid] = useState(true);

  const goalInputChangeHandler = (event) => {
    if (event.target.value.trim().length !== 0) {
      setIsValid(true);
    }
    setEnteredValue(event.target.value);
  };

  const formSubmitHandler = (event) => {
    event.preventDefault();
    if (event.target.value.trim().length === 0) {
      setIsValid(false);
      return;
    }
    props.onAddGoal(enteredValue);
  };

  return (
    <form onSubmit={formSubmitHandler}>
      <div className={`form-control ${!isValid ? 'invalid' : ''}`}>
        <label>Course Goal</label>
        <input type="text" onChange={goalInputChangeHandler} />
      </div>
      <Button type="submit">Add Goal</Button>
    </form>
  );
};

export default CourseInput;

 

유저가 아무것도 입력하지 않거나, 스페이스바만 누른 채로 버튼을 클릭했을 때

 

Styled Components 사용하기

한 컴포넌트에만 적용할 수 있는, scoped styling을 적용하기 위해 사용해보자. css파일을 import해오는 방식은 얼마든지 다른 요소에 영향을 미칠 수 있으니까 말이다. 수많은 개발자들이 함께 작업하는 파일에서 실수로 클래스명이 겹치는 악몽은 직접 겪지 말자.

 

https://styled-components.com/

 

styled-components

Visual primitives for the component age. Use the best bits of ES6 and CSS to style your apps without stress 💅🏾

styled-components.com

 

위 페이지에서 보다 자세한 정보를 볼 수 있다. 일단은 우리의 프로젝트에 설치하자.

npm install --save styled-components

위 명령어를 통해 설치할 수 있다. 

 

기존에 버튼 컴포넌트를 관리하던 Button.js Button.css파일을 styled-components를 사용하여 하나의 파일로 합쳐보자.

기존 파일은 다음과 같다.

 

import React from 'react';

import './Button.css';

const Button = props => {
  return (
    <button type={props.type} className="button" onClick={props.onClick}>
      {props.children}
    </button>
  );
};

export default Button;
.button {
  font: inherit;
  padding: 0.5rem 1.5rem;
  border: 1px solid #8b005d;
  color: white;
  background: #8b005d;
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.26);
  cursor: pointer;
}

.button:focus {
  outline: none;
}

.button:hover,
.button:active {
  background: #ac0e77;
  border-color: #ac0e77;
  box-shadow: 0 0 8px rgba(0, 0, 0, 0.26);
}

 

styled component는 styled.___ 문법으로 사용 가능하다. styled. 뒤에 올 수 있는 것들은 전부 HTML 태그들이다. styled component는 JSX element를 반환하며 해당 요소 안에 css를 적용하는 방법은 ``을 사용하여 그 안에 코드를 넣어주면 된다. 하지만 css selector가 필요 없으므로 생략하거나 추가로 작성하고자 하는 경우 & 연산자를 사용한다. 적용된 모습을 보면 다음과 같다.

import styled from "styled-components";

const Button = styled.button`
  font: inherit;
  padding: 0.5rem 1.5rem;
  border: 1px solid #8b005d;
  color: white;
  background: #8b005d;
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.26);
  cursor: pointer;

  &:focus {
    outline: none;
  }

  &:hover,
  &:active {
    background: #ac0e77;
    border-color: #ac0e77;
    box-shadow: 0 0 8px rgba(0, 0, 0, 0.26);
  }
`;

export default Button;

 

코드를 바꾸더라도 아무런 문제 없이 프로젝트는 잘 작동한다. 그리고 잊지말자! styled가 반환하는 객체는 JSX element이므로 변수는 대문자로 시작해야 한다.

 

Styled components & Dynamic props

앞서 form-control 부분 역시 styled-components를 적용해보자. 기존 코드는 다음과 같다.

.form-control {
  margin: 0.5rem 0;
}

.form-control label {
  font-weight: bold;
  display: block;
  margin-bottom: 0.5rem;
}

.form-control input {
  display: block;
  width: 100%;
  border: 1px solid #ccc;
  font: inherit;
  line-height: 1.5rem;
  padding: 0 0.25rem;
}

.form-control input:focus {
  outline: none;
  background: #fad0ec;
  border-color: #8b005d;
}

.form-control.invalid input {
  border-color: red;
  background: #ffd7d7;
}

.form-control.invalid label {
  color: red;
}
import React, { useState } from "react";

import Button from "../../UI/Button/Button";
import "./CourseInput.css";

const CourseInput = (props) => {
  const [enteredValue, setEnteredValue] = useState("");
  const [isValid, setIsValid] = useState(true);

  const goalInputChangeHandler = (event) => {
    if (event.target.value.trim().length !== 0) {
      setIsValid(true);
    }
    setEnteredValue(event.target.value);
  };

  const formSubmitHandler = (event) => {
    event.preventDefault();
    if (enteredValue.trim().length === 0) {
      setIsValid(false);
      return;
    }
    props.onAddGoal(enteredValue);
  };

  return (
    <form onSubmit={formSubmitHandler}>
      <div className={`form-control ${!isValid ? "invalid" : ""}`}>
        <label>Course Goal</label>
        <input type="text" onChange={goalInputChangeHandler} />
      </div>
      <Button type="submit">Add Goal</Button>
    </form>
  );
};

export default CourseInput;

 

styled-components를 적용하면 다음과 같다. nested된 요소 역시 & 연산자를 사용하면 된다.

 

import React, { useState } from "react";
import styled from "styled-components";

import Button from "../../UI/Button/Button";
import "./CourseInput.css";

const FormControl = styled.div`
  margin: 0.5rem 0;

  & label {
    font-weight: bold;
    display: block;
    margin-bottom: 0.5rem;
  }

  & input {
    display: block;
    width: 100%;
    border: 1px solid #ccc;
    font: inherit;
    line-height: 1.5rem;
    padding: 0 0.25rem;
  }

  & input:focus {
    outline: none;
    background: #fad0ec;
    border-color: #8b005d;
  }

  &.invalid input {
    border-color: red;
    background: #ffd7d7;
  }

  &.invalid label {
    color: red;
  }
`;

const CourseInput = (props) => {
  const [enteredValue, setEnteredValue] = useState("");
  const [isValid, setIsValid] = useState(true);

  const goalInputChangeHandler = (event) => {
    if (event.target.value.trim().length !== 0) {
      setIsValid(true);
    }
    setEnteredValue(event.target.value);
  };

  const formSubmitHandler = (event) => {
    event.preventDefault();
    if (enteredValue.trim().length === 0) {
      setIsValid(false);
      return;
    }
    props.onAddGoal(enteredValue);
  };

  return (
    <form onSubmit={formSubmitHandler}>
      <FormControl>
        <label>Course Goal</label>
        <input type="text" onChange={goalInputChangeHandler} />
      </FormControl>
      <Button type="submit">Add Goal</Button>
    </form>
  );
};

export default CourseInput;

 

자 그러나 className을 dynamic하게 넘겨주던 부분이 사라졌다. 이 부분도 다시 완성해보자. 물론 form-control이라는 클래스명은 더 이상 적어줄 필요가 없으니 다음과 같이 적어주기만 하면 된다. styled component로 만든 요소 역시 JSX element이므로 className 속성을 사용할 수 있다. 

<FormControl className={!isValid && 'invalid'}>

 

이 방식이 조금 더 클래식한 방식이라면, props를 사용하여 바꿔줄 수도 있다. 이 방식이 조금 더 css상으로는 깔끔하게 보일 것이다. 

<FormControl isvalid={!isValid}>

위와 같이 FormControl 요소에 isvalid(boolean 형) props를 넘겨줄 수도 있다! 그러면 이제 우리는 props.isvalid값에 따라 css를 다르게 작용해줄 수 있다. 왜냐하면 div 뒤에 css를 적어줄 때 `` 을 사용했기 때문에 ${}를 사용하여 얼마든지 js 구문을 쓸 수 있다.

 

import React, { useState } from "react";
import styled from "styled-components";

import Button from "../../UI/Button/Button";
import "./CourseInput.css";

const FormControl = styled.div`
  margin: 0.5rem 0;

  & label {
    font-weight: bold;
    display: block;
    margin-bottom: 0.5rem;
    color: ${(props) => (props.isvalid ? "red" : "black")};
  }

  & input {
    display: block;
    width: 100%;
    border: 1px solid ${(props) => (props.isvalid ? "red" : "#ccc")};
    background: ${(props) => (props.isvalid ? "#ffd7d7" : "transparent")};
    font: inherit;
    line-height: 1.5rem;
    padding: 0 0.25rem;
  }

  & input:focus {
    outline: none;
    background: #fad0ec;
    border-color: #8b005d;
  }
`;

const CourseInput = (props) => {
  const [enteredValue, setEnteredValue] = useState("");
  const [isValid, setIsValid] = useState(true);

  const goalInputChangeHandler = (event) => {
    if (event.target.value.trim().length !== 0) {
      setIsValid(true);
    }
    setEnteredValue(event.target.value);
  };

  const formSubmitHandler = (event) => {
    event.preventDefault();
    if (enteredValue.trim().length === 0) {
      setIsValid(false);
      return;
    }
    props.onAddGoal(enteredValue);
  };

  return (
    <form onSubmit={formSubmitHandler}>
      <FormControl isvalid={!isValid}>
        <label>Course Goal</label>
        <input type="text" onChange={goalInputChangeHandler} />
      </FormControl>
      <Button type="submit">Add Goal</Button>
    </form>
  );
};

export default CourseInput;

https://school.programmers.co.kr/learn/courses/30/lessons/42584

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

풀이

자기 뒤에서 자기보다 작은 가격이 등장한 최초의 순간만 찾으면 되는 간단한 문제라고 생각했는데......... .... 계속 시간초과가 나서 2시간정도 삽질하다가 해설을 봤다. deque 자료구조를 쓰거나 stack을 사용하면 되는 문제라고 한다. 

아래는 효율성 0점의 눈물나는 코드다.

# 단순 리스트 순회
def solution(prices):
    answer = []
    for i in range(len(prices)):
        cnt = 0
        for j in prices[i + 1 :]:
            if j < prices[i]:
                cnt += 1
                break
            else:
                cnt += 1
        answer.append(cnt)
    return answer

 

와 충격적... 알고리즘 오카방에 물어봤더니 알려줬다

for j in prices[i + 1 :]:

위 부분에서 매번 리스트를 새롭게 만들어주는 과정에서 시간초과가 난 것이다... 와 ... 그러고 보니 리스트 슬라이싱은 추출하여 새로운 리스트를 만들어주는 것이었다.... 그래서 이 부분 수정하면 효율성을 통과하기는 한다 대박사건

def solution(prices):
    answer = []
    for i in range(len(prices)):
        cnt = 0
        for j in range(i + 1, len(prices)):
            cnt += 1
            if prices[j] < prices[i]:
                break
        answer.append(cnt)
    return answer

리스트로 풀었을 때


deque 사용

list로 순회하는 방법에서 deque를 쓰는 방식으로 바꿨더니 바로 통과가 됬다. 분명 알고리즘은 똑같은데 이게 자료구조...?

# deque 사용
from collections import deque
def solution_deque(prices):
    queue = deque(prices)
    answer = []
    while queue:
        price = queue.popleft()
        cnt = 0
        for j in queue:
            cnt += 1
            if j < price:
                break
        answer.append(cnt)
    return answer

 

deque 라이브러리 사용 시

 

코드 실행 시간이 절반으로 줄었다... 더 효율적인 코드가 무엇일지 계속 고민하는 개발자가 되자

https://school.programmers.co.kr/learn/courses/30/lessons/42839

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

풀이

모든 경우의 수를 permutations를 사용하여 순열로 뽑아내었다. list로 나온 요소를 모두 join 메서드를 사용하여 이어붙인 뒤 숫자로 변환하였다. 문제에서 011과 11은 같은 숫자라고 명시했기 때문에 중복을 제거한 뒤 모든 경우를 소수 판별했다. 0과 1은 소수가 아니므로 예외처리를 해주고 그 외의 경우에는 1, 자기자신을 제외한 수로 모두 나눠보며 체크했다.

 

프로그래머스 내에서는 통과되었는데 분명 시간초과가 날 여지가 있다고 생각하여 소수판별 알고리즘을 한번 더 정리해보려 한다. 블로그에 이미 올려둔 내용이긴 하지만... 또 까먹었기 때문에 한번 더 정리

 

# 2022-07-22
# 프로그래머스 Lv2 - 소수 찾기
# https://school.programmers.co.kr/learn/courses/30/lessons/42839
# 소요시간 : 15:10 ~ 15:25 (15m)

from itertools import permutations

# 소수판별 알고리즘
def checkPrimeNum(num):
    if num == 0 or num == 1:
        return False

    for i in range(2, num):
        if num % i == 0:
            return False
    return True


def solution(numbers):
    answer = 0
    num_arr = []
    for num in numbers:
        num_arr.append(num)

    every_case = []
    for i in range(1, len(num_arr) + 1):
        case = list(permutations(num_arr, i))
        for arr in case:
            every_case.append(int("".join(arr)))

    every_case = list(set(every_case))

    for case in every_case:
        if checkPrimeNum(case):
            answer += 1

    return answer

 

소수 판별 알고리즘

단일 숫자 판별 시

약수의 성질을 살펴보면 모든 약수는 가운데 약수를 기준으로 곱셈 연산에 대해 대칭을 이룬다. 

16의 약수는 1, 2, 4, 8, 16 인데 2x8 = 16, 8x2=16이다.

따라서 특정한 자연수의 모든 약수를 찾을 때 가운데 약수(제곱근)까지만 확인해주면 된다. 

 

# 소수 판별 (개선된 알고리즘)
import math

def isPrimeNumber(num):
    # 2부터 num의 제곱근까지의 모든 수를 확인하기
    # 제곱근을 구하는 방법은 math.sqrt() 사용. 반환형은 float
    # 음수의 제곱근을 구하게 되면 error 발생하니 유의
    for i in range(2, int(math.sqrt(num)) + 1):
        if num % i == 0:
            return False
    return True

 

대량의 숫자 판별 시 (에라토스테네스의 체)

이 문제에서는 각각의 경우가 소수인지 판단해야 되서 아니지만, 만약 특정 범위까지의 자연수 중 소수가 몇개인지처럼 대량의 소수를 한꺼번에 판별하고자 할 때 사용하면 좋은 방법을 추가로 정리한다.

 

에라토스테네스의 체는 가장 먼저 소수를 판별할 범위만큼 배열을 할당, 그 인덱스에 해당하는 값을 넣어준다. 2부터 시작하여 특정 숫자의 배수에 해당하는 숫자를 모두 지운다. (자기 자신은 지우지 않음) 

 

n = int(input())

eratos = [True] * (n+1)
m = int(n**0.5) // math.sqrt 사용해도 ok

for i in range(2, m+1):
	if eratos[i] == True:
    	for j in range(i+i, n+1, i): #i의 배수를 모두 삭제, 본인은 제외
        	eratos[j] = False
            
print([i for i in range(2, n+1) if a[i] == True])

 

 

 

Dynamic하게 아이템을 띄우기

JSX 내에서 JS 코드를 실행하려면 { } 안에 적어주면 가능했다. 기존에 이미 작업한 코드를 한번 더 복습해보자!

{props.data.map((data) => (
          <ExpenseItem
            title={data.title}
            amount={data.amount}
            date={data.date}
          />
))}

 

Stateful List

새로운 아이템이 추가될 때마다 UI가 업데이트되도록 App.js에서 state를 추가해주자. 기존에 있던 데이터를 컴포넌트 밖으로 빼고 새로운 expense를 추가할 때마다 state를 업데이트하도록 수정하였다.

import { useState } from "react";
import Expenses from "./components/Expenses/Expenses";
import NewExpense from "./components/NewExpense/NewExpense";

const DUMMY_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),
  },
];

const App = () => {
  const [expenses, setExpenses] = useState(DUMMY_EXPENSES);

  const addExpenseHandler = (expense) => {
    setExpenses((prevExpenses) => {
      return [expense, ...prevExpenses];
    });
  };
  return (
    <div>
      <NewExpense onAddExpense={addExpenseHandler} />
      <Expenses data={expenses} />
    </div>
  );
};

export default App;

 

그나저나 계속 에러가 나고 있다. 이것부터 좀 해결해보자.

key warning이 계속 발생한다

 

Key란?

현재 코드를 개발자도구에서 살펴보면 새로운 아이템이 추가될 때마다 맨 아래에 실제 코드가 추가되는 것을 볼 수 있다. 하지만 실제로 새로운 아이템은 상단에 추가되고 있다. 리액트는 현재 리스트의 아이템들이 모두 비슷하게 생겼기 때문에 단지 array의 변경만 받아와 새로운 아이템을 추가한 뒤 모든 아이템을 훑어보며 array의 변경사항을 반영한다. 즉, 비효율적으로 작동중이다. 퍼포먼스 문제 뿐만 아니라 각 아이템 별로 state를 갖고 있는 경우 state끼리 잘못된 업데이트가 발생할 수도 있다. 

 

따라서 리액트에게 각각의 아이템을 구분할 수 있도록 key라는 prop을 주자. 마침 우리는 각각을 구분할 수 있는 고유한 값으로 id를 갖고 있다. 

        {props.data.map((data) => (
          <ExpenseItem
            key={data.id}
            title={data.title}
            amount={data.amount}
            date={data.date}
          />
        ))}

 

만약 리액트에서 리스트의 아이템을 mapping out할 때면, 꼭 key prop을 부여해주도록 하자. 

 

Filter 기능 구현하기

유저가 선택한 연도의 데이터만 보이게 하자. 어려울 것 없이, 기존에 가지고 있는 배열에서 조건에 일치하는 것들만 골라내면 된다! filter() 함수를 사용한다.

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);
  };
  const filteredExpenses = props.data.filter(
    (item) => item.date.getFullYear().toString() === filteredYear
  );
  return (
    <div>
      <Card className="expenses">
        <ExpensesFilter
          selected={filteredYear}
          onEnteredYear={enteredYearHandler}
        />
        {filteredExpenses.map((data) => (
          <ExpenseItem
            key={data.id}
            title={data.title}
            amount={data.amount}
            date={data.date}
          />
        ))}
      </Card>
    </div>
  );
};

export default Expenses;

 

Conditional Content

현재 구조는 아무런 데이터가 없으면 빈 공간만 출력되고 있다. 아무런 데이터가 없을 때는 "데이터가 없습니다"와 같이 다른 화면을 띄워주는 것이 보다 유저에게 친절한 화면일 것이다. 선택된 데이터의 길이에 따라 삼항연산자를 사용하여 조건부로 렌더링해보자.

{filteredExpenses.length === 0 ? (
          <p>No expenses found.</p>
        ) : (
          filteredExpenses.map((data) => (
            <ExpenseItem
              key={data.id}
              title={data.title}
              amount={data.amount}
              date={data.date}
            />
          ))
        )}

삼항연산자를 사용하면 코드가 약간 읽기 지저분해진다. 따라서 && 연산자를 사용해서 코드를 쪼개보자. 이정도 센스는 발휘해보자 :)

 

        {filteredExpenses.length === 0 && <p>No expenses found.</p>}
        {filteredExpenses.length > 0 &&
          filteredExpenses.map((data) => (
            <ExpenseItem
              key={data.id}
              title={data.title}
              amount={data.amount}
              date={data.date}
            />
          ))}

 

하지만 JSX에는 최대한 로직을 배제하고 간단하게 적어주는 것이 좋다고 하니, return문 밖으로 빼보자~!

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);
  };
  const filteredExpenses = props.data.filter(
    (item) => item.date.getFullYear().toString() === filteredYear
  );

  let expensesContent = <p>No expenses found.</p>;

  if (filteredExpenses.length > 0) {
    expensesContent = filteredExpenses.map((data) => (
      <ExpenseItem
        key={data.id}
        title={data.title}
        amount={data.amount}
        date={data.date}
      />
    ));
  }

  return (
    <div>
      <Card className="expenses">
        <ExpensesFilter
          selected={filteredYear}
          onEnteredYear={enteredYearHandler}
        />
        {expensesContent}
      </Card>
    </div>
  );
};

export default Expenses;

 

Conditional Return Statements

조건에 따라 내용을 설정할 수도 있지만, 조건에 따라 JSX를 다르게 리턴할 수도 있다. 다르게 리턴해주는 부분만을 Expenses 컴포넌트에서 쪼개자. ExpensesList라는 이름의 js, css 파일을 새롭게 만들어주었다.

 

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

const ExpensesList = (props) => {
  if (props.items.length === 0) {
    return <h2 className="expenses-list__fallback">Found no expenses.</h2>;
  }
  return (
    <ul className="expenses-list">
      {props.items.map((data) => (
        <ExpenseItem
          key={data.id}
          title={data.title}
          amount={data.amount}
          date={data.date}
        />
      ))}
    </ul>
  );
};

export default ExpensesList;

 

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

const Expenses = (props) => {
  const [filteredYear, setFilteredYear] = useState("2022");
  const enteredYearHandler = (enteredYear) => {
    setFilteredYear(enteredYear);
  };
  const filteredExpenses = props.data.filter(
    (item) => item.date.getFullYear().toString() === filteredYear
  );

  return (
    <div>
      <Card className="expenses">
        <ExpensesFilter
          selected={filteredYear}
          onEnteredYear={enteredYearHandler}
        />
        <ExpensesList items={filteredExpenses} />
      </Card>
    </div>
  );
};

export default Expenses;

 

보다 semantic한 코드를 위해 모든 아이템을 ul태그로 묶었고(ExpensesList.js), 각각의 아이템에게는 li태그를 추가했다. (ExpenseItem.js)

 

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

const ExpenseItem = (props) => {
  return (
    <li>
      <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>
    </li>
  );
};

export default ExpenseItem;

 

조건부 렌더링 추가 연습하기

Form을 활성화할 수 있는 버튼을 새롭게 만들어보자. 화면 업데이트가 필요하므로 state를 새롭게 선언해주었다. showForm이라는 이름의 state가 참이면 form을 보여주고, 거짓이면 버튼만을 보여주도록 설정하였다.

또한 form 내의 cancel 버튼으로 form을 끌 수 있도록 setShowForm 함수를 props로 넘겨주었다.

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

const NewExpense = (props) => {
  const [showForm, setShowForm] = useState(false);

  const saveExpenseDataHandler = (enteredExpenseData) => {
    const expenseData = {
      ...enteredExpenseData,
      id: Math.random().toString(),
    };
    props.onAddExpense(expenseData);
  };

  const btnClickHandler = () => {
    setShowForm((prevShowForm) => !prevShowForm);
  };

  return (
    <div className="new-expense">
      {!showForm && <button onClick={btnClickHandler}>Add New Expense</button>}
      {showForm && (
        <ExpenseForm
          onSetShowForm={setShowForm}
          onSaveExpenseData={saveExpenseDataHandler}
        />
      )}
    </div>
  );
};

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

const ExpenseForm = (props) => {
  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("");

    props.onSaveExpenseData(expenseData);
    props.onSetShowForm((prevShowForm) => !prevShowForm);
  };

  const cancelBtnClickHandler = () => {
    props.onSetShowForm((prevShowForm) => !prevShowForm);
  };

  return (
    <form onSubmit={sumbitHandler}>
      <div className="new-expense__controls">
        // ... 생략
      </div>
      <div className="new-expense__actions">
        <button type="button" onClick={cancelBtnClickHandler}>
          Cancel
        </button>
        <button type="submit">Add Expense</button>
      </div>
    </form>
  );
};

export default ExpenseForm;

 

Chart 추가하기

각 월별로 얼마나 예산을 썼는지 보여주는 간단한 차트를 추가하고 이 Expense 프로젝트를 마무리하려고 한다. 차트와 관련된 항목을 묶어주기 위해 새로운 폴더 Chart를 추가했다. 

 

차트를 보여주는 Chart.js, 실질적인 bar를 보여주는 ChartBar.js 두개의 js 파일과 각각 css 파일을 추가했다.

지금은 12개월을 보여줘야하는 것을 알기에 ChartBar 컴포넌트는 12개를 직접 만들어줘도 되지만, 그건 그렇게 효율적인 코딩방식은 아니어보인다. 따라서 Chart 컴포넌트는 부모로부터 어떤 데이터를 로딩해야될지 결정하는 dataPoints라는 배열을 전달받아 각각 value, label 값을 갖는 ChartBar 컴포넌트를 생성하려고 한다. 

 

import "./Chart.css";
import ChartBar from "./ChartBar";

const Chart = (props) => {
  return (
    <div className="chart">
      {props.dataPoints.map((dataPoint) => (
        <ChartBar
          key={dataPoint.label}
          value={dataPoint.value}
          label={dataPoint.label}
          maxValue={null}
        />
      ))}
    </div>
  );
};

export default Chart;

maxValue값은 임의로 null값으로 설정해주었다. 

 

Dynamic Style 추가하기

이제 ChartBar를 구현해야 하는데 이부분이 약간 복잡하다. 전달받은 value값과 maxValue값을 가지고 몇 %나 채워져있는지 계산하여 구현해야 하는데 이는 Math.round 함수를 통해 쉽게 구현할 수 있다.

import "./ChartBar.css";

const ChartBar = (props) => {
  let barFillHeight = "0%";
  if (props.maxValue > 0) {
    barFillHeight = Math.round((props.value / props.maxValue) * 100) + "%";
  }
  return (
    <div className="chart-bar">
      <div className="chart-bar__inner">
        <div className="chart-bar__fill"></div>
      </div>
      <div className="chart-bar__label">{props.label}</div>
    </div>
  );
};

export default ChartBar;

 

자 이제 barFillHeight 값으로 css 내의 높이를 설정해줘야 한다. css를 직접 건드리는 것은 style 속성을 이용하면 가능했다.

 

* 주의!

HTML 태그 내의 style 속성과 React의 style 속성은 약간 다르다. 일반적인 문법에서 문자열로 직접 적어주었다면, 리액트에서는 style 속성이 하나의 object로 작동한다. dynamic하게 값들을 넣어주기 위해 {} 를 사용하였고, style 속성이 object형이기 때문에 한번 더 내부에 {} 를 적어주었다. 헷갈리지 말자! 

 

* key:value 형태로 전달되는 style prop 값들은 만약 키에 "-"가 들어가는 경우(ex. background-color) 문자열(ex.'background-color')로 넘겨주거나 camelCase 방식(ex. backgroundColor)으로 넘겨줘야한다.

 

// ... 생략
	  <div
          className="chart-bar__fill"
          style={{ height: barFillHeight }}
        ></div>
      </div>

 

dataPoint 설정하러 가기

차트를 보여줄 수 있는 UI는 모두 설정해줬다. 이제 실제로 어떤 데이터를 차트로 보여줄 것인지 결정하는 ExpensesChart 컴포넌트를 생성했다. 해당 컴포넌트는 Chart 컴포넌트로 값을 넘겨줘야 하는데, 우리는 이 값을 object 여러 개를 배열로 넘겨줄 것이다. 

const chartDataPoints = [
        {label: 'Jan', value: 0},
        {label: 'Feb', value: 0},
        {label: 'Mar', value: 0},
        {label: 'Apr', value: 0},
        {label: 'May', value: 0},
        {label: 'Jun', value: 0},
        {label: 'Jul', value: 0},
        {label: 'Aug', value: 0},
        {label: 'Sep', value: 0},
        {label: 'Oct', value: 0},
        {label: 'Nov', value: 0},
        {label: 'Dec', value: 0},
    ]

1월부터 12월까지 label, value 값을 기본값으로 설정해주었다. 이제 실제로 어떤 value를 넣어줘야할 지 filtered된 expense값들을 살펴보자. (이 데이터는 Expenses 컴포넌트에 filteredExpenses라는 값으로 존재한다.)

 

유저가 설정한 연도에 해당하는 모든 데이터를 다 읽어온 뒤 각각 해당하는 달에 value값을 더해줘야 한다. expense의 date값에서 month값만 가져오는 방법은 JS의 Date 자료형에서 기본적으로 제공하는 getMonth() 함수를 통해 가져올 수 있으며 마침 이 값은 1월이 인덱스값 0으로 리턴된다. 즉 위에서 설정한 chartDataPoints의 인덱스으로 month를 사용해줄 수 있다! 추가 가공할 필요가 없어 아주 좋다. 

 

    for (const expense of props.expenses){
        const expenseMonth = expense.date.getMonth(); // Jan:0 -> same w/ dataPoints idx
        chartDataPoints[expenseMonth].value += expense.amount;
    }

 

이제 chartDataPoints는 유저가 필터링한 연도의 값들을 잘 월별로 갖고 있을 것이다. Chart 컴포넌트에 props로 넘겨주면 되겠다. 하지만 아직 작업이 전부 완료된 것은 아니다. Chart 컴포넌트에서 maxValue값을 추가로 계산하여 ChartBar 컴포넌트로 넘겨줘야 한다. maxValue값은 dataPoints의 각 월별 금액 중 가장 큰 값으로 설정해주면 되는데, Math.max() 함수를 사용하려고 한다. max() 함수는 인자로 최댓값을 계산하고자 하는 요소들을 콤마로 구분하여 받으므로 dataPoints 내 value값만을 올바른 포맷으로 넘겨주자.

 

import "./Chart.css";
import ChartBar from "./ChartBar";

const Chart = (props) => {
  const dataPointValues = props.dataPoints.map((dataPoint) => dataPoint.value);
  const totalMaxValue = Math.max(...dataPointValues);
  return (
    <div className="chart">
      {props.dataPoints.map((dataPoint) => (
        <ChartBar
          key={dataPoint.label}
          value={dataPoint.value}
          label={dataPoint.label}
          maxValue={totalMaxValue}
        />
      ))}
    </div>
  );
};

export default Chart;

 

이제 Expenses 컴포넌트에서 ExpensesChart 컴포넌트를 사용하는 것만 남았다! 

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

const Expenses = (props) => {
  const [filteredYear, setFilteredYear] = useState("2022");
  const enteredYearHandler = (enteredYear) => {
    setFilteredYear(enteredYear);
  };
  const filteredExpenses = props.data.filter(
    (item) => item.date.getFullYear().toString() === filteredYear
  );

  return (
    <div>
      <Card className="expenses">
        <ExpensesFilter
          selected={filteredYear}
          onEnteredYear={enteredYearHandler}
        />
        <ExpensesChart expenses={filteredExpenses} />
        <ExpensesList items={filteredExpenses} />
      </Card>
    </div>
  );
};

export default Expenses;

 

이제 원하는 대로 잘 동작한다.

결과 화면

 

Bug Fix

현재 여러개의 값들을 추가해줄 때 amount값이 string으로 추가되는 문제가 있어, 강제로 number형으로 형변환해주도록 ExpenseForm.js의 submitHandler함수를 수정하였다.

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

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

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

    props.onSaveExpenseData(expenseData);
    props.onSetShowForm((prevShowForm) => !prevShowForm);
  };

 

마무리

간단한 프로젝트가 끝났다. 아직 개선점이 많다고 하니 얼른 강의를 마저 따라가면서 리액트 핵심기술들을 모두 익혀보자. 

여기까지 작업한 코드를 올려두었다.

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

 

GitHub - cheonyeji/react_recap_ch3

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

github.com

 

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

모든 일련의 과정을 직접 적어줘야 하는 JS. React가 JS 기반 언어기 때문에 한번 전체적으로 JS를 훑고 들어가자.

 

var, const, let

데이터의 수정 여부에 관계 없이 모든 데이터 타입에 사용할 수 있었던 var는 구 버전 JS에서 많이 사용했다. 유저나 프로그래머가 값을 바꾸면 안되는 경우를 처리해주기 위해 수정 불가능한 const 자료형과 수정이 가능한 let 자료형이 등장했고 우리는 기본적으로 const로 데이터를 선언하되, 수정이 필요하다면 추후 let으로 바꾸도록 하자.

 

Exports & Imports 

const person = {
	name: 'Yeji'
}

export default person
export const someFn = () => { ... }
export const someData = 26;

위의 두 js 코드는 각각 default로 export 해주거나 개별 요소를 export 해주고 있다. 두 요소를 다른 js 코드에서 import 시킬 때 default로 내보낸 것은 어떤 이름으로 받아도 상관 없으나, 개별 요소가 export된 경우 정확한 이름을 명시하여 데이터를 가져와야 한다. 만약 데이터의 이름을 바꾸고 싶다면 'as'라는 키워드를 사용하여 alias를 설정한다.

 

만약 export한 데이터가 여러 개고 그 데이터를 모두 가져오고 싶다면 '*' 키워드를 사용하여 한꺼번에 모두 가져올 수 있다.

import person from './person.js'
import personData from './person.js'

import { someFn } from './util.js'
import { someData } from './util.js'
import { someData as age } from './util.js'

import * as bundle from './util.js'

 

Classes

class는 JS object의 blueprint라고 생각하자. 그 안에는 데이터, 함수, 생성자가 포함될 수 있으며 다른 클래스를 상속받을 수도 있다. 하단의 코드는 es6 방식이다.

class Female {
  constructor() {
    this.gender = 'Female';
  }
  printGender(){
    console.log(this.gender);
  }
}

class Student extends Female {
  constructor() {
    super();
    this.name = 'Yeji';
  }
  printName() {
    console.log(this.name);
  }
}

const student = new Student();
student.printName();
student.printGender();

 

다만 상속받은 데이터에 접근하려면 먼저 부모의 생성자를 호출( super() )한 뒤 사용해야 한다. 만약 Student  클래스 생성자 내에 super(); 가 빠지면 "ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor" 에러를 볼 수 있다. 

 

조금 더 모던한 방식으로 class를 선언해보자. 바로 변수값에 접근할 수 있고, this 키워드를 사용하지 않아도 된다는 장점이 있다. 만약 JS Bin으로 실습중이라면 상단에서 ES6 / Bable로 바꿔줘야 에러가 나지 않는다!

class Female {
  gender = 'Female';
  
  printGender = () => {
    console.log(this.gender);
  }
}

class Student extends Female {
  name = 'Yeji';
  
  printName = () => {
    console.log(this.name);
  }
}

const student = new Student();
student.printName();
student.printGender();

 

Spread & Rest Operators

'...' <- 이게 연산자다 놀랍게도.

 

Spread 연산자의 경우, array 요소나 object 속성을 쪼갤 때 사용한다. array나 object에 ...를 붙여주면 안에 들어있는 요소를 모두 꺼내서 처리해주는 편리한 기능이다.

const nums = [1,2,3];
const newNums = [...nums, 4];

const person = {
  name:"yeji"
}

const student = {
  ...person,
  age: 26
}

 

Rest 연산자의 경우, 함수의 arguments를 하나의 array로 합쳐줄 때 사용한다. 몇 개의 인자가 오든, 하나의 배열로 받아 처리할 수 있다.

const filter = (...args) => {
  return args.filter(data => data === 1);
}

console.log(filter(1,2,3,4));

 

Destructuring

Spread 연산자는 안의 모든 데이터를 꺼내서 새로운 요소를 만들 때 사용하지만, destructuring은 배열의 요소나 object의 속성을 추출해서 새로운 변수에 저장할 때 사용한다.

 

const nums = [1,2,3];
[num1, num2] = nums;
console.log(num1, num2); // 1 2

{name} = {name:'yeji', age:26}
console.log(name); // yeji
console.log(age); // undefined

 

Reference Type

자바에서 기본 데이터형(8가지, 논리형, 정수형, 실수형, 문자형)을 제외하고 다른 데이터 타입은 reference type이다. 즉 값을 메모리 상에 저장한 뒤 변수에는 그 메모리의 주소값만을 가지고 있는 타입이다. 따라서 JS에서 array나 object를 복제할 때 그 데이터 전체를 복제하게 되면 같은 주소를 다른 변수 2개가 가리키고 있는 상태가 될 수 있다.

const person = {
  name: "yeji"
}

const newPerson = person;

위와 같이 newPerson 객체를 만들게 되면, person 객체의 데이터를 변경했을 때 newPerson 객체의 데이터 역시 바뀌게 된다. 같은 주소지의 값을 가리키고 있기 때문에 당연한 결과다. 따라서 만약 안의 값들만 복제하고 싶다면 ... 연산자를 사용하여 새로운 배열이나 객체를 만들어 값을 넘겨주는 방식을 사용해야 한다. 

const person = {
  name: "yeji"
}

const newPerson = {
  ...person
};

 

참조 변수를 안의 데이터를 복사하여 새로운 요소를 만들어 복제하는 것이 아니라 단순 그 값 전체를 넘겨주게 된다면 예기치 못한 버그 폭탄을 맞을 수 있으니 주의하자.

 

Array Function 복습

array Function 방식은 코드를 간결하게 하는데 아주 유용하다. 많이 사용되는 패턴 하나를 통해 익혀보자.

const nums = [1,2,3];

const doubleNums = nums.map((num) => {
  return num*2;
})

 

array 내의 다양한 함수는 Mozilla 공식 문서(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)를 참고하자.

 

Array - JavaScript | MDN

The Array object, as with arrays in other programming languages, enables storing a collection of multiple items under a single variable name, and has members for performing common array operations.

developer.mozilla.org

 

map 함수는 해당 배열 내의 요소에 모두 접근하여 동작을 수행한 뒤 return되는 요소로 새로운 배열을 만들어주는 함수이다. 아주 유용하니까 기억해두자. 어차피 자주 사용해서 익숙해질 것이다 :) 

 

그 외에도 기억해두면 좋을 요소들은 한번 더 정리해두겠다. 기능이 기억나지 않는다면 읽어볼 것!

( 오,,, filter함수 shallow copy해준다.... 공식문서 영어버전을 보길 잘했다. 예기치 않은 버그를 만날 수도 있겠는걸)

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

React Basics  (0) 2022.07.20
React : create-react-app  (0) 2021.05.19
React의 메인 개념 : component  (0) 2021.05.19

+ Recent posts