2차 공부/TIL

24.08.14 리액트 개념 아티클

공대탈출 2024. 8. 14. 19:43

https://yozm.wishket.com/magazine/detail/2688/

 

알아두면 유용한 ‘리액트’ 개념과 성능 최적화 팁 | 요즘IT

리액트는 프론트엔드 개발에서 가장 널리 사용되고 있는 자바스크립트 라이브러리입니다. 컴포넌트 기반 아키텍처와 가상 DOM 등의 개념을 도입하였으며, 여러 글로벌 기업이 리액트를 활용하

yozm.wishket.com

요즘IT의 알아두면 유용한 리액트개념과 성능 최적화 팁에 대해 정리를 해보고자 한다.

 

 

리액트의 기본 개념

 

1. 컴포넌트 기반 아키텍쳐

- 복잡한 UI를 재사용가능한 단위인 컴포넌트로 분할하고, 각 컴포넌트는 상태(State)와 속성(prop)을 가지고 있으며, 독립적으로 작동한다.

- 각 컴포넌트는 특정 기능이나 UI를 책임지며, 관심사를 분리하여 개발에 집중할 수 있다.

- UI를 계층적으로 구조화하여 코드 가독성을 높이고, 유지보수에 도움을 준다. 

출처 - 나

컴포넌트 기반 아키텍쳐를 설계할 때에는 구성요소간 의존성을 최소화하고, 컴포넌트 하나마다 한가지 책임만을 지는 단일책임원칙을 생각해야한다. ==> 컴포넌트 복잡도를 낮추고, 재사용성을 높여야한다.

 

const Input = ({ onChangeHandler }) => {
    const onChangeHandler = (e) => {
        const { value } = e.target;
        setValue(value);
    };
    return <input onChange={onChangeHandler} />;
};

이와 같은 코드는 재사용성을 높여야할 input컴포넌트의 onChange를 setValue라는 행동으로 묶으므로 올바르지 않은 컴포넌트 사용이다.

 

const Input = ({ HandleonChange }) => {
    return <input onChange={HandleonChange} />;
};

이와 같이 어떤 특정 동작에 묶이지 않고 여러 군데에서 재사용이 가능하도록 만들어야 올바른 컴포넌트 사용이라고 생각한다.

 

또한 다른 개발자가 컴포넌트를 쉽게 이해하기위해 컴포넌트 속성과 return을 일관되게 작성해야한다.

 

2. JSX 문법

JavaScript eXtensible markup Language 를 줄여 JavaScript XML 을 한번 더 줄여 JSX 문법을 리액트에서 사용한다.

XML, extensible markup language는 AWS에서 다음과 같이 말한다.

XML이란 무엇인가요? - AWS

 

JSX는 결국 자바스크립트를 확장한 문법이다. 몇가지 규칙을 따르면 react에서 사용이 가능하다.

1. 모든 태그는 닫혀있어야 한다.

2. 최상위 요소는 하나여야 한다.

3. camelCase 속성명을 사용한다.

4. 중괄호를 사용하여 자바스크립트 표현식을 사용한다.

5. 조건부 렌더링은 if또는 삼항연산자를 사용한다.

6. 인라인 스타일은 style={{}}으로 적용한다.

