📚 서론

이전 포스트에서 모던 자바스크립트에 대한 간단한 문법과 특징들에 대해 배웠다. 이번엔 리엑트가 무엇이고 컴포넌트, state, props 등이 무엇인지에 대해 알아보자.

리액트를 공부하다 보니 최근에 import, export 에 대해 알아 봤던게 계속 떠오른다.

const App = () => {
	return (
    	<h1>hi</h1>
    )
}

위와 같은 컴포넌트를 모듈이라고 생각해보자. 다른 파일에서 이 모듈을 사용하려면 export, import 를 해줘야 한다.

const App = () => {
	return (
    	<h1>hi</h1>
    )
}

export defualt App;
import App from './App';

자잘하게 리엑트에 대해 알아야 할 점들

  • 리액트는 자바스크립트 기반이라 확장자가 .js 이나 컴포넌트는 .jsx 를 사용하기도 한다.

  • 이벤트는 js 와 달리 카멜케이스를 사용한다.
    onClick / onChange

  • css 속성명도 카멜케이스를 사용한다.
    fontSize / marginTop

  • JSX 안에서 JS 는 {} 로 감싸서 작성한다.

  • 리엑트에서 인라인 스타일로 스타일을 지정해줄때 {} 를 두번 쓰는게 왜 그런가 싶으면서도 번거로움이 느껴졌었다.
    이는 리엑트에서 css 의 각 요소가 자바스크립트 객체로 기술하기에 style = {} 내에 css 요소를 객체로 넣어야 해서 중괄호를 이중으로 사용하는 형태가 된 것이다.

<p style={{ color: "blue" }}>안녕하세요</p>
  • css 의 요소를 자바스크립트 객체로 기술하기에 다음과 같은 형태도 가능하다.
const customStyle = {
	color: "red",
	fontSize: "20px"
}

<p style={customStyle}>안녕하세요</p>

📘 컴포넌트

docs 의 내용을 번역하며 추가로 찾아본 내용을 정리했다.

React 를 사용하면 마크업, CSS, JS 를 앱의 재사용 가능한 UI 요소인 사용자 지정 컴포넌트로 결합할 수 있다. 컴포넌트는 UI (유저 인터페이스)의 구성요소로 고유한 논리와 모양을 갖는다. 컴포넌트는 아주 작은 버튼에서부터 아주 큰 페이지까지 될 수 있다.

<PageLayout>
  <NavigationHeader>
    <SearchBar />
    <Link to="/docs">Docs</Link>
  </NavigationHeader>
  <Sidebar />
  <PageContent>
    <TableOfContents />
    <DocumentationText />
  </PageContent>
</PageLayout>

리엑트 컴포넌트는 자바스크립트 함수로 마크업을 반환한다.

마크업 : React 컴포넌트에서 반환하는 자바스크립트 함수가 생성하는 구조화된 코드를 의미한다. 이는 일반적으로 JSX 문법을 사용하여 작성되며 화면에 표시될 요소들의 구조와 모양을 정의하는 역할을 한다.

같은 파일내라면 export 를 할필요 없이 생성한 컴포넌트를 다른 컴포넌트에 nest 중첩해줄수 있다.

const TableHeader = () => { ... }
const TableBody = () => { ... }

const Table = () => {
  return (
    <table>
      <TableHeader />
      <TableBody />
    </table>
  );
}

위와 같이 컴포넌트는 다른 컴포넌트에 중첩될 수 있다.

React 에서의 컴포넌트는 HTML 태그가 반드시 lowercase 로 작성하는 것과 달리 반드시 대문자로 시작한다.

<MyButton/>
<p></p>

주의점
return 의 경우 한줄일때 줄바꿈을 하면 안된다. 만약 그럴시엔 괄호로 감싸줘야 한다.

export default function Profile() {
  return
    <img src="https://i.imgur.com/jA8hHMpm.jpg" alt="Katsuko Saruhashi" />;
}

🅾️

export default function Profile() {
  return <img src="https://i.imgur.com/jA8hHMpm.jpg" alt="Katsuko Saruhashi" />;
}
export default function Profile() {
  return (
    <img 
      src="https://i.imgur.com/jA8hHMpm.jpg" 
      alt="Katsuko Saruhashi" 
    />
  );
}

컴포넌트, interface, type 별칭에는 파스칼 케이스를 사용한다.

