호이스팅에 대해서 알아보도록 하겠습니다. 

 

호이스팅은 자바스크립트에서 변수, 함수, 클래스의 선언이 코드의 실행 전의 스코프의 최상단으로 끌어올려지는 것처럼 동작하는 현상을 말합니다. 이로 인해 코드에서 선언 위치에 상관없이 변수를 참조하거나 함수를 호출할 수 있는 것처럼 보일 수 있습니다. 

 

1. 호이스팅의 핵심원리 

자바스크립트는 코드 실행 전에 변수와 함수의 선언을 스코프의 맨 위로 끌어올립니다. 

단, 실제 값의 할당은 원래 코드에 작성된 위치에서 수행되므로 선언만 먼저 이루어진 상태가 됩니다. 

 

2. 변수의 호이스팅 

(1) var의 경우 

- var는 선언이 호이스팅되고, 초기화되지 않은 상태에서는 undefined로 초기화됩니다. 

console.log(a); // undefined
var a = 10;
console.log(a); // 10

위의 예제는 다음과 같이 해석됩니다. 

var a;          // 선언이 호이스팅됨
console.log(a); // undefined (초기화는 아직 이루어지지 않음)
a = 10;         // 값이 할당됨
console.log(a); // 10

 

(2) let과 const의 경우 

- let과 const는 선언이 호이스팅되지만, 초기화되기 전까지는 TDZ상태에 머물며 접근이 불가능합니다. 

- TDZ는 변수 선언이 스코프의 시작부터 초기화가 이루어지는 시점까지의 구간을 말합니다.  

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;

 

3. 함수의 호이스팅 

(1) 함수 선언문 

- 함수 선언문은 선언과 정의가 모두 호이스팅되므로 선언 위치에 관계없이 함수 호출이 가능합니다. 

sayHello(); // "Hello!"
function sayHello() {
  console.log("Hello!");
}

 

(2)함수 표현식 

- 함수 표현식은 변수 호이스팅의 규칙을 따릅니다.

즉, 변수의 선언만 호이스팅되며 초기화된 함수는 실제 작성된 위치에서 할당됩니다. 

greet(); // TypeError: greet is not a function
var greet = function () {
  console.log("Hi!");
};

 

4. 클래스의 호이스팅 

클래스 선언도 호이스팅되지만, 초기화되기 전에는 TDZ에 머무리므로 접근이 불가능합니다. 

const obj = new MyClass(); // ReferenceError: Cannot access 'MyClass' before initialization
class MyClass {
  constructor() {
    this.name = "Class";
  }
}

자바스크립트에서 실행 컨텍스트(execution context)와 스코프(scope)는 모두 코드 실행과 변수 접근 방식에 관련된 개념이지만 서로 다른 역할을 수행한다. 이 둘의 차이를 이해하려면 각각의 정의와 역할을 살펴보는 것이 중요하다.

 

실행컨텍스트 (execution context)

정의 

실행 컨텍스트는 자바스크립트 코드가 실행되는 환경으로 코드가 어떻게 실행되고 변수와 함수가 어떻게 메모리에 저장되고 참조되는지를 정의한다. 즉, 자바스크립트가 코드를 실행하기 위해 필요한 모든 정보를 담고 있는 객체라고 볼 수 있다. 

 

구성요소 

1. 변수 환경 

- 선언된 변수 및 함수 선언과 관련된 정보가 저장된다. 

2. 렉시컬 환경 

- 현재 실행 중인 코드가 참조할 수 있는 스코프와 관련된 정보가 저장된다. 

- 외부 렉시컬 환경에 대한 참조를 포함해 상위 스코프를 참조한다. 

3. this 바인딩 

- this 키워드가 가리키는 객체를 정의한다. 

 

실행컨텍스트의 종류 

1. 전역 컨텍스트 (Global Context)

- 스크립트가 실행될 때 가장 먼저 생성되며, 브라우저 환경에서는 window객체가 전역 컨텍스트가 된다. 

2. 함수컨텍스트 (Function Context)

- 함수가 호출될 때 생성되며, 함수 내부의 변수와 this를 관리한다. 

3. Eval컨텍스트(Eval Context)

- eval( )함수가 실행될 때 생성된다. 

 

실행컨텍스트의 동작 

실행컨텍스트는 스택 구조로 관리되며 LIFO(Last In First Out)방식으로 처리된다. 

function outer() {
  console.log("Outer Start");
  inner(); // inner 실행
  console.log("Outer End");
}

function inner() {
  console.log("Inner Function");
}

outer(); // outer 실행

 

실행과정 

1. 전역 컨텍스트가 생성되고 스택에 추가된다. 

2. outer함수 호출 시 outer함수의 실행 컨텍스트가 생성되어 스택에 추가된다. 

3. inner함수 호출 시 inner함수의 실행 컨텍스트가 생성되어 스택에 추가된다. 

4. 함수 실행이 완료되면 해당 컨텍스트는 스택에서 제거된다. 

스코프(Scope)

정의 

스코프는 변수와 함수가 접근할 수 있는 유효 범위를 의미한다. 스코프는 실행컨텍스트와 다르게 코드 구조를 기준으로 동작하며 변수의 가시성을 결정한다. 

 

