클로저(Closure)란?

    “클로저는 함수가 외부의 변수에 접근할 수 있는 기능”이다.

     즉, 내부 함수가 외부 함수의 변수에 접근하고 기억하는 것이 클로저이다.


예제 코드로 살펴보기 🔍
function outer() {
    let count = 0; // 외부 함수의 지역 변수

    return function inner() {
        count++;
        console.log(`현재 count는 ${count}입니다.`);
    };
}

const counter = outer(); // outer는 실행되고, inner가 반환됨
counter(); // 현재 count는 1입니다.
counter(); // 현재 count는 2입니다.

 

겉으로만 봤을 때는 counter를 실행하면 count가 각각 1, 1로 출력될 것 같지만 실제로는 아니다.

outer() 함수는 실행되고 사라졌지만,그 안에서 만든 inner() 함수는 count를 기억하고 있다.

왜냐하면 count가 클로저에 포함되어서 메모리에 살아있기 때문이다.


클로저의 핵심 구성
구성 요소 설명
외부 함수 (outer) 지역 변수를 선언하는 함수
내부 함수 (inner) 외부 함수의 지역 변수에 접근하는 함수
반환 내부 함수가 외부로 반환되어 사용됨
기억 외부 함수의 지역 변수를 내부 함수가 기억 (→ 클로저)

그렇다면 클로저를 왜 쓰는걸까?

 

    1. 데이터 보호 / 은닉 (캡슐화): 외부에서 직접 접근 못하게 하고, 함수로만 조작하도록 할 수 있음.

function createCounter() {
    let count = 0;
    return {
        increment() { count++; return count; },
        decrement() { count--; return count; },
    };
}

const counter = createCounter();
counter.increment(); // 1
counter.decrement(); // 0
// count에 직접 접근은 불가능

 

    2. 상태 유지:  클로저를 사용하면 상태를 기억하게 할 수 있음.

    3. 콜백, 이벤트 핸들러:  비동기 처리에서도 클로저가 유용하게 쓰임.

 

 

[주의할 점]
1) 메모리 누수: 클로저로 인해 변수가 해제되지 않고 남아있으면 메모리 낭비가 발생할 수 있다.
2) 남용 주의: 모든 함수에 클로저를 만들 필요는 없고, 필요한 상황에서만 쓰는 게 좋다.

 

요약

 

    1) 클로저는 내부 함수가 외부 함수의 변수에 접근할 수 있게 해주는 기능

    2) 함수가 자신이 만들어질 때의 환경을 기억

    3) 상태 유지, 캡슐화, 비동기 처리 등에 매우 유용

    4) 하지만 메모리 관리에는 주의해야 함

 

 

최근 기존 프로젝트를 리팩토링하면서 느낀 점이 있었다.

지금은 혼자하기 때문에 PR 단위를 크게 인식하지 않았는데 나중을 대비하여 미리 습관을 들여놓으면 좋을 것 같다는 생각을 했다.

왜냐하면 지금 올리는 PR이 최근에는 file change가 40개가 넘어가는 그런 것도 있었기에 .. (머쓱)

 

그렇다면 좋은 PR이란 무엇일까?


좋은 PR이란? 

 

 

    1. 작고 명확한 목적을 가진다

        하나의 PR은 하나의 목적(기능 추가, 버그 수정, 리팩터링 등)을 가져야 한다.

        변경 범위가 넓으면 리뷰하기 어렵고, 롤백도 힘들어지기 때문!

    2. 의미 있는 커밋 메시지

         커밋 메시지는 무엇을 왜 바꿨는지를 간단히 설명해야 한다. (예: feat: 로그인 실패 시 에러 메시지 추가)

 

    3. 충분한 설명

         PR 설명란에는 변경 이유, 구현 방식, 참고 이슈 등을 적는다.

         복잡한 경우 스크린샷이나 GIF 첨부하는 것도 좋다.

   

    4. 자동화 통과 확인

         GitHub Actions나 CI에서 lint, test 등이 통과된 PR이 바람직하다.

 

    5. 리뷰어 입장을 고려

         리뷰어가 빠르게 이해할 수 있도록 변수명, 함수명, 구조 등을 신경써야한다.


PR의 단위

 

    1. 단일 책임 원칙

         하나의 PR하나의 "일"만 하도록 구성한다. (예: 로그인 기능을 추가하는 PR은 UI 리팩터링과는 분리해야 함.)

 

    2. 리뷰 가능한 크기

         너무 많은 파일/라인을 바꾸지 말 것, 일반적으로 300줄 이하의 변경이 가장 이상적이라고 한다. (물론 프로젝트에 따라 다름)

 

    3. 점진적인 변경

         큰 기능은 작은 PR로 나눠서 순차적으로 머지하는 것이 좋다.

 

    4. 기능별 혹은 작업 단위별 브랜치 관리

         예: feature/login, fix/signup-bug, refactor/header-ui

 

 