// React component
const LeftGridPanel = () => {
  ...
}

// Typescript interface
interface AdminUser {
  name: string;
  id: number;
  email: string;
}

// Typescript Type Alias
type TodoList = {
	todos: string[];
    id: number;
    name: string;
}

변수, 배열, 객체, 함수 등 JavaScript 데이터 타입에는 카멜 케이스를 사용한다.

const getLastDigit = () => { ... }

const userTypes = [ ... ]

📗 컴포넌트 export

function Profile() {
  return (
    <img
      src="https://i.imgur.com/lICfvbD.jpg"
      alt="Aklilu Lemma"
    />
  );
}

export default function Profile() {
  return (
    <img
      src="https://i.imgur.com/lICfvbD.jpg"
      alt="Aklilu Lemma"
    />
  );
}

그냥 export 를 쓸수는 없을까?

그냥 컴포넌트는 export 될 수 없고 앞에 export default 가 붙어야 한다.

확인해보니 그냥 export 를 붙였다고 항상 저 에러가 뜨는게 아니고 조건이 있었다.

출처 : react.dev docs - component

SyntaxExport statementImport statement
Defaultexport default function Button() {}import Button from './Button.js';
Namedexport function Button() {}import { Button } from './Button.js';

컴포넌트를 export default 로 export 할 경우 import 할때
import Button from './Button.js'; 방식으로 해야 하고

컴포넌트를 export 로 export 할 경우 import 할때
import { Button } from './Button.js'; 방식으로 해야 한다.

이게 지켜지지 않았을때 위와 같은 오류가 뜬다.

추가로 클래스나 함수를 내보낼때는 세미콜론을 붙이지 않는다. 위에선 잘 모르고 붙였었는데 세미콜론을 붙인다고 함수 선언 방식이 함수 선언문에서 함수 표현식으로 바뀌지 않는다. JS 스타일 가이드에서도 함수나 클래스 선언 뒤에 세미콜론을 붙이지 않는 것을 권장한다.

📕 import 순서

import React, { useState, useEffect, useCallback } from "react";
import Typography from "@material-ui/core/Typography";
import Divider from "@material-ui/core/Divider";
import Title from "../components/Title";
import Navigation from "../components/Navigation";
import DialogActions from "@material-ui/core/DialogActions"
import { getServiceURL } from '../../utils/getServiceURL";
import Grid from "@material-ui/core/Grid";
import Paragraph from "../components/Paragprah";
import { sectionTitleEnum } from "../../constants";
import { useSelector, useDispatch } from "react-redux";
import Box from "@material-ui/core/Box";
import axios from 'axios';
import { DatePicker } from "@material-ui/pickers";
import { Formik } from "formik";
import CustomButton from "../components/CustomButton";

react 에서 컴포넌트를 가져와 쓰다보면 어느새 수없이 많이 import 한 컴포넌트, 모듈, 패키지들을 볼 수 있게 된다. 어느순간 뭐가 뭔지 헷갈리게 될수 있어서 그룹으로 묶어두는 것을 권장하기도 한다.