스코프의 종류 

1. 전역 스코프

- 어디서든 접근 가능한 범위이다. 전역스코프에 정의된 변수는 모든 코드에서 사용할 수 있다. 

const globalVar = "I am global";

function printVar() {
  console.log(globalVar); // 접근 가능
}
printVar();

2. 지역스코프

- 함수 또는 블록 내부에서만 접근 가능한 범위이다. 

function myFunction() {
  const localVar = "I am local";
  console.log(localVar); // 접근 가능
}
myFunction();
// console.log(localVar); // 오류! 지역 변수는 함수 밖에서 접근 불가

3. 블록스코프 

- {}로 감싸진 블록 내부에서만 유효한 스코프이다. let과 const는 블록 스코프를 따른다. 

if (true) {
  let blockScoped = "I am block scoped";
  console.log(blockScoped); // 접근 가능
}
// console.log(blockScoped); // 오류! 블록 스코프 밖에서 접근 불가

 

렉시컬 스코프 

렉스컬 스코프는 코드가 작성된 위치를 기준으로 스코프가 결정되는 방식을 말한다. 자바스크립트는 렉시컬 스코프 규칙을 따르기 때문에, 함수를 어디서 호출되었는지가 아니라 함수를 어디서 선언했는지에 따라 스코프가 결정된다. 

function outer() {
  const outerVar = "I am outer";
  
  function inner() {
    console.log(outerVar); // outerVar를 참조 가능
  }

  inner();
}
outer();

'면접 준비 > javascript' 카테고리의 다른 글

[JS] 클로저란 무엇일까요?  (0) 2025.01.21
[JS]자바스크립트의 데이터 타입  (0) 2025.01.17
[JS]var, let, const의 차이 + TDZ  (0) 2025.01.17

클로저(closure)는 자바스크립트에서 함수와 그 함수가 선언된 렉시컬 환경의 조합을 의미합니다. 

즉, 클로저는 함수가 자신이 선언된 환경밖에서 호출될때에도 그 환경에 접근할 수 있도록 유지되는 특징을 말합니다. 

 

클로저는 어떻게 동작하나요? 

자바스크립트는 함수가 생성될 때 해당 함수가 선언된 위치의 렉시컬 스코프를 기억합니다. 이로 인해 함수 내부에서 사용하는 변수나 상수는 함수가 실행될 때의 스코프가 아니라 함수가 선언된 시점의 스코프를 기준으로 참조됩니다. 

 

클로저를 만드는 조건 

1. 함수 내부에 선언된 함수가 필요합니다. 

2. 내부 함수가 외부 함수의 변수에 접근해야합니다. 

 

클로저의 예제 

1. 기본적인 클로저 예제

function outerFunction() {
  let count = 0; // 외부 함수의 지역 변수

  return function innerFunction() {
    count++; // 외부 변수에 접근
    console.log(`현재 count 값: ${count}`);
  };
}

const increment = outerFunction(); // outerFunction 실행, innerFunction을 반환
increment(); // 현재 count 값: 1
increment(); // 현재 count 값: 2
increment(); // 현재 count 값: 3

여기서 중요한 점은 innerFunction이 count에 접근할 수 있다는 점입니다. 

outerFunction이 종료되었지만, 클로저 덕분에 innerfunction은 count를 기억하고 계속 접근할 수 있습니다. 

 

2. 클로저를 사용한 데이터 캡슐화 

클로저는 데이터를 외부에서 직접 수정하지 못하도록 캡슐화하는데 유용합니다. 