7. 주석 작성은 {/*~~~*}을 사용한다.

출처 - 나 / 참조 - 요즘 IT

 

3. Virtual DOM

리액트에서 컴포넌트의 최초 렌더링 시점에 Virtual DOM트리를 생성하고, 상태나 속성의 변경이 있을 때 비교와 조정절차를 통해 변경된 부분만 실제 DOM과 비교하여 반영한다. 이를 통해 불필요한 DOM 조작을 최소화 한다.

출처 - Medium

예를들어 우리가 바닐라 자바스크립트에서 id가 title인 저 h1태그에 접근하여 style값을 변경한다고 가정해보자.

const $title = document.getElementById('title')
$title.style.display = 'flex'
$title.style.color = 'red'
		.
        .
        .

이렇게 직접 돔 요소에 접근해서 해당 요소의 속성을 고쳐야 한다.

 

브라우저는 HTML을 탐색해 해당 요소를 찾고, 해당 요소와 자식 요소들을 DOM에서 제거한다. 이후 새롭게 수정된 요소로 이를 교체하는데, css는 이 과정 이후 다시 계산하여 레이아웃을 수정하는 것이다.

DOM조작은 트리에 있는 정보를 업데이트시켜주고, 빠른 알고리즘을 사용한다면 무리가 있는 작업은 아니지만, 이를 반복적으로 수행하면 충분히 무리를 줄 수 있다.

따라서 Virtual DOM이 등장하게 되었다.

 

리액트는 두개의 Virtual DOM을 가지고 있다.

1. 렌더링 이전 화면 구조를 나타내는 Virtual  DOM

2. 렌더링 이후 보이게 될 화면 구조를 나타내는 Virtual  DOM

State가 변경될 때마다 리렌더링이 발생하는데, 이때 새로운 내용이 담긴 Virtual DOM을 생성하게 된다.

출처 - Medium

렌더링 이전 내용의 Virtual DOM과 이후 Virtual DOM을 비교해 어떤 요소가 변했는지 비교하는 Diffing작업을 진행한다.

이는 효율적인 알고리즘이 적용되어있어 신속하에 어떤 요소에 차이가 있는지 파악할 수 있다.

리액트는 이 Diffing작업을 통해 차이가 발생한 부분만 실제 DOM에 적용을 하게 되는 것이다.

 

이 과정을 Reconcillation(재조정)이라 하는데, 이 과정은 Batch Update라는 변경된 요소를 집단화하여 한번에 실제 DOM에 적용하는 방식을 사용하여 효율적이다.

 

DOM조작에서 브라우저에 화면을 그려주는 작업이 가장 비용이 많이 발생하는데, 이를 Batch Update를 통해 한번에 실제 DOM에 적용하며 효율을 높인 것이다.

 

4. Props와 State

Props는 속성 State는 상태를 나타내는 리액트 컴포넌트에서 데이터를 관리하는 두가지 주요 개념이다

Props는 부모 컴포넌트에서 자식 컴포넌트로 전달되는 데이터를 의미한다. 일반적으로 데이터는 읽기전용이며 자식컴포넌트에서 직접적으로 수정할 수 없다.

.

import React from 'react';

const App = () => {
    const [title, setTitle] = useState('props')
    return (
        <Child title={title} />
    );
};

export default App;

const Child = (props) => {
    return <h1>{props.title}</h1>
}

부모 컴포넌트에서 title을 state의 title로 props를 내려주었다.

Child에서 해당 props를 받아 props의title을 보여주도록 한다.

 

import React, { useState } from "react";

const App = () => {
    const [title, setTitle] = useState("props");
    return <Child title={title} />;
};

export default App;

const Child = (props) => {
    props.title = "직접수정";
    return <h1>{props.title}</h1>;
};

이렇게 자식 컴포넌트에서 props로 받은 데이터를 직접수정하려하면 읽기전용 속성을 수정할수없다는 오류가 생기게 된다.

 


리액트 훅의 활용

 

1. 리액트 훅이란?

리액트 훅은 함수형 컴포넌트 내에서 다양한 리액트 내장기능을 사용할 수 있게 해주는 함수 API이다. 훅을 사용하면 로직을 간결하게 하고, 상태를 공유하고, 불필요 렌더링을 방지하여 성능을 최적화 할 수 있다.

위에서 사용했던 useState도 리액트 훅 중 하나이다.

 

2. 리액트 훅 사용 예시

1. useState

useState는 컴포넌트에서 상태를 관리하기 위한 훅이다. useState를 사용하여 초기상태값을 지정하고, 상태 값을 만들고, 상태값을 업데이트하는 함수를 만들 수 있다.

import React, { useState } from "react";

const App = () => {
    const [count, setCount] = useState(0);
    return (
        <button
            onClick={() => {
                setCount(count + 1);
            }}
        >
            {count}
        </button>
    );
};

export default App;

 

2. useRef

useRef는 컴포넌트 내에서 특정 값을 저장하고 참조할 수 있게 해준다. useRef로 생성한 ref객체는 컴포넌트 생명주기 동안 유지되며, 값이 변경되어도 컴포넌트가 다시 렌더링 되지 않는다. 주로 DOM엘리먼트에 직접 접근해야 하거나 이전 값을 저장해야 할 때 사용된다. useRef를 사용하여 저장한

값은 current 속성으로 접근할 수 있다.

import React, { useRef } from "react";

const App = () => {
    const modalRef = useRef();
    const modalHandler = () => {
        modalRef.current.showModal();
    };
    return (
        <>
            <button onClick={modalHandler}>modal Open!</button>
            <dialog ref={modalRef}>modal</dialog>
        </>
    );
};

export default App;

DOM의 dialog요소에 useRef로 생성한 modalRef를 ref값으로 넣어주고, 버튼을 클릭하면 modalRef.current의 showModal()메서드를 실행시킨다.

 

3. useEffect

useEffect는 콜백함수와 의존성 배열 두가지 인자를 가진다.

콜백함수는 useEffect는 컴포넌트가 처음 렌더링 될 때, 의존성 배열 내부의 값들 중 변경사항이 있는 값이 있을 때마다 실행된다.

import React, { useEffect, useState } from "react";

const App = () => {
    const [count, setCount] = useState(0);

    useEffect(() => {
        console.log("count값 변경!");
    }, [count]);

    return (
        <div>
            {count}
            <button
                onClick={() => {
                    setCount(count + 1);
                }}
            >
                1 더하기
            </button>
        </div>
    );
};

export default App;

count값을 의존성 배열로 가진 useEffect가 count가 변화할때마다 콜백함수를 실행하는 것을 볼 수 있다.

 

4. useMemo

useMemo는 계산량이 많은 함수의 return값을 기억하여 불필요한 중복계산을 방지하는 훅이다.

useMemo는 useEffect처럼 return값을 기억할 함수와 의존성 배열을 인자로 받는다. 의존성 배열 내부의 값이 변경되지 않는 한 이전 계산값을 재사용한다.

import React, { useState, useMemo } from "react";

const App = () => {
    const [count, setCount] = useState(0);
    const [count2, setCount2] = useState(0);
    const expensiveCalculation = useMemo(() => {
        let result = 0;
        for (let i = 0; i < count; i++) {
            for (let j = 100; j < 1000; j++) {
                result += i * j;
            }
        }
        return result;
    }, [count]);
    return (
        <div>
            <p>expensiveCalculation = {expensiveCalculation}</p>
            <p>count = {count}</p>
            <button
                onClick={() => {
                    setCount(count + 1);
                }}
            >
                count+1
            </button>
            <p>count2 = {count2}</p>
            <button
                onClick={() => {
                    setCount2(count2 + 1);
                }}
            >
                count+1
            </button>
        </div>
    );
};

export default App;

count가 의존성 배열에 들어가 있으므로 count2가 변해도 useMemo의 내부 함수가 실행되지 않는다.

 

5. useReducer

useState와 같이 컴포넌트의 상태를 관리하기 위한 훅이다. useState는 컴포넌트 내에 상태를 업데이트하는 로직을 두어야 하지만, useReducer은 상태 업데이트 로직을 컴포넌트 외부에 둘 수 있다.

이를 통해 상태 업데이트 로직을 한곳에 모아 관리할 수 있다. 특히 여러개의 상태를 관리하거나 프로젝트 규모가 큰 경우에 유용하게 사용할 수 있는 훅이다.

import React, { useReducer } from "react";

const INITIAL_STATE = { count: 0, title: "" };
const reducer = (state, action) => {
    switch (action.type) {
        case "plus":
            return { ...state, count: state.count + 1 };
        case "minus":
            return { ...state, count: state.count - 1 };
        case "setTitle":
            return { ...state, title: "Setting Title" };
        default:
            throw new Error("action Type이 적절하지 않습니다!");
    }
};

const App = () => {
    const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
    return (
        <div style={{ marginLeft: "20px" }}>
            <p>Count: {state.count}</p>
            <button onClick={() => dispatch({ type: "plus" })}>+</button>
            <button onClick={() => dispatch({ type: "minus" })}>-</button>
            <p>Title: {state.title}</p>
            <button onClick={() => dispatch({ type: "setTitle" })}>titleBtn</button>
        </div>
    );
};

export default App;

useReducer 훅을 사용하기 위해서는 Reducer함수와 Dispatch함수가 필요하다.

Reducer함수는 state와 action객체를 인자로 받아 새로운 상태를 반환하며, Dispatch함수는 action 객체를 인자로 받아 Reducer함수를 호출하게 된다.

 

3. 커스텀 훅 만들기

커스텀 훅은 개발자가 직접 만들어 사용하는 훅이다. 컴포넌트 간에 중복되는 로직을 제거하여 코드의 가독성을 높일 수 있다.

커스텀훅을 만들 때에는 use로 시작하는 함수명을 사용해야하며, 내부에 useEffect, useState ... 와 같은 리액트 훅을 사용해야한다.

또한 컴포넌트 최상위 레벨에서만 호출해야 한다. 이유는 리액트가 컴포넌트 렌더링 시 훅이 동일한 순서로 호출될 것이라고 가정하기 때문이다. 만약 반복문이나 조건문 안에서 훅을 사용하면 호출 순서가 일관되지 않아 리액트가 정상적으로 동작하지 않을 수 있다.

import React from 'react';

const App = () => {
    if (Math.random() > 0.5) {
        const [count, setCount] = useState(0)
    }
    for (let i = 0; i<something.length; i++) {
        useEffect(()=>{
            console.log(123132123)
        }, [deps])
    }
    return (
        <div>
            
        </div>
    );
};

export default App;

 

또한 훅을 과도하게 사용하면 안된다. 훅을 과도하게 필요없는 곳에서까지 사용하면 코드의 복잡성을 증가시킬 수 있기 때문이다. 상태와 로직을 추상화하여 불필요한 코드를 추가하지 않도록 해야하고, 의존성 배열리 있는 곳은 명시적으로 나타내어 불필요한 렌더링을 방지해야 한다.