import React, { useState, useEffect, useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import { Formik } from "formik";
import axios from 'axios';
import Typography from "@material-ui/core/Typography";
import Divider from "@material-ui/core/Divider";
import Box from "@material-ui/core/Box";
import DialogActions from "@material-ui/core/DialogActions";
import Grid from "@material-ui/core/Grid";
import { DatePicker } from "@material-ui/pickers";

import { getServiceURL } from '../../utils/getServiceURL";
import { sectionTitleEnum } from "../../constants";
import CustomButton from "../components/CustomButton";
import Title from "../components/Title";
import Navigation from "../components/Navigation";
import Paragraph from "../components/Paragraph";

이처럼 내부 및 외부 import 문을 분리하기도 하고

내장('react' 같은) -> 외부 (서드파티 모듈) -> 내부 순으로 정리하기도 한다.

import 를 정렬하는 것은 JS 파일을 다루는데 큰 장점이 있는데 먼저 각각의 패키지들을 import 한 것을 보기 쉽게 만들어주고 어떤 import 가 서드파티 패키지인지 지역 import 인지 쉽게 구분할 수 있게 해준다.

ESLint 를 사용한다면 이에 대해 큰 도움이 될 것이다.

ESLint 는 import 를 정렬하는 규칙이 내장되어 있다. 만약 내가 원하는 바를 충분히 충족시키지 않는다면 eslint-plugin-import 를 사용해보자.

# inside your project's working tree
npm install eslint-plugin-import --save-dev

{
  "import/order": [
    "error",
    {
      "groups": [
        // import 되는 순서를 정의, 그룹 내 각 요소의 순서와 동일
        // builtin / external / internal / unknown / parent / sibling / index / object / type
        "builtin",
        "external",
        "internal",
        // 'sibling', 'parent', 'index' 타입은 상대 경로의 파일로부터 import 되는 모듈을 뜻한다.
        ["sibling", "parent", "index"],
        "type",
        // 생략된 'object' 타입과 함께 가장 마지막에 import 된다.
        "unknown"
      ],
      "pathGroups": [
        // 추가적으로 옵션을 설정하고 싶은 경로를 별칭과 함께 다시 그룹화
        {
          "pattern": "react",
          "group": "external",
          "position": "before" // before / after
        },
        {
          "pattern": "@saas-fe/**/*.style",
          "group": "unknown"
        }
      ],
      "pathGroupsExcludedImportTypes": ["react", "unknwon"],
      // pathGroup 에서 설정에 의해 처리되지 않는 import type 을 정의한다.
      // 즉, import 문을 다른 그룹과 분리하여 순서 규칙에서 제외시 사용한다.
      // 보통 외부 라이브러리나 특정 패키지들을 다른 그룹으로 분리하고자 할 때 이 옵션을 사용한다.
      "newlines-between": "always",
      // 그룹 사이에 줄바꿈을 강제화 하거나 금지하는 옵션이다.
      // ignore / always / always-and-inside-groups / never
      "alphabetize": {
        // 그룹 내에서 알파벳을 기준으로 정렬 순서를 결정한다.
        "order": "asc",
        "caseInsensitive": true
      }
    }
  ]
}
  • groups : group 의 순서 설정
  • pathGroups : pattern 에 의해 설정한 경로에 의해 그룹화할 수 있다.
  • pathGroupsExcludedImportTypes : import type 을 정의한다. 위의 경우 react 는 다른 타입들처럼 다뤄질 것이기에 다른 외부 패키지들과 분리할 수 있다.
  • newline-between : 각 그룹을 새 라인을 사이에 두고 구분한다.
  • alphabetize : 그룹을 어떻게 정렬할지를 정한다.

오류가 발생했는데 parser 에 대한 설정을 잘못 했던게 원인이라 다음과 같이 수정했다.

... 왜일까? 적용이 전혀 안되고 있다... 플러그인도 설치하고 extension 도 적용했는데...

이 부분에 대해선 추가로 확인후 따로 eslint 에 관한 포스트를 정리할 예정이다.

📗 JSX

JavaScript 의 확장 문법으로 HTML 과 유사하나 JavaScript 코드로 컴파일되어 동작한다. 리엑트에서 사용중인 마크업 문법이다.

  • HTML 보다 조금 더 엄격하다. 태그는 반드시 닫아줘야 한다. <br />
  • 컴포넌트는 여러개의 JSX 를 반환할 수 없다. (하나만 반환)
  • 많은 HTML 태그를 JSX 로 전환해야 할때 온라인 컨버터를 사용할 수 있다.
  • JSX 를 사용하면 HTML 처럼 보이는 것을 작성할 수 있고 XML 과 유사한 자체 태그를 만들어 사용할 수 있다.
const heading = <h1 className="site-heading">Hello, React</h1>

// 위와 동일
const heading = React.createElement('h1', { className: 'site-heading' }, 'Hello, React!')

📕 React 가 마크업과 렌더링 로직을 혼합하는 이유

웹은 HTML, CSS 및 JS 를 기반으로 구축되었다. 오랜시간동안 콘텐츠는 HTML, 디자인은 CSS, 로직은 JS 로 구성했다. 콘텐츠는 HTML 내에서 마크업되었으나 로직은 JS 에서 별도로 존재했다.

그런데 웹이 점점 더 상호작용하게 되며 논리가 콘텐츠를 결정하게 되었고 JS 가 HTML 을 담당하게 되면서 리엑트에서 렌더링 로직과 마크업이 같은 위치인 컴포넌트에 함께 존재하게 되었다.

출처 : react.dev - JSX 로 마크업 작성

렌더링 로직과 마크업을 함께 유지하면 수정할 때마다 서로 동기화가 유지되는데 관련되지 않은 내용들은 서로 분리되어 있어 각각을 따로 변경하는 것이 좋다. React 컴포넌트는 React 가 렌더링하는 일부 마크업을 포함할 수 있는 JS 함수이다. 컴포넌트는 JSX 라는 구문 확장을 사용해서 마크업을 나타낸다.

📕 중괄호가 있는 JSX 의 JavaScript

JSX 를 사용시 JavaScript 파일 내에 HTML 과 유사한 마크업을 작성해서 렌더링 로직과 콘텐츠를 같은 위치에 유지할 수 있다. 때에 따라 JS 로직을 추가하거나 마크업 내에서 동적 속성을 참조하고 싶을 수 있는데 이럴때 JSX 에서 중괄호를 사용해서 JS 창을 열 수 있다.

  • 문자열은 작은 따옴표나 큰따옴표로 묶어야 한다.
  • 혹은 텍스트를 동적으로 지정하고자 할때 따옴표를 중괄호로 바꿔서 사용한다. 중괄호를 사용시 마크업에서 JS로 바로 동작할 수 있다.
export default function TodoList() {
  const name = 'Gregorio Y. Zara';
  return (
    <h1>{name}'s To Do List</h1>
  );
}

📖 중괄호를 사용할 수 있는 위치

  1. JSX 태그의 내부에 텍스트로서 사용할 수 있다. 그러나 태그를 중괄호로 감쌀수는 없다.
  • <h1>{name}'s To Do List</h1> : OK
  • <{tag}>Gregorio Y. Zara's To Do List</{tag}> : NO
  1. = 등호 뒤에 따라오는 어트리뷰트에 중괄호를 사용할 수 있다. 그러나 따옴표로 감싼 중괄호는 문자로서 취급된다.
  • src={avatar} : OK
  • src="{avatar}" : NO
const person = {
  name: 'Gregorio Y. Zara',
  theme: {
    backgroundColor: 'black',
    color: 'pink'
  }
};

export default function TodoList() {
  return (
    <div style={person.theme}>
      <h1>{person.name}'s Todos</h1>
      <img
        className="avatar"
        src="https://i.imgur.com/7vQD0fPs.jpg"
        alt="Gregorio Y. Zara"
      />
      <ul>
        <li>Improve the videophone</li>
        <li>Prepare aeronautics lectures</li>
        <li>Work on the alcohol-fuelled engine</li>
      </ul>
    </div>
  );
}

주의점
바로 앞의 예시코드에서 <h1>{person.name}'s Todos</h1> 의 경우 <h1>{person}'s Todos</h1> 처럼 사용할 수 없다.

위와 같은 방식으로 children 일부에 객체가 올 수는 없다. text 로 이루어진 요소노드엔 문자열을 넣어줘야 한다.

📗 스타일 추가하기

리엑트에서 CSS 클래스는 className 으로 추가한다.

📗 조건식

리엑트에서 따로 조건식을 위한 특별한 문법이 있지 않다. 대신 JS 에서 사용하듯 같은 조건식을 사용하면 된다.

그냥 if else 와 같은 조건식을 사용하거나 3항 연산자를 사용할 수 있다.

<div>
  {isLoggedIn ? (
    <AdminPanel />
  ) : (
    <LoginForm />
  )}
</div>

else 를 필요로 하지 않을 경우 && 논리곱 연산자를 사용할 수 있다.

<div>
  {isLoggedIn && <AdminPanel />}
</div>

이런 조건식은 attribute 를 명세하는데에도 같은 방식으로 사용된다.

정리

  • JSX 에서 {cond ? <A /> : <B />} 는 만약 cond 조건이라면 <A /> 를 렌더링하고 아니라면 <B /> 를 렌더링하라는 것을 의미한다.
  • JSX 에서 {cond && <A />} 는 만약 cond 을 만족하면 <A /> 를 렌더링하고 그렇지 않으면 아무것도 렌더링하지 않는다.

📗 배열 렌더링

for loop 이나 array map() 함수를 이용하여 컴포넌트 리스트를 렌더링할 수 있다.

예를 들어 어떤 제품에 대한 정보를 담은 배열이 있다고 할때 이 제품들을 map 을 통해 JSX 문법으로 치환하여 리스트에 넣어줄 수 있다.

const products = [
  { title: 'Cabbage', id: 1 },
  { title: 'Garlic', id: 2 },
  { title: 'Apple', id: 3 },
];

const listItems = products.map(product =>
  <li key={product.id}>
    {product.title}
  </li>
);

return (
  <ul>{listItems}</ul>
);

주의점

  • React 에서 <li> 와 같은 리스트를 사용할때 반드시 유일한 숫자나 문자로 이루어진 식별자 (예를들어 key)를 부여해야 한다.

📕 배열으로부터 데이터 렌더링하기

react.dev 의 renderling-lists 에 나오는 내용입니다. 바로가기

<ul>
  <li>Creola Katherine Johnson: mathematician</li>
  <li>Mario José Molina-Pasquel Henríquez: chemist</li>
  <li>Mohammad Abdus Salam: physicist</li>
  <li>Percy Lavon Julian: chemist</li>
  <li>Subrahmanyan Chandrasekhar: astrophysicist</li>
</ul>

위의 리스트 아이템들은 contents 만 다르다. 우리는 이런 데이터를 JS 객체나 배열에 저장하고 map 이나 filter 같은 메서드를 사용하여 컴포넌트 리스트를 렌더링할 수 있다.

📖 map 을 통한 배열 렌더링

  1. 데이터를 배열에 넣기
const people = [
  'Creola Katherine Johnson: mathematician',
  'Mario José Molina-Pasquel Henríquez: chemist',
  'Mohammad Abdus Salam: physicist',
  'Percy Lavon Julian: chemist',
  'Subrahmanyan Chandrasekhar: astrophysicist'
];
  1. people 멤버들을 새로운 배열이자 JSX 노드인 listItem 에 매핑한다.
const listItems = people.map(person => <li>{person}</li>);
  1. listItem<ol> 혹은 <ul> 로 감싼다.
return <ul>{listItems}</ul>;

📖 filter, map 를 통한 배열 렌더링

const people = [{
  id: 0,
  name: 'Creola Katherine Johnson',
  profession: 'mathematician',
}, {
  id: 1,
  name: 'Mario José Molina-Pasquel Henríquez',
  profession: 'chemist',
}, {
  id: 2,
  name: 'Mohammad Abdus Salam',
  profession: 'physicist',
}, {
  name: 'Percy Lavon Julian',
  profession: 'chemist',  
}, {
  name: 'Subrahmanyan Chandrasekhar',
  profession: 'astrophysicist',
}];

profession 이 'chemist' 인 사람들을 보이고 싶어하는 상황이다. 이때 filter 메서드를 사용할 수 있다.

  1. 먼저 profession 이 'chemist 인 조건을 만족하는 person 을 필터링하여 chemsits 라는 새로운 배열을 생성한다.
const chemists = people.filter(person =>
  person.profession === 'chemist'
);
  1. chemists 를 매핑한다.
const listItems = chemists.map(person =>
  <li>
     <img
       src={getImageUrl(person)}
       alt={person.name}
     />
     <p>
       <b>{person.name}:</b>
       {' ' + person.profession + ' '}
       known for {person.accomplishment}
     </p>
  </li>
);
  1. 마지막으로 listItems 를 컴포넌트에 넣어 반환한다.
return <ul>{listItems}</ul>;

계속 렌더링이란 말이 나왔는데 처음 화면을 표시하는 것을 렌더링이라 하고 변경을 감지하고 컴포넌트를 다시 처리하는 것을 재렌더링 이라 부른다. 렌더링과 재렌더링의 차이로 리엑트의 라이프 사이클에 관한 것을 다음 포스트에서 다뤄보자

Pitfall

  • 화살표함수에서 => 다음에 오는 것은 반환값이니 중괄호 없이 사용할 수 있다.
  • 단, 중괄호를 사용할 경우 return 을 꼭 명시해줘야 하며 이 경우엔 한줄 이상일 경우 권장된다.

Pitfall 예시코드


const listItems = chemists.map(person =>
  <li>...</li> // Implicit return!
);
                               
const listItems = chemists.map(person => { // Curly brace
  return <li>...</li>;
});

📗 이벤트에 응답

function MyButton() {
  function handleClick() {
    alert('You clicked me!');
  }

  return (
    <button onClick={handleClick}>
      Click me
    </button>
  );
}

이벤트는 뒤에 괄호가 붙지 않았다는 것에 주의하자. 리엑트는 유저가 이벤트를 동작시킬때 이벤트 핸들러를 부른다.

📘 props & state

📗 props


출처: 모던 자바스크립트로 배우는 리액트 입문

props 는 컴포넌트에 전달하는 인수와 같은 것으로 컴포넌트는 전달받은 props 에 따라 표시하는 스타일과 내용을 변경한다.

전에 만들어밨던 버튼 컴포넌트가 대략 아래와 같았다.

import styled, { css } from "styled-components";
import colors from "../../config/Colors";

const StyledButton = styled.button`
  width: ${(props) => props.width || "100%"};

  ${(props) =>
    props.input &&
    css`
      color: ${colors.gray};
    `};

  ${(props) =>
    props.login &&
    css`
      onClick: ${props.onClick};
    `};

  ${(props) =>
    props.disabled &&
    css`
      background: ${colors.bg_disabled};
      color: ${colors.white};
    `};
`;


function Button({ children, text, ...props }) {
    return <StyledButton {...props}>{text || children}
   </StyledButton>;
}
export default Button;

버튼 사용시

<Button input text="입력버튼"/>
<Button login />
<Button disabled />

컴포넌트를 사용하다보면 위에서 사례로 든 버튼처럼 기본 버튼, 비활성화 버튼, 로그인 버튼, 입력 버튼 등 큰 범위로 보면 공통요소가 있으나 세부적인 차이가 조금씩 있는 경우가 있다. 그런데 그들을 DefaultButotn, DisabledButton, LoginButton, InputButton ... 등 만들다 보면 컴포넌트가 너무 많아져 재사용하기 어려워진다. 이때 어느 정도 동적으로 컴포넌트를 재사용할 수 있게 props 로 조건을 전달할 수 있다.

props 는 컴포넌트 태그 안에 임의의 이름을 붙여 props 를 전달할 수 있다. 그러면 컴포넌트는 파라미터로 props 을 전달받아 조건을 확인할 수 있다.

그 외에 특별한 props 로 children 이 있다.
컴포넌트도 HTML 태그와 마찬가지로 임의의 요소를 감싸서 사용할 수 있는데 이 둘러싸인 부분이 props 에 설정된다.

<Button>
  <span>안녕하세요</span>
</Button>

위의 경우 안녕하세요 가 Button 의 children props 가 된다. 사용시엔 props.children 으로 사용할 수 있다.

📕 children 사용 예시

react.dev 의 passing-props-to-a-component 챕터의 예시코드다.

App.js

import Avatar from './Avatar.js';

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

export default function Profile() {
  return (
    <Card>
      <Avatar
        size={100}
        person={{ 
          name: 'Katsuko Saruhashi',
          imageId: 'YfeOqp2'
        }}
      />
    </Card>
  );
}

import Avatar from './Avatar.js'; 를 사용해서 import 한 것을 보면 Avatar 는 export default 를 사용했음을 추측할 수 있다.

Avatar.js

import { getImageUrl } from './utils.js';

export default function Avatar({ person, size }) {
  return (
    <img
      className="avatar"
      src={getImageUrl(person)}
      alt={person.name}
      width={size}
      height={size}
    />
  );
}

utils.js

export function getImageUrl(person, size = 's') {
  return (
    'https://i.imgur.com/' +
    person.imageId +
    size +
    '.jpg'
  );
}

string props 에는 중괄호를 피하자. strig 이 아니라 JS 표현식을 사용하고 싶다면 중괄호를 사용해야 한다.

📗 state

출처: 모던 자바스크립트로 배우는 리액트 입문

컴포넌트가 특정 정보들을 기억하도록 하기 위해서는 state 라는것이 필요하다. 리액트 개발에서는 화면에 표시하는 데이터나 길이가 변하는 상태 등을 모두 state 로 관리한다.

state : 컴포넌트의 상태를 나타내는 값

화면 상태로

  • 에러가 있는지
  • 모달 창을 열고 있는지
  • 버튼을 클릭했는지
  • 텍스트 박스에 무언가 입력했는지

등을 예로 들 수 있다.

함수 컴포넌트에서 리액트 훅이라 말하는 기능중 useState 라는 함수를 사용해 state 를 다룬다.

import { useState } from 'react';

useState 를 개발자모드로 조회해보면 useState 의 반환값이 배열 형태로 첫 번째 인수가 현재 상태를 의미하는 state 이고 두 번째 인수가 function 인 것을 볼 수 있다. 이 함수는 state 를 업데이트하기 위한 함수가 설정되어 주로 다음과 같은 형태로 사용한다.

const [mode, setMode] = useState();

useState의 인자로는 초깃값을 설정해 줄 수 있다.

function MyButton() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      Clicked {count} times
    </button>
  );
}