하루에 한 문제씩은 꼭 코딩 테스트를 풀고 있는 요즘 .. 메서드의 중요성을 크게 깨닫고 있다 ㅎㅎ ..

그래서 준비해본 자바스크립트에서의 다양한 메서드 알아보기 ! 🧐

 

문자열 관련 메서드

 

1. chatAt(index): 문자열 특정 인덱스의 문자 반환

const str = "Hello";
console.log(str.charAt(1));  // "e"

 

2. split(separator): 문자열을 특정 구분자로 나누어 배열로 반환

const str = "apple,banana,orange";
str.split(",");  // ["apple", "banana", "orange"]

 

3. replace(searchValue, newValue): 문자열에서 특정 패턴을 찾아 다른 값으로 교체

const str = "Hello, world!";
str.replace("world", "everyone");  // "Hello, everyone!"

 

4. toUpperCase(): 문자열을 대문자로 변환

  + toLowerCase(): 문자열을 소문자로 변환

const str = "Hello";
str.toUpperCase();  // "HELLO"
str.toLowerCase();  // "hello"

 

5. trim(): 문자열 양옆의 공백 제거

const str = "   Hello, World!   ";
str.trim();  // "Hello, World!"

 

6. repeat(count): 문자열을 지정한 횟수만큼 반복

const str = "abc";
str.repeat(3);  // "abcabcabc"

배열 메서드

 

1. push(element): 배열의 끝에 요소 추가

const arr = [1, 2, 3];
arr.push(4);  // [1, 2, 3, 4]

 

2. pop(): 배열의 마지막 요소 제거

const arr = [1, 2, 3];
arr.pop();  // [1, 2]

 

3. unshift(element): 배열의 첫 번째에 요소 추가

const arr = [1, 2, 3];
arr.unshift(0);  // [0, 1, 2, 3]

 

4. shift(): 배열의 첫 번째 요소 제거

const arr = [1, 2, 3];
arr.shift();  // [2, 3]

 

5. join(separator): 배열의 모든 요소를 연결하여 하나의 문자열로 반환

const array = [1, 2, 3];
array.join("");  // "123"

 

6. forEach(callback): 각 요소에 대해 콜백 함수 실행

// 배열
const arr = [1, 2, 3];
arr.forEach(num => console.log(num));

 

7. map(callback): 배열의 각 요소에 함수를 적용하여 새로운 배열 반환

const arr = [1, 2, 3];
arr.map(num => num * 2);  // [2, 4, 6]

 

8. filter(callback): 배열에서 조건을 만족하는 요소만 필터링하여 새로운 배열 반환

const arr = [1, 2, 3, 4, 5];
arr.filter(num => num % 2 === 0);  // [2, 4]

 

9. reduce(callback, initialValue): 배열을 하나의 값으로 축소

const arr = [1, 2, 3, 4];
arr.reduce((acc, num) => acc + num, 0);  // 10

 

10. sort(compareFunction): 배열을 정렬

const arr = [3, 1, 2];
arr.sort((a, b) => a - b);  // [1, 2, 3]

문자열과 배열 모두 사용할 수 있는 메서드

 

1. includes(searchValue): 특정 값이 포함되어 있는지 확인

const str = "Hello, world!";
const array = [1, 2, 3];

console.log(str.includes("world"));  // true
console.log(str.includes("Hi"));     // false

console.log(array.includes(2));      // true
console.log(array.includes(5));      // false

 

2. indexOf(searchValue):  특정 값의 첫 번째 위치를 반환

  + lastIndexOf(searchValue): 특정 값의 마지막 위치를 반환

const str = "Hello, world!";
const array = [1, 2, 1, 3];

str.indexOf("o");      // 4
str.lastIndexOf("l");  // 10

array.indexOf(1);      // 0
array.indexOf(1);      // 2

 

3. slice(startIndex, endIndex): 특정 부분을 잘라내어 반환

     * endIndex는 포함되지 않음.

const str = "Hello, world!";
const array = [1, 2, 3, 4];

str.slice(0, 5);  // "Hello"
array.slice(0, 2);  // [1, 2]

 

4. concat(value1, value2, ...): 여러 문자열이나 배열 결합

const str1 = "Hello";
const str2 = "World";
str1.concat(" ", str2);  // "Hello World"

const arr1 = [1, 2];
const arr2 = [3, 4];
arr1.concat(arr2)  // [1, 2, 3, 4]

문자열 메서드 의미 배열 메서드 의미 배열 메서드 의미
chatAt() 특정 인덱스의 문자 반환 push() 마지막 요소 추가 forEach() 각 요소에 대해 콜백 함수 실행
split() 특정 구분자로 나누어 배열로 반환 pop() 마지막 요소 제거 map() 각 요소에 함수를 적용하여
새로운 배열 반환
replace() 특정 값을 다른 값으로 교체 unshift() 첫 번째 요소 추가 filter() 조건을 만족하는 요소만
필터링하여 새로운 배열 반환
toUpperCase()
toLowerCase()
대문자로 변환
문자로 변환
shift() 첫 번째 요소 제거 reduce()  배열을 하나의 값으로 축소
trim() 양옆의 공백 제거 join() 모든 요소를 연결하여 하나의 문자열로 반환 sort() 배열 정렬
repeat() 지정한 횟수만큼 반복  
문자열 + 배열 공통 메서드 의미
includes() 특정 값이 포함되어 있는지 확인
indexOf()
lastIndexOf()
특정 값의 첫 번째 위치를 반환
특정 값의 마지막 위치를 반환
slice(startIndex, endIndex) 특정 부분을 잘라내어 반환
concat(value1, value2, ...) 여러 문자열이나 배열 결합