function createCounter() {
  let count = 0;

  return {
    increment() {
      count++;
      return count;
    },
    decrement() {
      count--;
      return count;
    },
    getCount() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount());  // 1

여기서 count는 외부에서 직접 접근할 수 없으며, 오직 반환된 객체의 메소드를 통해서만 접근할 수 있습니다. 

 

클로저의 주요 활용 사례 

1. 정보 은닉 및 데이터 캡슐화 

- 클로저를 이용하여 외부에서 접근이 불가능한 프라이빗 변수처럼 사용할 수 있습니다. 

2. 부분 적용함수 

- 함수의 일부 인자를 미리 고정하여 새 함수를 생성할 때 사용합니다. 

function multiply(multiplier) {
  return function (number) {
    return number * multiplier;
  };
}

const double = multiply(2); // multiplier = 2 고정
console.log(double(5)); // 10
console.log(double(10)); // 20

3. 이벤트 리스너 및 비동기 작업에서의 상태 유지 

- 클로저를 활용하여 이벤트 핸들러나 비동기 작업에서 상태를 유지할 수 있습니다. 

function setupButton() {
  let clickCount = 0;

  document.getElementById('myButton').addEventListener('click', function () {
    clickCount++;
    console.log(`버튼 클릭 횟수: ${clickCount}`);
  });
}

setupButton();

4. 메모리 효율적인 반복 작업 

- 클로저는 반복문에서 특정 변수 상태를 고정하여 효율적으로 작업을 수행할 수 있습니다. 

 

클로저 사용시 주의점 

1. 메모리 누수

클로저는 함수의 렉시컬 스코프를 계속 참조하므로 불필요한 클로저가 남앙 있으면 메모리 누수가 발생할 수 있습니다. 따라서 클로저가 더 이상 필요하지 않을 경우 참조를 해제해야합니다. 

2. 과도한 사용으로 코드 가독성 저하 

클로저가 많아지면 스코프 체인이 복잡해지고, 코드 가독성이 떨어질 수 있습니다. 적절한 설계를 통해 이를 방지해야합니다 .

 

자바스크립트이 데이터 타입은 크게 원시타입과 참조타입으로 나눌 수 있습니다. 각 타입은다루는 방식이 다르고 값의 저장방식이나 값을 비교할 때의 특성에 차이가 있습니다. 

원시타입 (primitive types)

원시타입은 불변성을(immutable) 가지고 있으며 변수에 직접 값이 저장됩니다. 원시 타입은 6가지로 정의됩니다.

 

 

string : 텍스트 데이터를 나타내는 타입입니다.

예시: "Hello, World!", 'JavaScript'

number: 정수와 부동소수점 숫자를 포함한 숫자 타입입니다.

예시: 42, 3.14, -1

bigint : BigInt는 정밀도가 큰 정수를 다룰 수 있는 타입입니다. number로 표현할 수 없는 매우 큰 숫자들을 표현할 수 있습니다.

예시: 1234567890123456789012345678901234567890n

boolean : 참(True) 또는 거짓(False) 값을 나타내는 데이터 타입입니다.

예시: true, false

undefined : 변수는 선언되었지만, 값이 할당되지 않은 상태일 때 자동으로 가지는 값입니다.

예시: let x; console.log(x); // undefined

null : "없음" 또는 "빈 값"을 나타내는 특별한 값입니다. 객체가 없는 상태를 나타냅니다.

예시: let y = null;

symbol : 유일하고 변경 불가능한 고유의 값을 생성하는 타입입니다. 주로 객체의 고유한 프로퍼티 키로 사용됩니다.

예시: let sym = Symbol('description');

 

undefined와 null의 차이

undefined: 값이 할당되지 않은 변수의 상태를 나타냅니다.

null: 값이 없다는 의도적인 표현입니다.

 

참조타입(Reference Types)

참조타입은 객체와 같은 복합적인 값을 나타냅니다. 원시타입과는 달리 변수에는 값 자체가 아니라 값이 저장된 메모리 주소가 저장됩니다. 참조 타입에는 다음이 포함됩니다. 

 

객체

여러 값을 키-값 쌍으로 저장하는 자료형입니다. javascript에서 객체는 다양한 속성과 매서드를 가질 수 있는 복합 데이터 타입입니다. 

let person = { name: "Alice", age: 25 };

 

배열

객체의 특별한 형태로 순서가 있는 데이터를 저장하는 자료형입니다. 배열의 요소들은 인덱스를 통해 접근합니다. 

let arr = [1, 2, 3, 4, 5];

 

함수 

함수도 객체의  일종으로 특정 작업을 수행하는 코드 블록을 나타냅니다. 

function greet(name) {
  return `Hello, ${name}!`;
}

프론트엔드에서는 변수를 다루를 때 var, let, const를 사용해서 선언합니다. 각각이 어떤 차이를 가지고 있는지에 대해서 한번 다루어 보고자 합니다. 

Var 

스코프 

var는 함수 스코프를 가집니다. 그렇기 때문에 변수가 선언된 함수 내에서만 유효합니다. 블록 스코프를 지원하지 않기 때문에 if, for등의 블록안에서 선언해도 블록 외부에서 접근이 가능합니다. 

호이스팅

var로 선언된 변수는 호이스팅되어 변수 선언이 코드의 최상단으로 끌어 올려집니다. 하지만 초기화는 호이스팅되지 않으므로 선언 전에 접근하면 undefined를 반환합니다. 

function testVar() {
  if (true) {
    var x = 10; // 블록 내부에서 선언
  }
  console.log(x); // 블록 외부에서도 접근 가능
}
testVar(); // 출력: 10

Let 

스코프

let은 블록스코프를 가집니다. 블록내에서만 유효하며 블록 외부에서는 접근할 수 없습니다. 

 

호이스팅

let으로 선언된 변수도 호이스팅 되지만 TDZ(Temporal Dead Zone)때문에 초기화 전에 접근하려 하면 ReferenceError가 발생합니다.

function testLet() {
  if (true) {
    let y = 20; // 블록 내부에서 선언
    console.log(y); // 출력: 20
  }
  console.log(y); // ReferenceError: y is not defined
}
testLet();

 

const 

스코프 

const도 let과 마찬가지로 블록 스코프를 가집니다. 

 

상수 

const로 선언된 변수는 재할당이 불가능합니다. 하지만 참조형 데이터(예: 배열, 객체)의 경우, 내부 값은 변경할 수 있습니다. 

 

호이스팅 

const도 호이스팅되지만 let과  마찬가지로 TDZ때문에 초기화 이전에 접근하면 referenceError가 발생합니다.

const z = 30;
z = 40; // TypeError: Assignment to constant variable.

const obj = { name: "Alice" };
obj.name = "Bob"; // 객체 내부의 속성은 변경 가능
console.log(obj.name); // 출력: Bob

 

결론 

const를 기본으로 사용하는 것이 좋습니다. 변수에 재할당이 필요하지 않다면 const로 선언해 안정성을 높이는 것이 좋습니다. 

let은 재할당이 필요한 경우에만 사용하는 것이 좋습니다. 

var는 사용하지 않는 것이 권장됩니다. 함수 스코프와 호이스팅으로 인해 예기치 않은 동작이 발생할 수 있습니다.

 


++ TDZ에 대해서 

TDZ(temporal Dead Zone)은 자바스크립트에서 let과 const로 선언된 변수를 초기화 전에 접근하려고 할 때 발생하는 특별한 구역을 의미합니다. TDZ는 변수 선언이 호이스팅되지만, 초기화가 되지 않은 상태에서 접근을 시도하면 에러를 발생시키는 구역입니다. 

function testTDZ() {
  console.log(a); // ReferenceError: Cannot access 'a' before initialization
  let a = 10; // 변수 'a'의 초기화
}

testTDZ();

let a=10;으로 a변수를 선언했지만 선언만 호이스팅되고 초기화는 나중에 이루어지기 때문에, console.log(a)에서 초기화 전인 TDZ구간에서 a에 접근하려 하면 ReferenceError가 발생합니다. 

 

var의 경우에는 TDZ의 영향을 받지 않습니다. var는 함수 스코프로 동작하며, 선언은 호이스팅되지만 TDZ가 적용되지 않기 때문에 초기화 전에 접근해도 undefined로 값이 반환됩니다. 

function testVar() {
  console.log(c); // 출력: undefined
  var c = 30; // 변수 c의 초기화
}

testVar();

 

그렇다면 TDZ는 왜 필요한걸까요? 

TDZ는 예기치 않은 오류를 방지하고 코드의 안정성을 높이는 데 중요한 역할을 합니다. let과 const를 사용함으로써 변수가 초기화 되기 전에 접근하는 실수를 줄이기 위해Javascript엔진이 이를 강제로 막아줍니다. 

 

React를 다시 공부하면서 state를 관리하는 중요성에 대해서 다시금 깨달았다. 

그리고 useState와 useReducer에 대한 개념을 복기하면서 이 둘의 차이에 대한 것을 명확히 짚고 넘어가야겠다고 싶었다. 

 

이 두 훅은 모두 상태를 업데이트하고 컴포넌트 리렌더링을 트리거하는 역할을 하지만 사용목적과 구조적인 차이점이 있다. 

 

useState란?

useState는 React에서 가장 기본적인 상태관리 훅으로 간단한 상태관리를 위해 사용된다. 

상태값과 이를 업데이트 하는 함수를 반환한다. 

const [state, setState] = useState(initialState);

 

특징 

1. 단순성을 가진다. 간단한 상태를 선언하고 업데이트 할 때 유용하다.

2. 상태 관리 로직이 분리되지 않는다. 상태를 업데이트하는 로직이 컴포넌트 내부에 존재하기 때문이다.

3. setState는 비동기로 작동하며, React는 효율적인 렌더링을 위해 상태 업데이트를 일괄 처리한다. 

 

예제

import React, { useState } from 'react';

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

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

useReducer란?

useReducer는 상태관리 로직이 더 복잡하거나, 여러 상태를 조합해야할 때 사용된다. reducer패턴을 사용하여 상태 업데이트 로직을 컴포넌트 외부로 분리한다. 

 

특징 

1. 상태 변경 로직이 복잡하거나 다양한 액션이 필요할 때 유용하다. 

2. 상태 업데이트 로직을 한 곳에 집중시켜 가독성과 유지 보수성을 높인다. 

3. 상태 업데이트가 동기적으로 실행된다. 

 

예제

import React, { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
}

 

useState와 useReducer의 주요 차이점 표정리

특징  useState useReducer
복잡성 간단한 상태 관리에 적합 복잡한 상태와 상태 전환 로직 관리에 적합
상태 업데이트 방식 업데이트 로직이 컴포넌트 내부에 위치 Reducer 함수로 상태 업데이트 로직을 분리
사용 사례 단순한 상태(숫자, 문자열, boolean 등) 여러 상태를 조합하거나 다양한 액션이 필요한 경우
상태 초기화 초기 상태 값을 간단히 설정 가능 초기 상태와 Reducer 함수가 필요
상태 업데이트 함수 이름 setState dispatch
코드 가독성 간단한 상태에는 더 직관적 복잡한 상태 관리에 더 체계적

 

계정 조회하기 

.GET

GET/:id엔드포인트는 다음과 같은 기능들을 제공한다. 

1. URL파라미터 유효성 검증은 id가 유요한 형식인지 확인한다. 

2. 요청자가 유효한 사용자인지 확인한다. 

3. 데이터베이스를 조회하여 특정 사용자가 소유한 계정을 검색한다.

4. 요청 결과에 따라 적절한 HTTP응답을 반환한다. 

 

URL파라미터 검증 

.get(
  "/:id",
  zValidator(
    "param",
    z.object({
      id: z.string().optional(),
    })
  ),
  clerkMiddleware(),
  async (c) => {
    const auth = getAuth(c);
    const { id } = c.req.valid("param");

- zValidator는 Zod라이브러리를 사용하여 요청된 파라미터를 검증한다. 

- z.object({ id: z.string().optional()})는 id는 선택적인 문자열로 정의되어 있고 만약 id가 제공되지 않았거나 잘못된 형식이라면 요청은 실패하게 된다. 

- c.req.valid("param")은 검증된 파라미터를 반환한다. 

if (!id) {
  return c.json({ error: "Missing id" }, 400);
}

- id가 없으면 400에러를 반환한다. 

 

인증처리 (ClekrMiddleware와 getAuth)

const auth = getAuth(c);
if (!auth?.userId) {
  return c.json({ error: "Unauthorized" }, 401);
}

- clerkMiddleware는 요청에 포함된 인증 정보를 처리한다. 

- getAuth는 인증 정보를 가져온다. auth?.userID가 존재하지 않으면 401응답을 반환한다. 

- 이를 통해 사용자 인증 여부를 확인한다.

데이터 베이스 조회 

const [data] = await db
  .select({
    id: accounts.id,
    name: accounts.name,
  })
  .from(accounts)
  .where(and(eq(accounts.userId, auth.userId), eq(accounts.id, id)));

- db.select는 데이터베이스에서 계정 정보를 조회한다. 선택한 필드는 id와 name

- where조건은 eq(accounts.userId, auth.userId)는 요청한 사용자가 소유한 계정인지 확인하고 eq(accounts.id, id)는 요청된 id와 일치하는 계정을 검색하낟. 

- and조건은 두 조건을 동시에 만족하는 데이터를 필터링한다. 

검색결과처리

- data가 없는 경우는 요청한 계정이 존재하지 않거나 사용자가 소유하지 않은경우이다. 

- 클라이언트에 404에러를 반환한다.

useGetAccount

React Query 기반으로 만들어진 이 훅은 특정 계정의 정보를 가져올 수 있도록 도와준다. 이 훅은 서버에서 데이터를 비동기적으로 가져오고 React Query의 강력한 기능을 활용해 상태를 관리하며 간단한 API호출을 가능하게 해준다. 

useQuery설정

const query = useQuery({
  enabled: !!id,
  queryKey: ["account", { id }],
  queryFn: async () => {
    const response = await client.api.accounts[":id"].$get({
      param: { id },
    });

- useQuery는 ReactQeury에서 데이터 fetching과 캐싱을 관리하는 훅이ㅏㄷ. 

- queryKey는 쿼리를 식별하는 키로 캐싱과 데이터 리패칭을 관리한다. ["account",{id}]는 계정 데이터를 특정 ID로 구분하도록 설정한다. 

- enabled:!!id는 id가 존재할때만 쿼리를 활성화한다. !!id는 id가 null또는 undefined인 경우 쿼리를 비활성화하여 불필요한 요청을 방지한다. 

 

API호출

const response = await client.api.accounts[":id"].$get({
  param: { id },
});

- client.api.accounts[":id"].$get에서 :id는 동적 경로를 나타내며 API요청시 {id}를 URL에 동적으로 주입한다.

- param:{id}는 API요청에서 필요한 URL파라미터를 설정한다. 

 

응답처리 

if (!response.ok) {
  throw new Error("Failed to fetch accounts");
}
const { data } = (await response.json()) as {
  data: { id: string; name: string };
};

return data;

- HTTP상태는 response.ok코드로 확인한다. 응답상태가 200~299범위에 속하지 않으면 오류를 발생시킨다. 이를 통해 API호출 실패 시 적절한 예외 처리가 가능하다. 

- response.json()은 응답 데이터를 JSON으로 변환하여 사용한다. 

- as { data: { id: string; name: string } }를 통해 응답 데이터 구조를 명확하게 지정한다. 

( 내가 여기서 좀 애를 좀 먹었다.. 타입 지정을 못한 탓에 data에서 계속 오류가 났었다)

 

동작흐름 

1. 쿼리 활성화 여부 확인 : id가 없는 경우, AP호출이 비활성화된다. 

2. API호출: id를 사용해 서버에서 특정 계정 정보를 요청한하고 응답 데이터를 JSON으로 변환하고, 타입 단언을 통해 데이터 구조를 명확히 지정한다. 

3. React Qeury상태 관리: 데이터 로드 성공 실패 상태를 관리하며 이를 반환한다. 

 


계정 수정하기 

.PATCH

이 코드는 patch 요청을 처리하는 엔드포인트를 구현하여 클라이언트가 특정 계정 정보를 업데이터 할 수 있도록 하는것이다. 

전체적인 맬락은 다른 메소드와 비슷하다. 

 

데이터 베이스 업데이트 

const [data] = await db
  .update(accounts)
  .set(values)
  .where(and(eq(accounts.userId, auth.userId), eq(accounts.id, id)))
  .returning();

데이터 베이스를 다루는 부분에서 업데이트 기능을 가져올 수 있다. 

- db.update()는 accounts테이블에서 데이터를 업데이트하는 SQL쿼리를 실행한다. 

- set(values)는 요청 본문에서 받은 값을 업데이트한다. 

- 해당 내용의 조건은 userId와 id가 모두 일치하는 계정을 찾아 업데이트 하는 것이다. 

- returning()을 통해서 업데이트된 데이터를 반환한다. 

- 그리고 반환된 데이터의 첫번째 항목을 data에 저장한다. 

useEditAccount

타입정의 

type ResponseType = InferResponseType<
  (typeof client.api.accounts)[":id"]["$patch"]
>;

type RequestType = InferRequestType<typeof client.api.accounts.$post>["json"];

ResponseType

- InferResponseType을 사용해 서버에서 반환할 응답 타입을 추론한다. 

- client.api.account..[$patch]는 PATCH요청에 대한 타입 정보를 제공한다. 

- ResponseType은 서버에서 반환되는 데이터 형식을 나타낸다. 

 

RequestType

- InferRequestType을 사용해 클라이언트에서 보낼 요청 데이터 타입을 추론한다. 

- client.api.accounts.$post의 요청 타입을 기반으로 설정하며, json 형태로 전송된다.

- 이 타입은 업데이트 요청에서 필요한 필드들(예: { name: string; })을 정의한다.

 

ReactQeury 클라이언트 초기화 

const queryClient = useQueryClient();

 

- queryClient는 React Qeury의 캐시관리를 담당하는 객체이다. 

- 이를 통해 기존 데이터를 갱신하거나 삭제할 수 있다. 

 

useMutation훅 

const mutation = useMutation<ResponseType, Error, RequestType>({
  mutationFn: async (json) => {
    const response = await client.api.accounts[":id"]["$patch"]({
      param: { id },
      json,
    });
    return await response.json();
  },
  ...
});

- useMutation: React Query에서 데이터를 수정하거나 삭제할 때 사용되는 훅이다. useMutation은 데이터를 변경하는 비동기 작업을 처리하고, 성공/실패 후 작업을 정의할 수 있게 해준다. 

- 제네릭타입 : responsetype은 서버 응답, error는 에러타입, requesttype은 요청 데이터 타입 

- mutationFn은 실제 데이터를 수정하는 비동기 함수이다. client.api.accounts...을 호출해서 서버에 PATCH요청을 보낸다. param은 경로 파라미터로 계정 ID를 전달한다. json은 요청 본문으로 수정하려는 데이터를 전달한다. 

- 요청 후에는 response.json()을 호출해 응답 데이터를 반환한다. 

 

성공시 동작 

onSuccess: () => {
  toast.success("Account updated");
  queryClient.invalidateQueries({ queryKey: ["accounts", { id }] });
  queryClient.invalidateQueries({ queryKey: ["accounts"] });
},

- 성공 알림 : toast.success("Account updated")는 수정 성공 메시지를 사용자에게 표시한다. 

- 캐시 무효화는 queryClient.invalidateQueries를 호출하여 React Qeury의 캐시를 무효화한다.  

또 "accounts",{id}는 수정된 특정 계정 데이터를 다시 가져오고 accounts는 계정 목록 데이터를 다시가져온다. 

실패시 동작

onError: () => {
  toast.error("Failed to edit account");
},

- 실패하면 toast를 통해 에러메시지가 표시된다. 

 

반환값 

return mutation;

mutation 객체는 useMutation에서 제공하는 메서드와 상태 값을 포함한다.

- mutation.mutate(data): 데이터를 수정하는 요청을 트리거한다.

- mutation.isLoading: 요청이 진행 중인지 확인한다.

- mutation.isError: 요청이 실패했는지 확인한다.

- mutation.isSuccess: 요청이 성공했는지 확인한다.

계정 삭제하기 

.delete

.delete(
    "/:id",
    clerkMiddleware(),
    zValidator(
      "param",
      z.object({
        id: z.string().optional(),
      })
    ),
    zValidator(
      "json",
      insertAccountSchema.pick({
        name: true,
      })
    ),
    async (c) => {
      const auth = getAuth(c);
      const { id } = c.req.valid("param");

      if (!id) {
        return c.json({ error: "Missing id" }, 400);
      }
      if (!auth?.userId) {
        return c.json({ error: " Unauthorized" }, 401);
      }
      const [data] = await db
        .delete(accounts)

        .where(and(eq(accounts.userId, auth.userId), eq(accounts.id, id)))
        .returning({
          id: accounts.id,
        });
      if (!data) {
        return c.json({ error: "Not Found" }, 404);
      }
      return c.json({ data });
    }
  );
export default app;

이것 도 결국 위에서 한 내용과 똑같다. 

db에서 .delete하면 된다. 

useDeleteAccount

import { toast } from "sonner";
import { InferResponseType } from "hono";
import { useMutation, useQueryClient } from "@tanstack/react-query";

import { client } from "@/lib/hono";

type ResponseType = InferResponseType<
  (typeof client.api.accounts)[":id"]["$delete"]
>;

export const useDeleteAccount = (id?: string) => {
  const queryClient = useQueryClient();

  const mutation = useMutation<ResponseType, Error>({
    mutationFn: async () => {
      const response = await client.api.accounts[":id"]["$delete"]({
        param: { id },
        json: { name: "" },
      });
      return await response.json();
    },
    onSuccess: () => {
      toast.success("Account deleted");
      queryClient.invalidateQueries({ queryKey: ["account", { id }] });
      queryClient.invalidateQueries({ queryKey: ["accounts"] });
    },
    onError: () => {
      toast.error("Failed to delete account");
    },
  });
  return mutation;
};

이것도 path에서 했던 방식과 동일하게 적용되었다. 

colums

새로운 타입 정의 

export type ResponseType = InferResponseType<
  typeof client.api.accounts.$get,
  200
>["data"][0];

Account.ts에서는 타입이 따로 정해져 있지 않았으나 ResponseType을 따로 정의를 해주었다. 

- InferResponseType을 사용해서 API응답 타입을 정의했다.

- client.api.accounts.$get의 응답 데이터에서 200 상태의 데이터를 기반으로 첫번째 객채타입을 추출하도록 했다. 

- 타입 명시로 인해 정확한 데이터 타입 추론 및 코드의 안전성이 강화되었다. 

체크박스 기능 확장 

이전에는 단일 체크 박스 기능만 가지고 있었다. 

header: ({ table }) => (
  <Checkbox
    checked={
      table.getIsAllPageRowsSelected() ||
      (table.getIsSomePageRowsSelected() && "indeterminate")
    }
    onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
    aria-label="Select all"
  />
),

- 이 코드를 통해 헤더 체크 박스에서 테이블의 모든 행을 선택 및 해제할 수 있도록 하였다.

- 전체선택은 table.getIsAllPageRowsSelected로 모든 행이 선택되었는지 확인한다. 

- 부분선택은 table.getIsSomePageRowsSelected로 일부만 선택되었는지 확인한다. 

- 체크상태에 따라 전체선택 부분 선택 또는 전체 해체로 상태를 토글할 수 있다. 

cell: ({ row }) => (
  <Checkbox
    checked={row.getIsSelected()}
    onCheckedChange={(value) => row.toggleSelected(!!value)}
    aria-label="Select row"
  />
),

개별 박스 cell은 각 행의 개별 선택 및 해제가 가능하다. 

- row.getIsSelected로 현재 행이 선택되었는지 확인할 수 있다. 

- 체크박스의 변경상태에 따라 row.toggleSelected로 선택 상태를 토글할 수 있다. 

 

AccountPage

Skeleton추가 

<CardHeader>
          <Skeleton className="h-8 w-48" />
        </CardHeader>

- accountsQuery.isLoading으로 상태를 확인하여 데이터를 로드 중일 때 로더와 스켈레톤 UI를 보여준다. 

- 이는 사용자 경험이 개선되어 데이터 로드 중에도 상태를 명확하게 전달해준다. 

삭제기능 및 상태관리 

const isDisabled = accountsQuery.isLoading || deleteAccounts.isPending;

<DataTable
  ...
  onDelete={(row) => {
    const ids = row.map((r) => r.original.id);
    deleteAccounts.mutate({ ids });
  }}
  disabled={isDisabled}
/>

- deleteAccounts.isPending상태를 추가해서 삭제 작업 중에도 버튼과 Datatable의 상호작용을 비활성화 시킨다. 

- onDelete핸들러에서 삭제하려는 계정 ID목록을 추출해서 삭제 API를 호출한다. 

 

- 이는 동시에 여러 작업 (로딩/삭제)로 인해 발생할 수 있는 UI비정상 작동을 방지한다. 

- 코드의 상태 관리를 명확히 해서 유지 보수성을 높인다. 

 

Account API  bulk-delete

인증 미들웨어를 통해 사용자 검증 

clerkMiddleware();
const auth = getAuth(c);
if (!auth?.userId) {
  return c.json({ error: "Unauthorized" }, 401);
}

- Clerkmiddleware를 통해 인증 정보를 미리 확인한다. 

- getAuth(c)로 인증 정보를 가져오고 auth.userId를 통해 요청 사용자를 식별한다. 

- 인증되지 않은 사용자는 401에러로 응답한다. 

요청 데이터 검증

zValidator(
  "json",
  z.object({
    ids: z.array(z.string()),
  })
);
const values = c.req.valid("json");

- zvalidator는 요청 데이터가 JSON형식인지를 검증한다. 

- ids가 반드시 문자열 배열이어야 함을 명시

- 검증에 실패하면 400에러 

삭제 조건 작성(and 와 inArray)

const data = await db
  .delete(accounts)
  .where(
    and(
      eq(accounts.userId, auth.userId),
      inArray(accounts.id, values.ids)
    )
  );

- 현재 사용자와 연관된 계정만 삭제하고 이를 통해 다른 사용자 데이터에 접근하거나 삭제하는 것을 방지한다. 

- 요청 데이터에 포함된 계정 ID만 삭제한다. 요청에 포함되지 않은 데이터는 삭제되지 않는다. 

삭제 결과 반환 

.returning({
  id: accounts.id,
});
return c.json({ data });

- 삭제된 레코드의 ID값을 반환한다. 

- 클라이언트는 이 데이터를 통해 성공적으로 삭제된 항목을 확인할 수 있다. 

 

갑자기 궁금한게 왜 .delete가 아니라 .post를 사용한걸까?

RESTful제약을 완화한 설계 

- Delete메서드는 리소스 삭제를 위해 설계되었지만 HTTP표준에서 Delete요청 본문에 데이터를 포함하는 것은 권장하지 않는다. 

- 브라우저나 일부 클라이언트 환경에서 Delete요청의 본문지원이 제한적일 수 있기 때문에 데이터를 안전하게 전달하기 위해 POST를 선택했다. 

클라이언트 제약 

일부 클라이언트나 API게이트웨이에서는 DELETE요청의 본문을 제대로 처리하지 못하는 경우가 있어 대안으로 POST를 사용하는 경우가 많다. 

 

  POST DELETE
사용 사례 본문에 데이터를 포함해야 할 때, 혹은 클라이언트 제약 시 삭제할 리소스의 식별자가 URL로 전달 가능할 때
RESTful 원칙 준수 여부 다소 제한적 RESTful 설계 원칙을 충실히 따름
클라이언트 호환성 DELETE 본문을 지원하지 않는 환경에서도 안전함 일부 환경에서 DELETE 본문 처리에 제약이 있을 수 있음
복잡한 데이터 삭제 여러 ID를 삭제하거나, 추가적인 정보가 필요한 경우 적합 단일 리소스 삭제나 URL로 간단히 식별 가능한 경우 적합

use-delete-account

request와 reponse타입

type ResponseType = InferResponseType<
  (typeof client.api.accounts)["bulk-delete"]["$post"]
>;
type RequestType = InferRequestType<
  (typeof client.api.accounts)["bulk-delete"]["$post"]
>["json"];
  • InferRequestType와 InferResponseType은 Hono 클라이언트를 활용해 서버 API의 요청(Request)과 응답(Response) 타입을 추론한다.
  • 이를 통해 서버와의 데이터 구조 불일치를 방지하며, TypeScript의 타입 안전성을 유지한다.

useMutation

const mutation = useMutation<ResponseType, Error, RequestType>({
  mutationFn: async (json) => {
    const response = await client.api.accounts["bulk-delete"]["$post"]({
      json,
    });
    return await response.json();
  },
  onSuccess: () => {
    toast.success("Account deleted");
    queryClient.invalidateQueries({ queryKey: ["accounts"] });
  },
  onError: () => {
    toast.error("Failed to delete account");
  },
});

useMutation은 서버에 데이터를 전송하거나 변경할 때 사용하는 React Query훅이다. 

 

  • mutationFn: 서버에 삭제 요청을 보내는 비동기 함수이다. 여기서는 Hono 클라이언트를 통해 /bulk-delete API를 호출한다.
  • onSuccess: 요청 성공 시 호출된다. 성공 알림을 표시하고, ["accounts"]라는 키를 가진 데이터를 무효화해 계정 목록을 최신 상태로 유지한다.
  • onError: 요청 실패 시 호출되고 실패 알림을 표시한다.

Query Client

const queryClient = useQueryClient();

 

 

  • queryClient는 React Query에서 제공하는 전역 클라이언트로, 캐싱된 데이터를 관리한다.
  • 여기서는 invalidateQueries를 사용해 특정 쿼리 키를 무효화하여 삭제 작업 이후 계정 목록이 다시 로드되도록 처리한다.

Toast알림

toast.success("Account deleted");
toast.error("Failed to delete account");

 

- sooner라이브러리를 사용해 사용자에게 성공 및 실패 메시지를 표시한다. 

- 사용자는 작업 결과를 직관적으로 확인할 수 있다.

 

useBulkDeleteAccounts 훅은 서버와의 데이터 통신, 상태 관리, 사용자 피드백이라는 주요 문제를 효율적으로 해결한다. React Query와 Hono 클라이언트를 활용한 이 접근 방식은 간결하면서도 타입 안전성을 보장하며, 확장 가능한 코드 구조를 제공한다.

 

useConfirm

promise상태 관리 

const [promise, setPromise] = useState<{
  resolve: (value: boolean) => void;
} | null>(null);

- promise는 현재 모달 상태를 관리한다. 모달이 열리면 resolve함수를 저장해서 사용자가 확인 또는 취소 버튼을 눌렀을 때 Promise를 완료한다. 

confirm함수 

const confirm = () =>
  new Promise((resolve, reject) => {
    setPromise({ resolve });
  });

- 새로운 promise를 반환하여 사용자가 선택할 때까지 대기한다. promise가 완료되면 확인 또는 취소값을 반환한다. 

 

handle함수들 

- handleClose는 모달을 닫고 상태를 초기화한다. 

- handleConfirm은 사용자가 확인 버튼을 클릭했을 때 호출된다. 

- handleCancel은 사용자가 취소 버튼을 클릭했을 때 호출되고 promise를 false로 완료한 뒤 모달을 닫는다. 

+ Recent posts