수치 외에도 자바스크립트에서 변수로 다루는 문자열, 논리값, 배열, 객체등 무엇이든 state 로 관리할 수 있다.

그런데 사실 위의 코드는 다음과 같이 사용하는게 엄밀하게는 더 올바른 방법이다.

function MyButton() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount((prev) => prev + 1); // 바꾼 부분
  }

  return (
    <button onClick={handleClick}>
      Clicked {count} times
    </button>
  );
}

첫 번째 코드

  • setCount 함수에 새로운 상태값을 전달시 현재의 count 값을 사용해서 1을 더한값을 넘긴다. 즉, 현재 상태의 값을 바탕으로 새로운 상태를 설정한다.

두 번째 코드

  • seCount 함수에 콜백 함수를 전달하여 이전 상태 값을 매개 변수로 받아와 그 값을 기반으로 새로운 상태 값을 계산한다. 이전 상태 값을 사용해서 새로운 값을 설정하는 접근 방식을 함수형 업데이트라고 하며 비동기적 업데이트에서 안정성을 보장한다.
function handleClick() {
  setCount(count + 1);
  setCount(count + 1);
}

function handleClick() {
  setCount((prev) => prev + 1);
  setCount((prev) => prev + 1);
}

전자의 경우 각 호출에서 사용되는 count 값이 동일한 값을 가지기에 두 번 실행해도 상태가 한번만 증가한다.
후자의 경우 각 호출에서 이전 상태 값을 기반으로 하여 새로운 값을 계산하므로 두 번 실행시 상태가 두 번 증가한다.