객체 관련 메서드

 

1. Object.keys(): 객체의 key를 배열로 반환

const obj = { a: 1, b: 2, c: 3 };
const keys = Object.keys(obj);
console.log(keys);  // ["a", "b", "c"]

 

2. Object.values(): 객체의 value를 배열로 반환

const obj = { a: 1, b: 2, c: 3 };
const values = Object.values(obj);
console.log(values);  // [1, 2, 3]

 

3. Object.entries(): 객체의 key-value 쌍을 배열로 반환

const obj = { a: 1, b: 2, c: 3 };
const entries = Object.entries(obj);
console.log(entries);  // [["a", 1], ["b", 2], ["c", 3]]

 

4. Object.assign(): 객체를 다른 객체로 복사하거나 병합

const obj1 = { a: 1 };
const obj2 = { b: 2 };
const merged = Object.assign({}, obj1, obj2);
console.log(merged);  // { a: 1, b: 2 }

 

5. Object.freeze(): 객체를 동결하여 수정 불가하게 만들기

const obj = { a: 1 };
Object.freeze(obj);
obj.a = 2;  // 수정 불가
console.log(obj.a);  // 1

 

6. Object.seal(): 객체를 봉인하여 프로퍼티 삭제 불가하게 만들기 (수정은 가능)

const obj = { a: 1 };
Object.seal(obj);
obj.a = 2;  // 수정 가능
delete obj.a;  // 삭제 불가
console.log(obj.a);  // 2

 

7. Object.hasOwnProperty(searchValue): 객체에 특정 속성이 존재하는지 확인

const obj = { a: 1, b: 2 };
console.log(obj.hasOwnProperty("a"));  // true
console.log(obj.hasOwnProperty("c"));  // false

 

8. Object.fromEntries(): 배열이나 Map을 객체로 변환

const arr = [["a", 1], ["b", 2]];
const obj = Object.fromEntries(arr);
console.log(obj);  // { a: 1, b: 2 }

Map 관련 메서드

 

1. set(key, value): key-value 쌍을 Map에 추가

const map = new Map();
map.set("a", 1);
map.set("b", 2);
console.log(map);  // Map(2) { 'a' => 1, 'b' => 2 }

 

2. get(key): Map에서 특정 key 값을 가져옴

const map = new Map([["a", 1], ["b", 2]]);
console.log(map.get("a"));  // 1
console.log(map.get("c"));  // undefined

 

3. has(key): Map에 특정 key가 존재하는지 확인

const map = new Map([["a", 1], ["b", 2]]);
console.log(map.has("a"));  // true
console.log(map.has("c"));  // false

 

4. delete(key): Map에서 특정 key-value 쌍을 삭제

const map = new Map([["a", 1], ["b", 2]]);
map.delete("a");
console.log(map);  // Map(1) { 'b' => 2 }

 

5. clear(): Map의 모든 요소 삭제

const map = new Map([["a", 1], ["b", 2]]);
map.clear();
console.log(map);  // Map(0) {}

 

6. size(): Map의 요소 개수 확인

const map = new Map([["a", 1], ["b", 2]]);
console.log(map.size);  // 2

 

7. keys(): Map의 key들을 iterator 형태로 반환

const map = new Map([["a", 1], ["b", 2]]);
const keys = [...map.keys()];
console.log(keys);  // ["a", "b"]

 

8. values(): Map의 value들을 iterator 형태로 반환

const map = new Map([["a", 1], ["b", 2]]);
const values = [...map.values()];
console.log(values);  // [1, 2]

 

9. entries(): Map의 key-value 쌍들을 iterator 형태로 반환

const map = new Map([["a", 1], ["b", 2]]);
const entries = [...map.entries()];
console.log(entries);  // [["a", 1], ["b", 2]]

객체와 Map에 공통적인 메서드

 

1. forEach(): Map의 각 요소에 대해 콜백 함수 실행

// 객체
const obj = { a: 1, b: 2 };
Object.entries(obj).forEach(([key, value]) => {
  console.log(`${key}: ${value}`);
});

// Map
const map = new Map([["a", 1], ["b", 2]]);
map.forEach((value, key) => {
  console.log(`${key}: ${value}`);
});
[객체와 Map의 차이점 💡]
1) 객체는 주로 문자열을 key로 사용하고, 순서가 보장되지 않음.
    (비록 ES6 이후 객체는 키의 순서를 어느 정도 보장하지만 Map보다는 덜 신뢰할 수 있음)
