[태그:] 리액트

  • Context API vs 상태관리 라이브러리

    상태관리는 왜 필요한가?

    리액트는 기본적으로 단방향 데이터 흐름을 가진다.

    부모 → 자식으로 props로 전달하며 UI를 구성하는데, 앱이 커질수록 이 구조에서 문제가 발생한다.

     

    여러 컴포넌트가 같은 데이터를 필요로 하거나, 중첩 전달이 반복되고 이벤트가 깊은 자식에서 발생해 상위 컴포넌트에 영향을 주는 등의 상황을 마주하게 되면 컴포넌트 간 데이터 공유를 쉽게 만드는 구조가 필요하다.

    → 상태 관리 라이브러리 도입의 필요성

     

    Context API

    리액트에서 기본으로 제공하는 전역 상태 컨텍스트로, props drilling(중첩 전달)을 줄이기 위해 설계되었다.

    리액트 내장 기능으로 편하게 사용 가능(createContext, useContext)하며, Provider → Consumer 구조로 전역 데이터를 공유한다.

    const ThemeContext = createContext();
    function App() {
      return (
        <ThemeContext.Provider value="dark">
          <Toolbar />
        </ThemeContext.Provider>
      );
    }
    function Toolbar() {
      const theme = useContext(ThemeContext);
      return <Button theme={theme} />;
    }

    장점

    • 외부 의존성 X
    • 단순한 전역 상태에 적합하다. e.g. 테마, 언어 설정 등

    단점

    • 상태가 자주 바뀌면 Provider 전체가 리렌더링 된다.
    • 규모가 커지면 성능 저하 및 관리 복잡도 증가.

     

    Redux

    상태의 중앙집중 관리 모델.

    Redux는 모든 상태를 하나의 Store에 보관, state를 오직 action과 reducer를 통해서만 수정.

    const initialState = { count: 0 };
    function counterReducer(state = initialState, action) {
      switch (action.type) {
        case 'increment':
          return { count: state.count + 1 };
        default:
          return state;
      }
    }

    장점

    • 명확한 상태 추적: action log, time travel debug 가능.
    • 대규모 팀, 복잡한 상태 흐름에서 일관성 유지 용이.
    • 미들웨어를 통한 확장성(redux-thunk, redux-saga 등).

    단점

    • 보일러플레이트 코드가 많음(action, reducer, dispatch 반복)
    • 초기 진입장벽이 높고, 단순 앱에는 과한 구조.

    → 명확한 구조와 예측 가능성이 필요한 대규모 프로젝트에 적합함.

     

    Zustand

    Redux의 철학을 유지하며 코드 복잡도를 최소화한 라이브러리.

    Hook 기반으로 Store하며 Context나 Provider 불필요,

    React의 리렌더링 성능을 최적화했다.

    const useStore = create((set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }));

    장점

    • 매우 간단한 API
    • 부분 구독(selectors)으로 불필요한 리렌더링 방지
    • Immer, persist(스토리지 저장) 등 기본 지원.

    단점

    • Redux처럼 상태 변경 추적(logging, time travel) 기능은 부족.
    • 너무 자유도가 높음 → 일관된 패턴 유지 어려움

    → 단일 페이지나 컴포넌트 중심의 중간 규모 프로젝트에 이상적.

     

    Recoil

    React에 최적화된 의존성 기반 상태 관리.

    Facebook이 React 전용으로 설계한 상태 그래프 기반 라이브러리로,

    각 상태 단위를 atom으로 관리하며 selector를 통해 파생 상태(derived state)를 계산한다.

    const countState = atom({ key: 'count', default: 0 });
    const doubleCount = selector({
      key: 'doubleCount',
      get: ({ get }) => get(countState) * 2,
    });

    장점

    • useState처럼 자연스러운 사용성
    • atom 간 의존성 자동 관리 → 부분 리렌더링 최소화
    • React Suspense, concurrent feature와 호환.

    단점

    • 조금 불안정한 상태.
    • 디버깅/도구 지원이 Redux만큼 성숙하지 않음.

    → React 내부 동작과 밀접하게 통합, 차세대 React 환경과 잘 맞음

     

    그래서 뭘 써야 할까?

    구분특징적합한 경우
    Context API리액트 기본 내장, 단순한 전역 데이터 공유다크 모드, 로케일, 간단한 설정 값
    Redux중앙집중 + 명확한 상태 흐름, 미들웨어 풍부대규모 SPA, 협업이 많은 프로젝트
    ZustandHook 기반, 코드 간결, 빠른 성능스타트업, MVP, UI 상태 중심 앱
    Recoilatom/selector 기반, React 친화적React 18+ 기반 신규 프로젝트
  • React state

    1. state

    컴포넌트 안에서 사용되는 이벤트에 의해 변경이 일어나는 동적인 값.

    값을 변경할 수 있으며 반드시 setState를 사용하여 변경한다.

    ※ state 사용할 때 주의사항

    ❗state에 저장되는 객체는 반드시 초기화(생성자에서 초기화)

    ❗state 값을 임의로 직접 변경하지 말 것, 반드시 state 값 변경시 setState() 사용.

    직접 변경하면 render 함수가 호출되지 않아 리렌더링 발생 X

    1) 사용법

    (1) 기본 사용: useState

    ① import useState

    import { useState } from 'react';

    ② declare useState()

    • useState 기본형태
    const [state, setState] = useState(initialState);
    • state: 현재 상태
    • setState: 상태를 변경하는 setState 함수 리턴
    • initialState: 상태 초기값. 생략 가능

    (2) state 값 변경: setState()

    setState() 함수를 호출하여 state 값을 변경하면 자동으로 render 함수를 호출

    다음과 같은 예시를 통해 useState와 setState 사용:

    • App.js: Increase를 클릭할때마다 1씩 증가
    import React, { useState } from 'react';
    
    const App = () => {
        const [num, setNumber] = useState(0);//초기값 0const onIncrease = () => {
            setNumber(num + 1);
        }
        return(
            <div>
                <h1>{num}</h1>
                <button onClick={onIncrease}>Increase</button>
            </div>
        );
    }
    
    export default App;

    🌟setState() 함수는 비동기 처리, 즉 작업 순서대로가 아닌 이벤트에 따라 처리.

    어떤 작업에서 시간이 오래 소요되면 해당 작업이 완료될때까지 기다려주지 않고 다음 작업을 실행…

    즉 작업의 실행 순서를 보장하지 않는다.

    🌟 setState() 함수의 인자로 함수를 전달하면 이전 state 값을 쉽게 가져올 수 있음

    state를 변경한 후 변경된 state를 사용해야 할 경우(state의 업데이트가 이전의 값에 의존):

    • Counter.js: Increase, Decrease
    import React, { useState } from 'react';
    
    function Counter() {
        const [number, setNumber] = useState(0);
    
        const onIncrease = () => {
            setNumber(preNumber => preNumber + 1);
        }
    
        const onDecrease = () => {
            setNumber(preNumber => preNumber - 1);
        }
    
        return(
            <div>
                <h2>{number}</h2>
                <button onClick={onIncrease}>Increase</button>
                <button onClick={onDecrease}>Decrease</button>
            </div>
        );
    }
    
    export default Counter;

    (3) state 값 변경: forceUpdate()

    forceUpdate() 함수를 사용하면 화면을 강제로 새로 고침하여 render() 함수 호출

    인스턴스 변수와 화면이 변경되어 출력됨.

    • ForceUpdate.js
    import React, { Component } from 'react';
    
    class ForceUpdate extends Components {
        constructor(props) {
            super(props);
            this.state = {
                stateString: 'react',
            }
        }
        StateChange = () => {
            this.state.stateString = 'React';
            this.forceUpdate();
        }
    
        render() {
            return(
                <div>
                    <button onClick={this.stateChange}>forceUpdate</button>
                    {this.state.stateString}                // react에서 React로 변경
                </div>
            );
        }
    }
    
    export default ForceUpdate;

    ※ forceUpdate()를 사용하는 것은 리액트 성능에 제약이 있으므로

    매번 화면을 새로 출력해야 하는 경우가 아니면 가급적 사용하지 않도록 함

  • React 기초

    1) 리액트의 특징

    (1) 컴포넌트(Component)

    재사용이 가능한 각각의 작고 독립적인 모듈. 마치 레고 블럭…

    이미 만들어진 컴포넌트들을 조합하여 화면을 효율적으로 구성하는 것이 리액트의 특징.

    🌟 컴포넌트의 구성요소

    • 프로퍼티(Props)
    • state
    • 컨텍스트(Context)

    (2) 가상돔(Virtual DOM)

    ✍🏻 브라우저에서 화면을 렌더링하는 과정

    서버로부터 받는 파일을 파싱하여 DOM Tree와 CSSOM Tree를 만들고 결합 : 렌더트리(Render Tree) 생성.

    생성한 Render Tree로  Layout을 계산하고 만든 후, 이 요소들을 실제로 화면을 그리는 Paint를 한다.

    DOM + CSSOM → Render Tree

    • DOM(Document Object Model) Tree

    HTML 파일을  파싱하여 구조화하여 표현한 것.

    • CSSOM(CSS Object Model) Tree

    CSS 파일을 DOM처럼 파싱하고 구조화하여 만든 것.

    DOM은 아주 작은 변경사항이 있더라도 항상 리렌더링을 하고 이때 문제점 발생

    = 화면이 커지면 커질수록 화면을 그리는 시간이 길어지는, 즉 속도가 느려지는 것.

    따라서 가상돔(Virtual DOM)을 사용.

    🌟 리액트가 가상돔(Virtual DOM)을 반영하는 절차

    ① 변경사항 발생 → 전체 UI를 Virtual DOM에 리렌더링.

    ② 이전 Virtual DOM과 현재를 비교 : 가상돔끼리 비교

    ③ 변경된 부분만 실제 DOM에 업데이트

    즉, 간단히 요약하자면:

    기존 : 화면의 일부가 수정되면 화면 전체를 업데이트.

    가상 DOM 도입 : 이전 UI에서 변경된 부분만 반영하여 업데이트

  • React props

    1. 프로퍼티(properties, props)

    상위 컴포넌트에서 하위 컴포넌트로 값을 전달할 때 사용(단방향 데이터 흐름).

    프로퍼티 값은 수정 불가능, 즉 읽기 전용 데이터.

    1) 사용법

    상위 컴포넌트에서 Props를 지정하고 하위 컴포넌트에서 받은 Props 값을 렌더링.

    • 문자열 전달: 큰 따옴표(” “) 사용
    • 숫자형, boolean 등의 값(문자열 외의 값) 전달: 중괄호( { } ) 사용

    (1) 단일값 전달

    • App.js
    import React from 'react';
    import Hello from "../src/Hello";
    
    function App() {
        return (
                <Hello name="React"></Hello>//사용할 컴포넌트 props의 name 값을 "React"로
            );
    }
    
    export default App;
    • Header.js
    import React from 'react';
    
    function Hello(props) {
        return <div>Hello, {props.name}</div>;// name 값을 조회하기 위해 props.name 이용.
    }
    
    export default Hello;

    (2) 여러값 전달

    • App.js
    import React from 'react';
    import Hello from '../src/Hello';
    
    function App() {
        return (
                <Hello name="React" color="blue"/>
            );
    }
    
    export default App;
    • Hello.js
    import React from 'react';
    
    function Hello(props){
        return(
                <div style={{ color: props.color }}>Hello, {props.name}</div>
            );
    }
    
    export default Hello;

    이런 경우 객체값을 부를 때마다 매번 props에서 불러와야 함.

    비구조화 할당 방식으로 코드를 바꾸면:

    import React from 'react';
    
    function Hello({name, color}){
        return(
                <div style={{ color: color }}>Hello, {name}</div>//동일하게 <div style={{ color }}Hello, {name}</div>
            );
    }
    
    export default Hello;

    (3) 기본값 설정: defaultProps

    • Hello.js
    import React from 'react';
    
    function Hello( {color, name} ) {
        return <div style={{ color }}>Hello, {name}</div>;
    }
    
    Hello.defaultProps = {
        name: 'noname'//defaultProps로 기본값 설정.
    }
    
    export default Hello;

    (4) 컴포넌트 태그 내부의 값 조회: props.children

    예를 들어, 스타일 정의된 파일 Wrapper.js를 App.js의 컴퍼넌트로 추가한다면,

    • Wrapper.js
    import React from "react";
    
    function Wrapper() {
        const style = {
            border: '2px solid black',
            padding: 20
        };
        return <div style={style}><div>
    }
    
    export default Wrapper;
    • App.js
    import React from "react";
    import Hello from "./Hello";
    import Wrapper from "./Wrapper";
    
    function App() {
        return (
            <Wrapper>
                <Hello name="React" color="blue"></Header>
            </Wraaper>
        );
    }
    
    export default App;

    하지만 이렇게 작성된 Wrapper 컴포넌트를 App.js에 추가하면 Wrapper 태그 내부에서 작성한 값들을 출력 X

    이럴 때, {children}을 사용하여 태그 내부를 보이도록 만들어주도록 한다.

    따라서 Wrapper.js를 다음과 같이 수정한다:

    • Wrapper.js
    import React from 'react';
    
    function Wrapper({ children }) {
        const style = {
            border: '2px solid black',
            padding: 20
        };
    
        return (
            <div style={style}>
                {children}
            </div>
        )
    }
    
    export default Wrapper;
  • React CRA

    1. CRA(create-react-app)

    React 개발 환경을 쉽게 구축해주는 도구.

    💻 CRA를 통해 자동으로 구축되는 요소:

    • webpack: 모듈 번들러
    • babel: JSX를 JavaScript로 컴파일
    • jest: 기능 테스트
    • eslint: 코드 교정 및 스타일 맞추기(형상관리)
    • polyfill: 구형 브라우저에서 지원하지 않는 문법(기능) 지원
    • HMR(Hot Module Replacement): reload 없이 변경사항 반영
    • CSS 후처리: sass 사용시 CSS 컴파일, 구형 브라우저에는 Vendor 접두사(perfix) 필요

    ※ 단점: webpack, babel, eslint 등 설정 변경하기 어려움.(설정을 변경할 경우 eject)

    1. CRA 사용
    npx create-react-app 프로젝트폴더명

    🌟 CRA는 서버사이드 렌더링(SSR)을 지원하지 않는다

    CRA에 eject하여 webpack과 babel을 설정하면 서버사이드 렌더링을 구현할 수는 있으나 효율적이지는 않으므로

    서버사이드 렌더링이 필요하면 SSR을 기본적으로 제공하는 Next.js를 사용하는 것을 권장.

    1. CRA scripts

    🌟package.json

      "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test",
        "eject": "react-scripts eject"
      }

    1) start

    • React 개발 서버 구동.
    • 기본 http로 실행

    ✍🏻 https 실행 원하는 경우

    • CMD
    set HTTPS=true && npm start
    • Power Shell
    ($env:HTTPS = "true") -and (npm start)
    • Linux, MacOS
    HTTPS=true npm start

    2) build

    • 실제 서버 배포시 사용.
    • 빌드시 정적파일 생성(/build 파일)
    • 서버에 별도의 애플리케이션 실행 없음

    3) test

    • 테스트 실행
    • 기본으로 존재하는 App.test.hs 파일 테스트 실행.
    • 파일이름.test.js, 파일이름.spec.js 형식 테스트 파일이면 인식 가능하여 테스트실행됨.

    4) eject

    • react-scripts를 사용하지 않고 모든 설정파일 추출
    • CRA를 기본으로 직접 개발 환경 설정 및 구축하고 싶을 경우 사용
    • 한 번 실행하면 되돌릴 수 없음
    1. 환경변수

    1) NODE_ENV 환경변수

    CRA에서 기본적으로 가지고 있는 환경변수, 실행 명령어에 따라 자동으로 NODE_ENV 값이 정해짐.

    process.env.NODE_ENV

    (1) 개발 환경: development

    npm start로 실행할 경우

    (2) 테스트 환경: test

    npm test로 실행할 경우

    (3) 배포 환경: production

    npm run build로 실행할 경우

    2) 새로운 환경변수 설정

    React 환경 변수명은 생성시 REACTAPP 으로 시작.

  • React Component

    1. 컴포넌트(Component)

    재사용이 가능한 각각의 작고 독립적인 모듈.

    리액트로 개발한 앱을 이루는 가장 작은 단위, 조각.

    레고블럭으로 비유할 때, 레고블럭으로 만든 집은 리액트 앱으로 보고

    집을 구성하는 하나의 작은 블록들은 컴포넌트라고 할 수 있음.

    1) 왜 컴포넌트인가

    기존 웹 프레임워크는 MVC(Model, View, Controller) 방식으로 분리하여 관리.

    따라서 각 요소들이 독립적이지 않고 의존성이 높아서 재사용이 어려움.

    → 컴포넌트는 View를 독립적으로 구성하여 재사용 가능!

    2) 컴포넌트 사용

    컴포넌트의 이름은 항상 대문자로 시작.

    ※ React에서 소문자로 시작하는 컴포넌트는 DOM 태그로 인식함.

    🌟 컴포넌트의 구성요소

    • 프로퍼티(Props)
    • state
    • 컨텍스트(Context)
    1. 컴포넌트의 선언방식

    1) 함수형 컴포넌트(Functional Component)

    • state와 LifeCycle API 사용이 불가능했으나 Hook 기능으로 사용 가능 .
    • 클래스형 컴포넌트보다 선언하기 편함.
    • 클래스형 컴포넌트보다 메모리 자원을 덜 사용함

    (1) 함수형 컴포넌트 선언

    import React from 'react';
    
    const App = () => {
        return(
            <div></div>
        );
    }
    
    export default App;

    2) 클래스형 컴포넌트(Class Component)

    • state와 LifeCycle API 사용 가능.
    • 임의 메서드 정의 가능.

    (1) 클래스형 컴포넌트 선언

    • class 키워드 필요
    • Component를 상속 받아야 함.
    • render() 메소드 반드시 필요.
    import React, { Component } from 'react';
    
    class App extends Component {
        render() {
                return(
                <div></div>
                );
        }
    }
    
    export default App;

    ※ 클래스 컴포넌트로 개발을 진행한 프로젝트의 유지보수를 위해 알아둘 것

  • React 실전 가이드: 입문

    React의 세계에 오신 것을 환영합니다! 이 가이드는 React를 처음 시작하는 분들을 위해 만들어졌습니다. 가장 기본적인 개념부터 차근차근 알아보고, 직접 코드를 작성하며 React와 친해져 봅시다.

    목차

    1. React, 왜 배워야 할까요?

      • React란?
      • React의 핵심 장점
    2. 첫 React 프로젝트 시작하기

      • 개발 환경 준비
      • create-react-app으로 프로젝트 생성
    3. React의 핵심 문법 3가지

      • JSX: JavaScript와 HTML을 한번에
      • 컴포넌트: 재사용 가능한 UI 조각
      • Props와 State: 데이터를 다루는 방법

    4. 간단한 예제: 나만의 카운터 만들기



    1. React, 왜 배워야 할까요?

    1.1. React란?

    React는 페이스북에서 개발한 사용자 인터페이스(UI)를 만들기 위한 JavaScript 라이브러리입니다. 웹사이트의 ‘보이는 부분’을 쉽고 효율적으로 만들 수 있도록 도와주는 도구라고 생각하면 됩니다.

    1.2. React의 핵심 장점

    • 컴포넌트 기반: 레고 블록처럼 UI를 여러 개의 독립적인 ‘컴포넌트’로 나누어 만듭니다. 덕분에 코드를 재사용하기 쉽고, 유지보수가 편리해집니다.
    • 빠른 속도: 가상돔(Virtual DOM)을 사용하여 변경된 부분만 실제 화면에 업데이트하므로, 불필요한 작업을 줄여 빠른 성능을 보여줍니다.
    • 선언형 코드: “이렇게 저렇게 해서 이걸 만들어줘”가 아니라, “이러한 상태일 때, 화면은 이 모습이어야 해”라고 선언적으로 코드를 작성합니다. 코드가 훨씬 직관적이고 예측 가능해집니다.

    2. 첫 React 프로젝트 시작하기

    2.1. 개발 환경 준비

    React 개발을 위해서는 컴퓨터에 Node.jsnpm(또는 yarn)이 설치되어 있어야 합니다. Node.js를 설치하면 npm은 자동으로 함께 설치됩니다.

    2.2. create-react-app으로 프로젝트 생성

    가장 쉬운 방법은 React 팀에서 공식적으로 제공하는 create-react-app 도구를 사용하는 것입니다. 터미널(명령 프롬프트 또는 PowerShell)을 열고 다음 명령어를 순서대로 입력하세요.

    # 1. 'my-first-react-app'이라는 이름의 React 프로젝트를 생성합니다.
    npx create-react-app my-first-react-app
    
    # 2. 생성된 프로젝트 폴더로 이동합니다.
    cd my-first-react-app
    
    # 3. 개발 서버를 실행합니다.
    npm start

    npm start 명령어를 실행하면, 잠시 후 브라우저에 React 로고가 빙글빙글 돌아가는 시작 페이지가 나타날 것입니다. 첫 React 프로젝트가 성공적으로 실행된 것입니다!


    3. React의 핵심 문법 3가지

    3.1. JSX: JavaScript와 HTML을 한번에

    JSX는 JavaScript 파일 안에서 HTML과 유사한 코드를 작성할 수 있게 해주는 문법입니다.

    // 일반 JavaScript 변수
    const name = "React";
    
    // 변수를 포함한 JSX 코드
    const element = <h1>Hello, {name}!</h1>; // 결과: <h1>Hello, React!</h1>

    주의할 점:

    • HTML의 class 속성은 JSX에서 className으로 써야 합니다. (class는 JavaScript의 예약어이기 때문입니다.)
    • 모든 태그는 반드시 닫혀있어야 합니다. (예: <br> -> <br />)

    3.2. 컴포넌트: 재사용 가능한 UI 조각

    컴포넌트는 React의 심장입니다. UI를 독립적인 단위로 쪼개어 관리할 수 있게 해줍니다. 현재는 주로 함수형 컴포넌트를 사용합니다.

    // 'Welcome'이라는 이름의 간단한 컴포넌트
    function Welcome(props) {
      return <h1>Hello, {props.name}</h1>;
    }
    
    // 컴포넌트 사용하기
    function App() {
      return (
        <div>
          <Welcome name="Sara" />
          <Welcome name="Cahal" />
          <Welcome name="Edite" />
        </div>
      );
    }

    3.3. Props와 State: 데이터를 다루는 방법


    • Props (Properties): 부모 컴포넌트가 자식 컴포넌트에게 물려주는 데이터입니다. 위 예제의 name="Sara"가 바로 props입니다. Props는 자식 컴포넌트 안에서 절대 직접 수정할 수 없습니다.



    • State (상태): 컴포넌트가 자체적으로 가지는 내부 데이터입니다. 사용자의 입력이나 시간의 흐름에 따라 변할 수 있습니다. State가 변경되면, React는 컴포넌트를 화면에 다시 그려줍니다(리렌더링). State는 useState라는 Hook을 사용해 만듭니다.



    4. 간단한 예제: 나만의 카운터 만들기

    지금까지 배운 개념을 모두 활용하여 간단한 카운터 컴포넌트를 만들어 보겠습니다. src/App.js 파일의 내용을 아래 코드로 바꿔보세요.

    import React, { useState } from 'react'; // useState를 import 합니다.
    import './App.css';
    
    function App() {
      // 'count'라는 이름의 state를 만들고, 초기값을 0으로 설정합니다.
      // setCount는 count 값을 변경할 때 사용할 함수입니다.
      const [count, setCount] = useState(0);
    
      return (
        <div className="App">
          <header className="App-header">
            {/* state인 count 값을 화면에 보여줍니다. */}
            <p>You clicked {count} times</p>
    
            {/* 버튼을 클릭하면 setCount 함수를 호출하여 count 값을 1 증가시킵니다. */}
            <button onClick={() => setCount(count + 1)}>
              Click me
            </button>
          </header>
        </div>
      );
    }
    
    export default App;

    코드를 저장하면 브라우저 화면이 자동으로 새로고침되고, ‘Click me’ 버튼이 있는 카운터가 나타날 것입니다. 버튼을 누를 때마다 숫자가 올라가는 것을 확인해보세요. 이것이 바로 State가 변경될 때 React가 화면을 업데이트하는 방식입니다.

    이제 여러분은 React의 가장 중요한 첫걸음을 내디뎠습니다. 이 기본기를 바탕으로 더 복잡하고 멋진 웹 애플리케이션을 만들어 나갈 수 있을 것입니다.

  • React 실전 가이드:중급

    React 입문 가이드를 통해 기본기를 다지셨다면, 이제 한 단계 더 나아갈 시간입니다. 이 중급 가이드에서는 애플리케이션의 성능을 최적화하고, 더 복잡한 데이터를 효율적으로 다루며, 코드의 품질을 높이는 실전적인 기술들을 다룹니다.

    목차

    1. Hooks 심화 학습: useMemouseCallback

      • 불필요한 렌더링은 이제 그만!
      • 언제 사용해야 할까?
    2. 데이터 페칭(Data Fetching) 마스터하기

      • useEffect + Axios 패턴의 한계
      • 서버 상태 관리의 구세주: React Query
    3. 성능 최적화와 React.memo

      • React.memo로 컴포넌트 렌더링 최적화하기
      • useCallback과의 시너지
    4. 컴포넌트 생명주기(Lifecycle)와 useEffect

      • useEffect로 생명주기 흉내 내기
      • Clean-up 함수: 메모리 누수 방지
    5. UI 라이브러리 활용

      • Material-UI (MUI)로 개발 속도 높이기

    1. Hooks 심화 학습: useMemouseCallback

    useStateuseEffect에 익숙해졌다면, 이제 성능 최적화를 위한 Hooks를 배울 차례입니다.

    • useMemo: 값(value)을 기억(memoization)합니다. 복잡한 연산의 결과값을 저장해두고, 의존성 배열(deps)의 값이 변경될 때만 다시 계산합니다. 이를 통해 불필요한 연산을 줄일 수 있습니다.

    • useCallback: 함수(function)를 기억합니다. 컴포넌트가 리렌더링될 때마다 함수가 새로 생성되는 것을 방지합니다. 자식 컴포넌트에 props로 함수를 내려줄 때, 불필요한 리렌더링을 막는 데 특히 유용합니다.

    import React, { useState, useMemo, useCallback } from 'react';
    
    function Calculator({ a, b }) {
      // a 또는 b가 변경될 때만 expensiveCalculation 함수가 다시 실행됩니다.
      const result = useMemo(() => expensiveCalculation(a, b), [a, b]);
    
      // 컴포넌트가 리렌더링 되어도 이 함수는 재생성되지 않습니다.
      const handleSave = useCallback(() => {
        saveResult(result);
      }, [result]);
    
      return (
        <div>
          <p>결과: {result}</p>
          <ChildComponent onSave={handleSave} />
        </div>
      );
    }

    2. 데이터 페칭 마스터하기

    2.1. useEffect + Axios 패턴의 한계

    입문 과정에서 배운 useEffectAxios를 사용한 데이터 페칭은 간단한 작업에는 유용하지만, 실무에서는 여러 한계에 부딪힙니다.

    • 로딩(loading), 에러(error) 상태를 직접 useState로 관리해야 합니다.
    • 데이터 캐싱(caching)이 없어, 같은 데이터를 여러 번 요청하게 됩니다.
    • 코드가 길고 복잡해지기 쉽습니다.

    2.2. 서버 상태 관리의 구세주: React Query

    React Query는 데이터 페칭, 캐싱, 동기화, 서버 상태 업데이트 등 비동기 작업을 매우 쉽게 만들어주는 라이브러리입니다. 보일러플레이트 코드를 대폭 줄여줍니다.

    • 주요 장점:
      • 데이터를 가져오는 로직과 UI 로직을 분리
      • 자동 캐싱 및 백그라운드 업데이트
      • isLoading, isError, data 등의 상태를 자동으로 제공
    import { useQuery } from 'react-query';
    import axios from 'axios';
    
    // 데이터를 가져오는 함수
    const fetchTodos = async () => {
      const { data } = await axios.get('https://api.example.com/todos');
      return data;
    };
    
    function TodoList() {
      // useQuery 훅 하나로 로딩, 에러, 데이터 상태를 모두 관리합니다.
      const { data: todos, isLoading, isError } = useQuery('todos', fetchTodos);
    
      if (isLoading) return <span>Loading...</span>;
      if (isError) return <span>Error fetching data</span>;
    
      return (
        <ul>
          {todos.map(todo => <li key={todo.id}>{todo.title}</li>)}
        </ul>
      );
    }

    3. 성능 최적화와 React.memo

    3.1. React.memo로 컴포넌트 렌더링 최적화하기

    React.memo는 고차 컴포넌트(HOC, Higher-Order Component)로, 컴포넌트를 감싸서 props가 변경되지 않으면 리렌더링을 방지하는 역할을 합니다.

    const MyComponent = (props) => {
      /* 렌더링 로직 */
    };
    
    // props가 변경될 때만 MyComponent가 리렌더링됩니다.
    export default React.memo(MyComponent);

    3.2. useCallback과의 시너지

    React.memo를 사용하더라도, props로 함수를 내려주는 경우 부모가 리렌더링될 때마다 함수가 새로 생성되어 React.memo가 무용지물이 될 수 있습니다. 이때 useCallback으로 함수를 기억시켜주면, React.memo가 제대로 동작하여 최적화 효과를 극대화할 수 있습니다.

    4. 컴포넌트 생명주기(Lifecycle)와 useEffect

    useEffect의 두 번째 인자인 의존성 배열(deps)을 어떻게 조작하느냐에 따라 클래스형 컴포넌트의 생명주기 메서드들을 흉내 낼 수 있습니다.

    • componentDidMount: useEffect(() => { ... }, []) (빈 배열)
    • componentDidUpdate: useEffect(() => { ... }, [dep1, dep2]) (의존성 지정)
    • componentWillUnmount: useEffect(() => { return () => { ... } }, []) (Clean-up 함수 반환)

    Clean-up 함수: 메모리 누수 방지

    컴포넌트가 사라질 때(unmount) 실행되는 return 함수는 매우 중요합니다. setInterval, setTimeout이나 외부 라이브러리 구독 등을 설정했을 때, 컴포넌트가 사라지기 전에 이를 해제하지 않으면 메모리 누수가 발생할 수 있습니다.

    useEffect(() => {
      const timerId = setInterval(() => {
        console.log('Tick');
      }, 1000);
    
      // 컴포넌트가 언마운트될 때 타이머를 정리합니다.
      return () => {
        clearInterval(timerId);
      };
    }, []);

    5. UI 라이브러리 활용

    모든 UI 컴포넌트를 직접 만드는 것은 비효율적일 수 있습니다. Material-UI(MUI), Ant Design, Chakra UI 같은 UI 라이브러리는 잘 디자인되고 검증된 컴포넌트들을 제공하여 개발 속도를 비약적으로 향상시켜 줍니다.


    이제 여러분은 React를 좀 더 깊이 있게 이해하고, 더 견고하고 성능 좋은 애플리케이션을 만들 준비가 되었습니다. 다음 단계로는 전역 상태 관리(Global State Management) 라이브러리인 ReduxZustand 등을 학습해보는 것을 추천합니다.

  • React 실전 가이드: 고급

    React의 기본과 중급 과정을 마스터한 당신, 이제는 프로덕션 레벨의 애플리케이션을 구축하고 최적화하는 고급 기술을 익힐 차례입니다. 이 가이드에서는 상태 관리 아키텍처, 재사용 가능한 로직 추상화, 렌더링 최적화, 그리고 프로덕션 배포 전략까지, 실무에서 마주하는 복잡한 문제들을 해결하기 위한 고급 주제들을 다룹니다.

    목차

    1. 고급 상태 관리 아키텍처

      • 클라이언트 상태 vs 서버 상태
      • React Query 심화: Mutations, Invalidation, Optimistic Updates
      • 전역 상태 관리: Zustand, Recoil, Redux Toolkit
    2. 재사용성을 극대화하는 Custom Hooks

      • 나만의 Hook 만들기
      • Custom Hooks 디자인 패턴
    3. 렌더링 최적화의 정점

      • React.lazySuspense를 이용한 코드 분할(Code Splitting)
      • 동시성(Concurrency) 렌더링: useTransitionuseDeferredValue
    4. 견고한 애플리케이션 설계

      • 디자인 패턴: 제어 컴포넌트 vs 비제어 컴포넌트
      • 폴더 구조: 대규모 애플리케이션을 위한 아키텍처
    5. 프로덕션 준비와 배포

      • 환경 변수 관리
      • Docker를 이용한 컨테이너화

    1. 고급 상태 관리 아키텍처

    1.1. 클라이언트 상태 vs 서버 상태

    현대 React 애플리케이션의 상태는 두 가지로 나뉩니다.

    • 서버 상태(Server State): 서버 API로부터 받아오는 데이터. 비동기적이고, 원격으로 관리되며, 다른 사람에 의해 변경될 수 있습니다. (예: 게시글 목록, 사용자 정보)
    • 클라이언트 상태(Client State): UI의 현재 상태. 동기적이고, 오직 클라이언트에서만 관리됩니다. (예: 다크 모드 여부, 모달 창 열림 상태)

    이 둘을 명확히 분리하고 각기 다른 도구로 관리하는 것이 중요합니다. 서버 상태는 React Query로, 클라이언트 상태는 useState, useReducer 또는 전역 상태 관리 라이브러리로 다루는 것이 효율적입니다.

    1.2. React Query 심화

    useQuery를 넘어, 데이터를 변경하는 작업을 위한 고급 기능을 알아봅니다.

    • useMutation: 서버의 데이터를 생성(Create), 수정(Update), 삭제(Delete)할 때 사용합니다.
    • Query Invalidation: mutation 성공 후, 관련된 query를 무효화시켜 자동으로 데이터를 최신 상태로 다시 가져오게 하는 강력한 기능입니다. (queryClient.invalidateQueries('todos'))
    • Optimistic Updates: 서버 응답을 기다리지 않고 UI를 먼저 업데이트하여 사용자 경험을 극대화하는 기법입니다. mutation이 실패하면 원래 상태로 롤백합니다.

    1.3. 전역 상태 관리

    Props drilling을 피하고 여러 컴포넌트가 공유하는 클라이언트 상태를 효율적으로 관리하기 위해 전역 상태 관리 라이브러리를 사용합니다. Redux가 전통적인 강자였지만, 최근에는 더 간결하고 사용하기 쉬운 Zustand, Recoil, Jotai 등이 많이 사용됩니다.

    2. 재사용성을 극대화하는 Custom Hooks

    반복되는 로직을 여러 컴포넌트에서 사용해야 할 때, Custom Hook을 만들어 로직을 추상화하고 재사용할 수 있습니다. Custom Hook은 이름이 use로 시작하는 JavaScript 함수입니다.

    // 화면의 너비를 추적하는 Custom Hook 예제
    import { useState, useEffect } from 'react';
    
    function useWindowWidth() {
      const [width, setWidth] = useState(window.innerWidth);
    
      useEffect(() => {
        const handleResize = () => setWidth(window.innerWidth);
        window.addEventListener('resize', handleResize);
        return () => window.removeEventListener('resize', handleResize);
      }, []);
    
      return width;
    }
    
    // 사용법
    function MyComponent() {
      const width = useWindowWidth();
      return <p>Window width is: {width}px</p>;
    }

    3. 렌더링 최적화의 정점

    3.1. React.lazySuspense를 이용한 코드 분할

    애플리케이션이 커지면 초기 로딩 속도가 느려질 수 있습니다. React.lazy를 사용하면 컴포넌트를 동적으로 import하여, 해당 컴포넌트가 실제로 렌더링될 때까지 관련 코드의 로딩을 지연시킬 수 있습니다. Suspense는 코드가 로딩되는 동안 보여줄 fallback UI(예: 스피너)를 설정하는 데 사용됩니다.

    const OtherComponent = React.lazy(() => import('./OtherComponent'));
    
    function MyComponent() {
      return (
        <div>
          <Suspense fallback={<div>Loading...</div>}>
            <OtherComponent />
          </Suspense>
        </div>
      );
    }

    3.2. 동시성(Concurrency) 렌더링

    React 18부터 도입된 동시성 기능은 긴급하지 않은 렌더링을 잠시 중단하고, 더 중요한 사용자 입력(예: 타이핑)을 먼저 처리하여 앱의 반응성을 높입니다.

    • useTransition: 상태 업데이트를 긴급하지 않은 것으로 표시하여, UI가 멈추는 현상 없이 부드러운 전환을 가능하게 합니다.
    • useDeferredValue: 값의 업데이트를 지연시켜, 해당 값을 사용하는 컴포넌트의 리렌더링이 다른 긴급한 렌더링을 막지 않도록 합니다.

    4. 견고한 애플리케이션 설계

    4.1. 디자인 패턴

    고급 컴포넌트를 설계할 때 자주 사용되는 패턴을 이해하는 것은 중요합니다. 예를 들어, 폼(Form)을 다룰 때 상태를 React에서 모두 제어하는 제어 컴포넌트(Controlled Component) 방식과, DOM 자체에 맡기고 필요할 때만 값을 가져오는 비제어 컴포넌트(Uncontrolled Component) 방식의 장단점을 이해하고 상황에 맞게 사용해야 합니다.

    4.2. 폴더 구조

    애플리케이션이 성장함에 따라 유지보수 가능한 폴더 구조를 갖는 것이 중요합니다. 일반적인 패턴으로는 기능(feature)별로 컴포넌트, hooks, API 호출, 타입 등을 그룹화하는 Feature-based 구조가 있습니다.

    5. 프로덕션 준비와 배포

    5.1. 환경 변수 관리

    개발, 스테이징, 프로덕션 환경에 따라 다른 API 주소나 키 값을 사용해야 합니다. React(create-react-app 기준)는 .env 파일을 통해 환경 변수를 관리하는 기능을 기본적으로 제공합니다. REACT_APP_ 접두사를 사용하여 변수를 정의하고, 코드에서는 process.env.REACT_APP_API_URL과 같이 접근합니다.

    5.2. Docker를 이용한 컨테이너화

    Docker는 애플리케이션과 그 의존성들을 컨테이너라는 격리된 환경으로 패키징하는 기술입니다. Docker를 사용하면 “제 컴퓨터에서는 잘 됐는데…”와 같은 문제를 없애고, 개발부터 프로덕션까지 일관된 환경에서 애플리케이션을 실행하고 배포할 수 있습니다.


    이 가이드에서 다룬 주제들은 React를 사용하여 복잡하고 성능이 뛰어난 실제 서비스를 구축하는 데 필수적인 요소들입니다. 꾸준한 학습과 실제 프로젝트 적용을 통해 React 전문가로 거듭나시길 바랍니다.

  • React-query로 데이터 패칭 최적화하기

    React 애플리케이션에서 데이터를 가져오는 작업, 즉 데이터 페칭(Data Fetching)은 핵심적인 부분입니다. 많은 개발자가 useEffectuseState, 그리고 Axios 같은 HTTP 클라이언트를 조합하여 이 문제를 해결합니다. 하지만 이 방식은 애플리케이션이 복잡해질수록 여러 문제점을 드러냅니다.

    이 글에서는 전통적인 데이터 페칭 방식의 한계를 살펴보고, React-query가 어떻게 이 문제들을 해결하고 데이터 페칭을 최적화하는지 알아봅니다.

    목차

    1. 전통적인 데이터 페칭의 문제점
    2. 서버 상태 관리 라이브러리, React-query
    3. 핵심 API로 최적화 시작하기
      • useQuery: 데이터 조회와 캐싱의 자동화
      • useMutation: 간편한 데이터 변경
      • queryClient.invalidateQueries: 데이터 동기화의 마법
    4. 사용자 경험을 극대화하는 고급 패턴
      • Optimistic Updates (낙관적 업데이트)
    5. 결론: 왜 React-query를 선택해야 하는가?

    1. 전통적인 데이터 페칭의 문제점

    useEffectuseState를 사용해 데이터를 가져오는 코드를 생각해봅시다.

    import React, { useState, useEffect } from 'react';
    import axios from 'axios';
    
    function TodoList() {
      const [isLoading, setIsLoading] = useState(false);
      const [isError, setIsError] = useState(false);
      const [data, setData] = useState(null);
    
      useEffect(() => {
        setIsLoading(true);
        axios.get('/api/todos')
          .then(response => {
            setData(response.data);
          })
          .catch(error => {
            setIsError(true);
          })
          .finally(() => {
            setIsLoading(false);
          });
      }, []);
    
      if (isLoading) return <div>로딩 중...</div>;
      if (isError) return <div>에러 발생!</div>;
    
      // ...
    }

    이 코드에는 몇 가지 비효율적인 부분이 있습니다.

    • 보일러플레이트 코드: isLoading, isError, data 상태를 모두 직접 선언하고 관리해야 합니다. 컴포넌트마다 이 코드가 반복됩니다.
    • 캐싱의 부재: 다른 컴포넌트에서 동일한 데이터를 요청하면, 불필요한 API 호출이 다시 발생합니다.
    • 상태 동기화의 어려움: 사용자가 할 일을 추가(Create)하거나 삭제(Delete)했을 때, 목록을 최신 상태로 다시 불러오는(refetch) 로직을 수동으로 구현해야 합니다.

    2. 서버 상태 관리 라이브러리, React-query

    React-query는 자신을 “데이터 페칭 라이브러리”가 아닌 “서버 상태(Server State) 관리 라이브러리”라고 소개합니다. 서버에서 받아온 비동기 데이터를 클라이언트의 UI 상태와 분리하여, 캐싱, 동기화, 업데이트를 매우 효율적으로 관리해줍니다.

    3. 핵심 API로 최적화 시작하기

    3.1. useQuery: 데이터 조회와 캐싱의 자동화

    useQuery는 데이터 조회를 위한 기본 Hook입니다. 위의 장황한 코드를 아래와 같이 단 몇 줄로 줄일 수 있습니다.

    import { useQuery } from 'react-query';
    import axios from 'axios';
    
    const fetchTodos = () => axios.get('/api/todos').then(res => res.data);
    
    function TodoList() {
      const { data, isLoading, isError } = useQuery('todos', fetchTodos);
    
      if (isLoading) return <div>로딩 중...</div>;
      if (isError) return <div>에러 발생!</div>;
    
      // ...
    }

    useQueryisLoading, isError, data 상태를 알아서 관리해줄 뿐만 아니라, 강력한 캐싱 기능을 제공합니다.

    • useQuery('todos', ...)의 첫 번째 인자인 'todos'는 이 데이터의 고유한 쿼리 키(Query Key)입니다.
    • React-query는 이 키를 기준으로 데이터를 캐시에 저장합니다.
    • 다른 컴포넌트에서 동일한 'todos' 키로 useQuery를 호출하면, API를 다시 호출하는 대신 캐시된 데이터를 즉시 반환하여 불필요한 네트워크 요청을 막습니다.
    • stale-while-revalidate 전략을 기본으로 사용하여, 캐시된 데이터를 먼저 보여준 후 백그라운드에서 데이터를 최신으로 업데이트하여 사용자 경험과 데이터 정합성을 모두 잡습니다.

    3.2. useMutation: 간편한 데이터 변경

    데이터를 생성, 수정, 삭제할 때는 useMutation을 사용합니다.

    import { useMutation } from 'react-query';
    
    const addTodo = (newTodo) => axios.post('/api/todos', newTodo);
    
    function AddTodoComponent() {
      const mutation = useMutation(addTodo);
    
      const handleAddTodo = () => {
        mutation.mutate({ title: '새로운 할 일', completed: false });
      };
    
      // ...
    }

    3.3. queryClient.invalidateQueries: 데이터 동기화의 마법

    useMutation의 가장 강력한 기능은 다른 쿼리와의 연동입니다. 할 일이 추가된 후, 목록을 어떻게 새로고침할 수 있을까요? invalidateQueries를 사용하면 됩니다.

    import { useMutation, useQueryClient } from 'react-query';
    
    function AddTodoComponent() {
      const queryClient = useQueryClient();
    
      const mutation = useMutation(addTodo, {
        onSuccess: () => {
          // 'todos' 쿼리를 무효화시켜 자동으로 다시 가져오게 합니다.
          queryClient.invalidateQueries('todos');
        },
      });
    
      // ...
    }

    mutation이 성공하면 onSuccess 콜백이 실행되고, queryClient.invalidateQueries('todos')'todos'라는 키를 가진 useQuery를 “오래된 데이터(stale)”로 만듭니다. 해당 쿼리를 사용하는 모든 컴포넌트는 자동으로 데이터를 리เฟetch하여 UI를 최신 상태로 업데이트합니다. 더 이상 수동으로 상태를 관리할 필요가 없습니다.

    4. 사용자 경험을 극대화하는 고급 패턴

    Optimistic Updates (낙관적 업데이트)

    서버의 응답을 기다리지 않고, mutation이 성공할 것이라고 “낙관”하여 UI를 먼저 업데이트하는 기법입니다. 사용자는 즉각적인 피드백을 받게 되어 앱이 매우 빠르게 느껴집니다. useMutationonMutateonError 옵션을 사용해 구현할 수 있으며, 만약 mutation이 실패하면 이전 상태로 롤백합니다.

    5. 결론: 왜 React-query를 선택해야 하는가?

    React-query는 단순히 코드를 줄여주는 것을 넘어, 애플리케이션의 데이터 페칭 로직을 근본적으로 최적화합니다.

    • 성능 향상: 자동 캐싱과 불필요한 API 호출 방지로 뛰어난 성능을 제공합니다.
    • 개발 경험 향상: 반복적인 로딩/에러 상태 관리를 제거하고, 선언적으로 데이터를 다룰 수 있게 해줍니다.
    • 사용자 경험 향상: stale-while-revalidate, Optimistic Updates 등의 패턴으로 사용자에게 빠르고 부드러운 인터페이스를 제공합니다.

    React 애플리케이션의 데이터 페칭을 한 단계 끌어올리고 싶다면, React-query는 단연 최고의 선택입니다.