컴포넌트를 여러번 사용시 각 컴포넌트는 각자의 상태를 고유하게 갖는다.

📘 추가 학습 JS +

📗 Set

let animals = [
  {
    name:'Lion',
    category: 'carnivore'
  },
  {
    name:'dog',
    category:'pet'
  },
  {
    name:'cat',
    category:'pet'
  },
  {
    name:'wolf',
    category:'carnivore'
  }
]

set 은 반복을 제거해준다.

let category = animals.map((animal)=>animal.category);
console.log(category); //["carnivore" , "pet" , "pet" , "carnivore"]

그냥 map 을 사용하면 중복 여부를 체크하지 않는다고 할때 (조건식 여부 제외하고 그냥 모든 값을 보고 반환하는 상황)

//wrap your iteration in the set method like this
let category = [...new Set(animals.map((animal)=>animal.category))];

console.log(category); //["carnivore" , "pet"]

set 을 사용하면 위와 같이 중복을 제거할 수 있다.

📗 reduce()

reduce 는 매우 강력한 배열 함수이다. 이는 filter 와 find 메서드를 대체할 수 있고 대용량 데이터를 다룰때 map 과 filter 를 사용하는 것보다 좀더 용이하다.

map 과 filter 를 연달아 사용시 작업을 두번 하게 된다. 먼저 모든 각각의 값을 필터링하고 그 뒤에 남은 값들을 매핑한다.