2) Map임의의 자료형을 key로 사용 가능하고, 항상 순서를 보장하며, 성능이 더 뛰어날 때가 많음.

객체 메서드 의미 Map 메서드 의미
Object.keys() key를 배열로 반환 set(key, value) key-value 쌍을 Map에 추가
Object.values() value를 배열로 반환 get(key) 특정 key 값을 가져옴
Object.entries() key-value 쌍을 배열로 반환 has(key) 특정 key가 존재하는지 확인
Object.assign() 다른 객체로 복사하거나 병합 delete(key) 특정 key-value 쌍을 삭제
Object.freeze() 동결하여 수정 불가하게 만들기 clear() 모든 요소 삭제
Object.seal() 봉인하여 프로퍼티 삭제 불가하게 만들기 (수정은 가능) size() 요소 개수 확인
Object.hasOwnProperty() 특정 속성이 존재하는지 확인 keys() key들을 iterator 형태로 반환
Object.fromEntries() 배열이나 Map을 객체로 변환 values() value들을 iterator 형태로 반환
    entries()

key-value 쌍들을 iterator 형태로 반환
객체 + Map 공통적인 메서드 의미
forEach() Map의 각 요소에 대해 콜백 함수 실행

 

 

들어가기 앞서, React는 컴포넌트 기반의 UI 라이브러리이다.

컴포넌트란, 마치 레고 블록처럼 조립이 가능한 작은 UI 조각들이며,

컴포넌트끼리 정보를 주고받고 화면이 바뀌기도 하는데, 그때 props와 state라는 개념이 사용되는 것이다.


Props에 대해 알아보자!

Props란?

     props는 컴포넌트가 부모 컴포넌트로부터 받는 데이터 또는 설정 값을 말한다.

function Welcome(props) {
  return <h1>안녕, {props.name}!</h1>; // 화면에 "안녕, 길동!" 출력
}

<Welcome name="길동" />

 


Props의 특징
  • 읽기 전용(Read-Only): 컴포넌트는 받은 props를 변경할 수 없음.
  • 부모 → 자식 방향으로만 전달 가능
  • 컴포넌트 간의 통신 수단이 될 수 있음.
[Props는 어떤 상황에서 쓰는게 좋을까?]
1) 화면에 어떤 내용을 보여줄지 부모 컴포넌트가 결정하게 하고 싶을 때
2) 재사용 가능한 컴포넌트를 만들고 싶을 때

State에 대해 알아보자!

State란?

     state컴포넌트 자체가 저장하고 관리하는 값이다.

     사용자의 입력, 버튼 클릭, API 호출 결과 등 변화하는 값을 저장하는데 쓰인다.

import { useState } from 'react';

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

  return (
    <div>
      <p>카운트: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

State의 특징
  • 컴포넌트 내부에서만 관리
  • 값이 바뀌면 자동으로 리렌더링
  • 함수형 컴포넌트에서는 useState 훅을 사용해서 만듦.
[state는 어떤 상황에서 쓰는게 좋을까?]
1) 사용자 입력을 저장하고 싶을 때 (예: 입력 폼)
2) 버튼 클릭 횟수를 세고 싶을 때
3) 특정 UI의 표시 여부를 토글할 때
=> 한마디로 변화하는 값을 사용하고 싶을 때!

Props와 State 한눈에 비교하기 🔍
항목 Props State
데이터 소유자 부모 컴포넌트 현재 컴포넌트
수정 가능 여부 ❌ 수정 불가 (읽기 전용) ⭕️ 가능 (setState 사용)
사용 목적 외부에서 받은 데이터로 렌더링 컴포넌트 내부 동적 상태 표현
주 사용처 재사용 가능한 컴포넌트 만들기 사용자 상호작용 반영
전달 방향 부모 → 자식 (단방향) 컴포넌트 내부 전용

 

'공부 > React' 카테고리의 다른 글

[React] React에서의 렌더링 방식 알아보기 🔍  (0) 2025.04.24

 

React의 렌더링

     먼저 React는 UI를 컴포넌트 단위라는 작은 단위로 나누어서 만든다.

     그렇기 때문에 React 렌더링은 이 컴포넌트를 보고 화면에 어떻게 보여줄지 결정한 다음, 그걸 실제로 DOM 적용하는 과정을 말한다.


그럼 React의 렌더링은 어떻게 이루어질까?

1. 컴포넌트 생성 → 가상 DOM 생성

    컴포넌트를 만들면, React는 이걸 가상 DOM으로 변환한다.

function Hello() {
  return <h1>Hello, React!</h1>;
}

이런 코드가 있다면, 화면에 h1 태그의 'Hello, React!'가 표출되어야 할 것이다.

하지만 React는 이를 바로 보여주지 않고, 내부적으로 아래와 같은 가상 DOM 객체를 만든다.

{
  type: 'h1',
  props: {
    children: 'Hello, React!'
  }
}

[개념 알고가기 🔍]
1) 가상 DOM(Virtual DOM)이란?
     가상 DOM은 실제 DOM(Document Object Model)의 가벼운 복사본으로,
     React는 이걸 메모리 상에서 유지하면서 UI 변화를 먼저 여기에 반영한다.
     즉, 실제 브라우저의 DOM을 직접 만지지 않고 모의 시뮬레이션을 해보는 느낌이다.
    (DOM은 HTML을 브라우저가 이해할 수 있게 구조화한 트리 구조의 객체들을 말한다.)

     실제 DOM은 느리기 때문에 DOM을 직접 바꾸는 작업은 브라우저에 많은 부담을 준다.
     그래서 React는 직접 DOM을 건드리지 않고가상 DOM이라는 중간 단계를 둔 것이다!

2) 가상 DOM이 실제 쓰이는 방식

    ① 컴포넌트를 처음 렌더링 할 때
         JSX를 기반으로 가상 DOM 트리를 만들고,
이걸 바탕으로 실제 DOM을 생성해서 브라우저에 보여준다.

    ② 컴포넌트가 업데이트 될 때
         변경된 내용을 반영한 새로운 가상 DOM을 만들고, 이전 가상 DOM과 새 가상 DOM을 비교(diffing)하여
         바뀐 부분만 찾아서 실제 DOM에 적용한다. (최소한의 조작)


2. 실제 DOM으로 변경 (초기 렌더링)

     처음 렌더링할 때는 React가 가상 DOM을 기반으로 실제 DOM을 생성해서 브라우저에 붙여준다.

     * 이걸 마운팅(Mounting)이라고 함.


3. 상태(state)나 props 변경 → 리렌더링 발생

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

 

     위의 코드를 보면 버튼 클릭 시 setCount(1) 호출되고, React는 다시 컴포넌트를 재실행한다.

     그리고 새 가상 DOM을 만들어서 이전 가상 DOM과 비교를 시작한다.


4. Diffing (차이 비교)

 

     React는 이전 가상 DOM과 새로운 가상 DOM을 비교하여 바뀐 부분만 추려낸다. (예: <span>0</span> → <span>1</span>)

     이 과정을 Diffing Algorithm이 처리한다. (효율적으로 트리 비교)


5. Reconciliation (진짜 DOM 업데이트)

     Diffing 결과를 바탕으로 실제 브라우저의 DOM에 필요한 부분만 업데이트한다.

     전체를 다시 그리지 않고, 바뀐 부분만 갱신하기 때문에 성능이 아주 좋고 부드럽다.


[요약]

컴포넌트 구현 → 가상 DOM 생성 → (초기) 실제 DOM으로 변환 → [상태 변경 발생] → 새 가상 DOM 생성
→ 이전과 비교 (Diffing) → 차이점만 실제 DOM에 반영 (Reconciliation) → 화면 업데이트 + 후처리 (useEffect 등)

'공부 > React' 카테고리의 다른 글

[React] Props와 State란?  (0) 2025.04.25

 

var, let, const는 크게 변수의 범위(스코프) / 중복 선언 / 호이스팅으로 구분하여 비교할 수 있다.


변수의 범위로 보는 차이점

 

1. var함수 스코프(function scope)를 가지는 변수이다. 즉, 변수를 선언한 함수 내에서만 유효하고, 함수 밖에서는 접근할 수 없다.

    함수 바깥에서 선언하면 전역 변수(global variable)로 취급된다.

     * 함수 스코프에서만 가지므로 if문이나 for문 등에서는 적용이 되지 않는다.

function sayHi () { 
  var say = 'Hi';
  console.log(say);  // Hi 출력
}
console.log(say);  // Error 발생

if(true) {
  var title = 'JavaScript';
  console.log(title);  // JavaScript 출력
}
console.log(title);  // JavaScript 출력

for(var i = 1; i < 4; i++;) {
  console.log(i);  // 1부터 3까지 출력
}
console.log(i); // 4 출력

   

2. let, const블록 스코프(block scope)를 가지므로, 변수를 선언한 {} 안에서만 유효하고, 그 밖에서는 접근할 수 없다.

function sayHi () { 
  let say = 'Hi';
  console.log(say);  // Hi 출력
}
console.log(say);  // Error 발생

if(true) {
  const title = 'JavaScript';
  console.log(title);  // JavaScript 출력
}
console.log(title);  // Error 발생

for(let i = 1; i < 4; i++;) {
  console.log(i);  // 1부터 3까지 출력
}
console.log(i); // Error 발생

중복 선언

 

1. var같은 이름으로 선언된 변수를 다시 선언할 수 있다.

    중복 선언 시에는 나중에 선언된 값으로 덮어씌워진다.

var title = 'JavaScript';
console.log(title);  // JavaScript 출력

var title = 'HTML';
console.log(title);  // HTML 출력

title = 'CSS';
console.log(title);  // CSS 출력

 

2. let같은 스코프 내에서 같은 이름의 변수는 다시 선언할 수 없다.