반면에 reduce 는 필터링과 매핑을 한번에 할 수 있게 한다.

reduce 는 배열을 반복하면서 map, filter, find 등과 같이 콜백 함수를 얻는다. 차이점으로는 reduce 는 배열을 단일 값(숫자, 배열, 객체) 으로 줄인다.

다음의 예시에서 reduce 는 두개의 인자를 갖는데 첫 번째 인자는 계산의 총합이고 두 번째 인자는 현재 반복 값이다.

let staffs = [
  { name: "Susan", age: 14, salary: 100 },
  { name: "Daniel", age: 16, salary: 120 },
  { name: "Bruno", age: 56, salary: 400 },
  { name: "Jacob", age: 15, salary: 110 },
  { name: "Sam", age: 64, salary: 500 },
  { name: "Dave", age: 56, salary: 380 },
  { name: "Neils", age: 65, salary: 540 }
];
const salaryInfo = staffs.reduce(
  (total, staff) => {
    let staffTithe = staff.salary * 0.1;
    total.totalTithe += staffTithe;
    total['totalSalary'] += staff.salary;
    return total;
  },
  { totalSalary: 0, totalTithe: 0 }
);

console.log(salaryInfo); // { totalSalary: 2150 , totalTithe: 215 }

자세히 알아볼까?