let title = 'JavaScript';
console.log(title);  // JavaScript 출력

let title = 'HTML';
console.log(title);  // Syntax Error

title = 'CSS';
console.log(title);  // CSS 출력

 

3. const같은 스코프 내에서 같은 이름의 변수는 다시 선언할 수 없다.

    또한, 선언 후 재할당이 불가능하고, 한번 값이 할당되면 그 값을 변경할 수 없다.

    하지만 객체나 배열 같은 참조형 데이터의 경우, 객체의 속성이나 배열의 요소를 변경할 수 있다.

const title = 'JavaScript';
console.log(title);  // JavaScript 출력

const title = 'CSS' // Syntax Error
title = 'HTML'; // Type Error

const obj = { name: 'Alice' };
obj.name = 'Bob'; // 가능

호이스팅(Hoisting)

 

[호이스팅이란? 🔍]

호이스팅은 자바스크립트의 변수 선언함수 선언코드 실행 전에 끌어올려지는 현상이다.

자바스크립트는 코드를 실행하기 전에 선언을 먼저 처리하고, 그 다음에 실제 코드 실행을 처리한다는 특징이 있다.

하지만 호이스팅이 어떻게 작동하는지에 따라 다르게 동작할 수 있다.


1. var로 선언된 변수는 선언만 호이스팅되고 초기화는 되지 않는다.

    즉, 변수가 함수의 제일 위로 끌어올려지지만 값은 할당되지 않은 상태로 올라간다.

console.log(title);  // undefined 출력

var title = 'This is title';

 

2. let, const으로 선언된 변수도 호이스팅이 일어나지만, 변수 선언 전에 접근하면 ReferenceError가 발생한다.

    초기화 전까지는 Temporal Dead Zone (TDZ)에 있기 때문에 값을 참조할 수 없다.

console.log(title);  // Reference Error
console.log(name);   // Reference Error

let title = 'This is title';
const name = 'My name is name';

요약 정리
특성 var let const
범위(scope) 함수 스코프 블록 스코프 블록 스코프
재선언 가능 불가능 불가능
재할당 가능 가능 불가능
호이스팅 선언만 호이스팅 (값은 undefined) 선언 및 초기화 전까지 접근 불가 (TDZ) 선언 및 초기화 전까지 접근 불가 (TDZ)

 

 

웹 개발을 하면서 중요하다고 생각하는 것은 UI/UX인 것 같다.

그렇다면 프론트엔드에서 성능을 최적화할 수 있는 방법은 뭐가 있을까? 🤔


이미지 최적화

     문제: 큰 이미지 파일은 페이지 로딩을 느리게 만듦.

     

     [해결방법]

  • 이미지 압축: TinyPNG 같은 도구로 용량 줄이기
  • WebP 포맷 사용: JPEG/PNG보다 훨씬 가볍고 품질 좋음
  • 필요한 크기만 로딩: 너무 큰 이미지 불러오지 말고, 사용 위치에 맞는 크기로 리사이즈
<!-- 나쁜 예 -->
<img src="/images/hero-original.png" width="300" height="200">

<!-- 좋은 예 -->
<img src="/images/hero-optimized.webp" width="300" height="200" loading="lazy">

지연 로딩 (Lazy Loading)

     문제: 사용자가 안 보는 부분의 리소스를 처음부터 다 불러오면 느림.

     

     [해결방법]

  • 이미지나 컴포넌트는 필요할 때만 로딩
<!-- 이미지 지연 로딩 -->
<img src="product.jpg" loading="lazy">

<!-- React에서 컴포넌트 lazy load -->
const ProductPage = React.lazy(() => import('./ProductPage'));

DOM 조작 최소화

     문제: DOM을 자주 건드리면 렌더링 성능이 떨어짐.

 

     [해결방법]

  • 변경 사항 한 번에 적용 (ex: documentFragment)
  • React/Vue 같은 프레임워크 사용 시, state 변경 최소화
// 나쁜 예
list.forEach(item => {
  const el = document.createElement('li');
  el.textContent = item;
  document.body.appendChild(el); // 매번 DOM 수정
});

// 좋은 예
const fragment = document.createDocumentFragment();
list.forEach(item => {
  const el = document.createElement('li');
  el.textContent = item;
  fragment.appendChild(el);
});
document.body.appendChild(fragment); // 한 번만 DOM 수정

비동기 처리 활용

     문제: 모든 스크립트를 동기로 실행하면 로딩 막힘.

 

     [해결방법]

  • <script async> 또는 <script defer> 사용
  • API 요청은 fetch나 axios로 비동기 처리
<!-- 좋은 예: HTML 렌더링 끝나고 스크립트 실행 -->
<script src="main.js" defer></script>

[script 실행 순서에 관한 내용은 아래 글의 '자바스크립트 코드는 언제 실행될까?' 부분 참고!] ⤵️

https://y-flm.tistory.com/79

 

[CS] 브라우저의 렌더링 과정

브라우저의 렌더링 과정은 웹 개발자라면 필수적으로 알아야 할 지식 중 하나이다!간단히 말하자면, 우리가 작성한 HTML / CSS / JS를 브라우저가 화면에 그리는 과정을 말한다. 쉽게 이해해보기 위

y-flm.tistory.com


이 외에도 불필요 라이브러리 제거, CSS Animation 활용 등이 있고, LightHouse를 이용해서 성능을 측정할 수도 있다!


[최종 요약]

방법 효과
이미지 최적화 로딩 속도 개선
코드 최소화/압축 파일 사이즈 ↓
지연 로딩 처음 로딩 빠르게
캐싱 재방문 시 빠르게
라이브러리 정리 불필요한 용량 ↓
DOM 조작 최소화 렌더링 부드럽게
비동기 처리 브라우저 멈춤 방지
Core Web Vitals 최적화 SEO와 UX 개선

 

리액트 쿼리(React Query)란?

     React Query리액트에서 서버 상태(Server State)를 쉽게 관리할 수 있도록 도와주는 라이브러리이다.

     일반적인 상태관리 라이브러리(예: Redux, Zustand)는 클라이언트 상태(UI 상태 등)를 주로 다루는데,

     React Query는 서버에서 가져오는 데이터(API 요청 등)를 관리하는 데 특화되어 있다.

[개념 알고 가기 🔍]
1) 서버 상태란?
  - 예를 들어 GET /users, GET /posts 같은 API로 받아오는 데이터를 말한다.
     이 데이터는 서버에 있고, 클라이언트에서 가져와야 하니까 서버 상태라고 불린다.

리액트 쿼리를 사용하는 이유

 

  • 로딩 상태 관리 (loading, isFetching)
  • 에러 상태 처리 (try/catch)
  • 데이터 캐싱
  • 다시 불러오기 (Refetch)
  • 백그라운드 업데이트
  • 페이지네이션, 무한 스크롤
  • 데이터 동기화

    일반적으로 데이터 API를 가져올 때는 위의 내용들이 필요하다.

    하지만 리액트 쿼리에서는 위의 내용들을 지원해주므로 편리하게 사용할 수 있다.

기능 설명
로딩/에러 상태 관리 isLoading, isError 등 자동으로 제공
데이터 캐싱 동일한 요청에 대해 중복 호출 방지
백그라운드 리패칭 오래된 데이터는 자동으로 새로고침
쿼리 무효화 mutation 이후 관련 데이터만 정확히 다시 불러오기
윈도우 포커스시 재요청 사용자 눈에는 항상 최신 데이터처럼 보이도록
페이지네이션/무한 스크롤 쉽게 구현 가능
SSR & prefetch 지원 서버사이드 렌더링도 지원

 


따라서 리액트 쿼리를 사용한다면 다음과 같은 이점을 얻을 수 있다.

 

1. 코드 간결성

    복잡한 상태 관리 로직을 생략할 수 있고, 필요한 건 useQuery, useMutation 두 가지 훅이면 충분하다.

 

2. 성능 향상 
    중복 요청 방지(캐시), 사용자 인터랙션 시 빠르게 응답, 백그라운드 업데이트로 UX 향상

 

3. 유지보수 용이성

    비즈니스 로직과 API 관리가 분리되어 깔끔, 새로운 API 추가나 수정 시에도 일관된 방식


리액트 쿼리 쓰는 이유 정리 🔍

     1) 복잡한 서버 상태 관리간단하게 만들어줌
     2) 로딩, 에러, 캐싱, 재요청 등 필수 기능을 자동으로 처리
     3) 코드가 간결해지고 버그가 줄어듦
     4) 성능사용자 경험(UX)이 좋아짐
     5) 빠른 개발 속도, 유지보수 쉬움


 

그렇다면 이제 React-Query를 사용해보자!

설치 방법
npm install @tanstack/react-query

    * 예전 이름은 react-query였고, 지금은 @tanstack/react-query로 바뀜

 

⚠️ 리액트 쿼리를 설치할 때 리액트 버전과 호환 가능한 버전인지 확인해야한다.

React-Query 버전 호환 가능 React 버전 비고
v3.x (react-query) React 16.8 이상 오래된 버전, 이제는 권장되지 않음
v4.x (@tanstack/react-query) React 17, React 18 이상 최신 기능 대응, 현재 주로 사용되는 버전
v5 (알파/베타) React 18 이상 Suspense/Concurrent 대응 강화, 사용 가능하지만 실험적일 수 있음

설치 후 세팅

    리액트 쿼리를 사용하기 위해서는 QueryClient 만들고, Provider로 감싸줘야한다.

    * Provider를 꼭 써야 React Query 기능이 작동

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyComponent />
    </QueryClientProvider>
  );
}

리액트 쿼리 주요 기능
기능 설명
useQuery 데이터 조회 (GET)
useMutation 데이터 쓰기 (POST, PUT, DELETE 등)
isLoading, error 상태 자동 관리
refetch() 수동 재요청
invalidateQueries() 쿼리 무효화 (자동 리패칭)
캐싱 동일한 데이터 요청 방지
배경 데이터 갱신 사용자 모르게 데이터 업데이트