reduce 메서드는 자신을 호출한 배열의 모든 요소를 순회하며 인수로 전달받은 콜백 함수를 반복 호출한다. 그리고 콜백 함수의 반환값을 다음 순회시 콜백 함수의 첫 번째 인수로 전달하면서 콜백 함수를 호출하여 하나의 결과값을 만들어 반환하며 원본 배열은 변경되지 않는다.

reduce 메서드는 첫 번째 인자로 콜백 함수, 두 번째 인수로 초기값을 전달받는다. reduce 메서드의 콜백함수는 4개의 인수를 갖는데 초기값 또는 이전 반환값, reduce 메서드를 호출한 배열의 요소값과 인덱스, reduce 메서드를 호출한 배열 그 자체인 this 가 전달된다.

const sum = [1, 2, 3, 4].reduce((accumulator, currentValue, index, array) => accumulator + currentValue, 0);

console.log(sum); // 10

reduce 를 사용하는 사례로 평균 구하기, 최대값 구하기, 요소의 중복 횟수 구하기, 중첩 배열 평탄화, 중복 요소 제거등이 있다.

그중 중첩 배열 평탄화는 다음과 같이 구현할 수 있다.

const values = [1, [2, 3], 4, [5, 6]];

const flatten = values.reduce((acc, cur) => acc.concat(cur), []);
// [1] => [1, 2, 3] => [1, 2, 3, 4] => [1, 2, 3, 4, 5, 6]

console.log(flatten); // [1, 2, 3, 4, 5, 6]

물론 위의 경우는 ES2019 에서 도입된 Array.prototpe.flat 메서드가 더 직관적이다.

[1, [2, 3], 4, [5, 6]].flat(); // [1, 2, 3, 4, 5, 6]

[1, [2, 3, [4, 5]].flat(2); // [1, 2, 3, 4, 5], 인수 2 는 중첩 배열을 평탄화하기 위한 깊이 값이다.

📔 레퍼런스

docs
react docs ⭐️
book
모던 자바스크립트로 배우는 리엑트 입문
모던 자바스크립트 Deep Dive
blog
react tutorial
FreeCodeCamp - React 배우기 전에 알아야 할 상위 JS 개념
log4me - ESLint
DEKU - ESLint
lakelouise - ESLint
helloinyoung - ESLint
column
Sorting your imports correctly in React
FreeCodeCamp - 리엑트 모범 사례 - 2022 더 나은 리액트 코드 작성을 위한 팁 ⭐️

📓 결론

  • 컴포넌트는 재사용 가능한 JS 함수가 생성하는 구조화된 코드를 의미하며 JSX 문법을 사용하여 작성된다.
  • export, export default 의 경우 import 할때 차이점이 있다.
  • import 순서를 구분해서 명세하는 것이 추후 내장, 외부, 서드파티 패키지등을 구분하기 쉽게 한다. 이때 ESLint 를 사용할 수 있다.
  • JSX 의 특징, JSX 에서의 조건식과 배열 렌더링 방식에 대해 알아봤다.
  • props 와 state 의 기본 개념을 비교해봤다.

문서를 찾다 보면 역시 공식문서 docs 가 가장 잘 설명되어 있다. 책, docs, 정리가 잘 된 블로그 들을 비교하며 정리하고 있으며 그중 이해가 되지 않는 부분에 대해서 chatGPT 의 도움을 받고 있다.

그냥 크롤링한 코드를 복붙하는 개발자보다 왜 동작하는지 원리와 개념에 대해 확실히 아는 개발자가 되고 싶다.

추가로 robinwieruch - React Folder Structure in 5 Steps - 2022 에서 보면 react 의 구조에 대해 궁금했었는데 react 폴더 구조에 관한 글을 볼 수 있었다. 흥미로운 내용이니 한번 참고해보자.

profile
미래의 나를 만들어나가는 한 개발자의 블로그입니다.

0개의 댓글

Powered by GraphCDN, the GraphQL CDN