1. useQuery로 데이터 불러오기

  • queryKey: 이 쿼리의 고유 키. 데이터를 식별하고 캐시 관리에 사용
  • queryFn: 데이터를 실제로 가져오는 함수 (API 호출 등)
  • data, isLoading, error: 상태 자동 관리

 

[예시 코드]

import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

function MyComponent() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: () => axios.get('/api/users').then(res => res.data)
  });

  if (isLoading) return <p>로딩 중...</p>;
  if (error) return <p>에러 발생!</p>;

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

2. useMutaion으로 데이터 쓰기

 

[예시 코드] 

import { useMutation } from '@tanstack/react-query';
import axios from 'axios';

function AddUser() {
  const mutation = useMutation({
    mutationFn: (newUser) => axios.post('/api/users', newUser),
    onSuccess: () => {
      // 성공 시 users 쿼리를 다시 불러옴
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });

  return (
    <button onClick={() => mutation.mutate({ name: '홍길동' })}>
      유저 추가
    </button>
  );
}

3. 리패칭 (refetching)

데이터를 수동으로 다시 불러오고 싶을 때 사용한다.

const { data, refetch } = useQuery(...);

<button onClick={() => refetch()}>다시 불러오기</button>

4. 자동 새로고침 (Refetch Interval)

자동으로 새로고침하고 싶을 때는 useQuery에 refetchInterval 옵션을 설정한다.

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  refetchInterval: 10000, // 10초마다 자동으로 새로고침
});

5. 캐싱과 상태관리

React Query는 자동으로 데이터 캐싱을 하고, 동일한 queryKey로 요청하면 서버에 다시 요청하지 않고 캐시된 데이터를 반환한다.

  • 최초 로딩 시 서버 요청
  • 같은 컴포넌트가 다시 렌더링되면 캐시된 데이터 사용
  • 설정에 따라 일정 시간이 지나면 자동으로 다시 요청 (stale time)
[개념 알고가기 🔍]
1) staleTime이란? - 신선한 상태 유지 시간
    ① 동작 방식 (staleTime: 10000으로 설정된 경우)
         쿼리가 처음 실행되면 데이터를 받아오고, 그 시점부터 10초 동안은 stale 상태가 아님.
         이 기간 동안은 React Query가 자동으로 재요청(refetch) 하지 않음.
         10초가 지나면 데이터가 stale(오래됨)으로 간주되고, 다음 조건 중 하나가 일어나면 다시 데이터를 요청(refetch) 해
          : 컴포넌트가 다시 mount되거나, 창에 다시 포커스되거나, 네트워크가 다시 연결될 때 등

    ② 언제 유용할까?
         너무 자주 데이터를 새로 불러오지 않아도 되는 경우
         (예: 공지사항, 프로필, 상품 목록 등 자주 바뀌지 않는 데이터)

2) cacheTime이란? - 캐시 유지 시간
    ① 동작 방식 (cacheTime: 300000으로 설정된 경우)
         컴포넌트가 unmount되어도 데이터를 바로 삭제하지 않음.
         이 cacheTime이 끝날 때까지 메모리 안에 캐시로 유지
         다시 같은 쿼리가 mount되면, 서버에 요청하지 않고 캐시된 데이터를 바로 사용함.

    ② 언제 유용할까?
         - 사용자가 같은 페이지로 자주 왔다 갔다 할 때
         - 리스트 → 상세 → 다시 리스트 같은 구조에서 유용
         - 불필요한 재요청을 줄여 성능 최적화 가능

  •  staleTime이 길면 = 네트워크 요청 줄어듦, 대신 데이터는 오래됨 가능성 ↑
  • cacheTime이 길면 = 빠른 화면 전환 가능, 하지만 메모리 사용량 ↑
[간단 요약]
옵션 의미 기본 값
staleTime 데이터를 신선하다고 간주할 시간 (ms) 0 (즉시 stale 처리)
cacheTime 데이터가 사용되지 않아도 캐시에 유지되는 시간 (ms) 5분 (300000)
[추천 예시]
1) 실시간 데이터 (ex. 실시간 주가, 라이브 채팅 등) ➡️ staleTime: 0, refetchInterval 사용해서 실시간 유지
2) 일반 리스트 (ex. 게시글 목록) ➡️ staleTime: 30초~1분, cacheTime: 3~5분
3) 잘 바뀌지 않는 데이터 (ex. 유저 프로필) ➡️ staleTime: Infinity (영원히 fresh), cacheTime: Infinity (계속 보관)

자주 사용하는 옵션들
useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  staleTime: 5000, // 5초 동안은 신선한 데이터로 간주
  cacheTime: 1000 * 60 * 5, // 5분간 캐시 유지
  refetchOnWindowFocus: true, // 창 포커스될 때 refetch
});

 

+ Recent posts