프론트엔드 개발을 하며 검색창 자동완성이나 탭 전환 메뉴, 좋아요 버튼 같은 기능을 만들다 보면 사용자가 버튼을 마구 연타하거나 키보드를 빠르게 칠 때가 있습니다.

과거 프로젝트에서 사용자가 탭 버튼을 'A ➔ B ➔ C'로 빠르게 연속 클릭했을 때 황당한 버그를 마주쳤습니다. 분명 사용자는 마지막에 'C 탭'을 눌렀는데, 네트워크 응답 지연으로 인해 가장 늦게 도착한 'A 탭'의 데이터가 화면을 덮어씌워 버리는 '경쟁 상태(Race Condition)'가 발생한 것입니다.

이로 인해 화면에는 잘못된 데이터가 표시되었고, 서버에는 이미 쓸모없어진 이전 탭들의 불필요한 API 요청이 그대로 쌓여 리소스를 낭비하고 있었습니다.

오늘은 브라우저의 표준 내장 API인 AbortController를 활용하여 이전 비동기 요청을 깔끔하게 취소하고 경쟁 상태를 방지했던 노하우를 공유해 보려고 합니다.

1. 내가 마주친 문제: "왜 마지막으로 클릭한 결과가 안 나올까?" (경쟁 상태)

네트워크 요청은 항상 보낸 순서대로 도착한다는 보장이 없습니다.

  • 1번 요청 (A 탭 클릭, 응답 시간 1.5초)
  • 2번 요청 (B 탭 클릭, 응답 시간 0.2초)

사용자가 A를 누르고 곧바로 B를 눌렀을 때, B의 응답이 먼저 화면에 반영되었다가 1초 뒤 뒤늦게 도착한 A의 응답이 화면을 덮어버리는 끔찍한 UX 참사가 발생합니다.

이 문제를 해결하려면 "새로운 요청이 발생하면, 아직 끝나지 않은 이전 요청은 브라우저 차원에서 즉시 취소(Abort)"해야 합니다.

2. 해결책: 브라우저 표준 AbortController란?

AbortController는 웹 브라우저에 내장된 객체로, 하나 이상의 웹 요청(예: fetch API)을 원하는 시점에 중단할 수 있는 신호(Signal)를 제공합니다.

  • controller.signal: 통신 함수(fetch)에 전달하여 취소 신호를 수신할 수 있게 연결합니다.
  • controller.abort(): 이 메서드를 호출하면 연결된 네트워크 요청이 브라우저 수준에서 즉시 취소됩니다.

3. 실무 적용 1: fetch에 취소 기능 적용하기

새로운 요청을 시작하기 직전, 이전 컨트롤러의 abort()를 실행해 이전 통신을 취소하도록 작성했습니다.

TypeScript
 
// ⭕ AbortController를 활용한 안전한 API 호출 함수
let previousController: AbortController | null = null;

async function fetchSearchResults(query: string) {
  // 1. 이미 진행 중인 이전 요청이 있다면 취소 신호 발송!
  if (previousController) {
    previousController.abort();
  }

  // 2. 새로운 요청을 위한 컨트롤러 생성
  previousController = new AbortController();
  const { signal } = previousController;

  try {
    const response = await fetch(`https://api.example.com/search?q=${query}`, {
      signal, // 👈 fetch 두 번째 옵션에 signal 전달!
    });
    const data = await response.json();
    return data;
  } catch (error: any) {
    // 취소로 인한 중단인 경우 에러 처리에서 안전하게 무시
    if (error.name === 'AbortError') {
      console.log('이전 요청이 안전하게 취소되었습니다.');
    } else {
      console.error('실제 네트워크 에러 발생:', error);
    }
  }
}

이렇게 하면 사용자가 검색어를 빠르게 연속 입력하더라도, 마지막 입력값에 대한 응답만 정상 처리되고 앞선 요청들은 네트워크 레벨에서 모두 취소됩니다.

new AbortController() 및 fetch(url, { signal }) 코드

4. 실무 적용 2: React useEffect 클린업(Clean-up)에서 요청 취소하기

React 컴포넌트 안에서 데이터를 불러올 때는 useEffect의 뒷정리(Clean-up) 함수에서 controller.abort()를 호출하는 것이 정석 패턴입니다.

컴포넌트가 언마운트되거나 의존성 변수가 변경될 때 이전 요청을 자동으로 끊어주어 메모리 누수(Memory Leak)를 완벽히 막아줍니다.

TypeScript
 
import { useState, useEffect } from 'react';

export default function UserDetail({ userId }: { userId: number }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, {
      signal: controller.signal,
    })
      .then((res) => res.json())
      .then((data) => setData(data))
      .catch((err) => {
        if (err.name !== 'AbortError') console.error(err);
      });

    // 💡 컴포넌트가 사라지거나 userId가 바뀌면 이전 요청 즉시 취소!
    return () => {
      controller.abort();
    };
  }, [userId]);

  return
{data ? JSON.stringify(data) : '로딩 중...'}
;
}

연속 요청시 이전 요청이 취소됨

5. 결론 및 느낀 점

이전에는 비동기 통신 에러가 나면 무조건 백엔드나 네트워크 망 문제라고만 생각했습니다.

하지만 프론트엔드 단에서 AbortController를 통해 불필요한 요청을 선제적으로 취소해 주는 것만으로도, 예기치 않은 버그(경쟁 상태)를 원천 차단하고 서버 트래픽 비용을 획기적으로 줄일 수 있었습니다.

검색창 실시간 자동완성이나 탭 전환 컴포넌트를 만들고 계신다면, 지금 바로 AbortController를 도입해 안전하고 매끄러운 통신 환경을 구축해 보세요!

웹 사이트를 개발하다가 사용자가 실수로 브라우저 새로고침(F5)을 누르거나 페이지를 이동했을 때, 기껏 작성 중이던 폼 데이터나 다크 모드 설정값이 초기화되어 날아가는 문제를 겪어보신 적 있으신가요?

저 역시 초기 프로젝트에서 모든 상태를 React useState로만 관리했다가, 사용자가 브라우저를 껐다 켜거나 새로고침할 때마다 설정이 리셋되는 피드백을 받았습니다.

서버 데이터베이스(DB)에 일일이 저장하기에는 너무 자잘한 프론트엔드 설정값들(다크 모드 테마, 장바구니 임시 데이터, 팝업 '오늘 하루 보지 않기' 등)을 어디에 보관해야 할지 고민하다가 브라우저 저장소(Web Storage API)를 본격적으로 활용하게 되었습니다.

오늘은 브라우저의 대표적인 3대 저장소인 LocalStorage, SessionStorage, Cookie의 차이점과 실무에서 안전하게 활용했던 노하우를 공유해 보려고 합니다.

1. 3대 브라우저 저장소 핵심 비교: "어디에 무엇을 저장해야 할까?"

웹 브라우저가 제공하는 저장소는 수명(Lifecycle)과 용도에 따라 명확히 나뉩니다.

  • LocalStorage (로컬 스토리지): 브라우저 창을 닫아도 데이터가 영구적으로 유지됩니다. (용량 약 5MB) ➔ 다크 모드 테마, 사용자 맞춤 UI 설정에 적합
  • SessionStorage (세션 스토리지): 현재 탭(세션)이 열려 있는 동안에만 유지되며, 탭을 닫으면 즉시 삭제됩니다. ➔ 일회성 폼 입력 중간 저장, 결제 진행 단계 임시 데이터에 적합
  • Cookie (쿠키): 만료일을 설정할 수 있고 서버 요청 시 자동으로 헤더에 포함됩니다. (용량 약 4KB) ➔ 로그인 인증 토큰(HttpOnly) 등에 주로 사용

2. 실무 적용 1: LocalStorage로 다크 모드(Theme) 설정 영구 저장하기

사용자가 라이트 모드/다크 모드를 선택했을 때, 브라우저를 닫고 며칠 뒤에 다시 들어와도 그 설정을 기억하도록 localStorage를 적용했습니다.

TypeScript
 
// ⭕ 로컬스토리지에 테마 저장 및 불러오기 함수
const THEME_KEY = 'app_user_theme';

// 1. 테마 저장하기
export const saveTheme = (theme: 'light' | 'dark') => {
  try {
    localStorage.setItem(THEME_KEY, theme);
  } catch (error) {
    console.error('스토리지 저장 실패:', error);
  }
};

// 2. 테마 불러오기 (기본값: 'dark')
export const getSavedTheme = (): 'light' | 'dark' => {
  if (typeof window === 'undefined') return 'dark';
  const saved = localStorage.getItem(THEME_KEY);
  return saved === 'light' ? 'light' : 'dark';
};

3. 실무 적용 2: 객체(Object)나 배열 다룰 때 JSON.stringify 필수 사용

초보 시절 가장 많이 범했던 실수는 객체 데이터를 그대로 localStorage.setItem('user', { name: 'dev' })로 저장했던 것입니다.

브라우저 스토리지는 문자열(String)만 저장할 수 있기 때문에, 객체를 그냥 넣으면 [object Object]라는 깨진 문자열로 변환되어 저장됩니다.

JavaScript
 
const userProfile = { id: 1, name: '김개발', role: 'frontend' };

// ❌ 잘못된 저장 방식: [object Object]로 저장됨
localStorage.setItem('user', userProfile);

// ⭕ 올바른 저장 방식: JSON 문자열로 직렬화 후 저장
localStorage.setItem('user', JSON.stringify(userProfile));

// ⭕ 꺼내올 때: JSON.parse로 다시 자바스크립트 객체로 변환
const savedUser = JSON.parse(localStorage.getItem('user'));
console.log(savedUser.name); // '김개발' 정상 접근 가능!

localStorage.setItem 및 JSON.stringify

4. 실무 적용 3: 크롬 개발자 도구(Application 탭)로 데이터 확인하기

스토리지에 데이터가 제대로 들어갔는지 확인하려면 브라우저 개발자 도구의 Application 탭을 활용하면 됩니다.

  1. F12 개발자 도구 열기
  2. 상단 메뉴 중 Application (애플리케이션) 클릭
  3. 좌측 메뉴 Storage > Local Storage 클릭 후 내 도메인 선택

그러면 내가 코드로 저장한 Key와 Value가 실시간으로 표 형태로 직관적이게 표시되는 것을 확인할 수 있습니다.

local storage저장 확인

5. 결론 및 주의할 점

웹 스토리지는 사용하기 정말 편리하지만, 보안상 한 가지 중요한 원칙이 있습니다.

  • LocalStorage에는 절대 민감한 정보(비밀번호, 개인정보, 중요한 API 시크릿 키)를 저장하지 말 것!

XSS(Cross-Site Scripting) 공격에 노출될 경우 자바스크립트 코드로 쉽게 탈취될 수 있기 때문입니다. 따라서 UI 설정값, 캐시성 데이터, 비인증 임시 상태 위주로 활용하고, 중요한 인증 토큰은 안전한 HttpOnly Cookie를 활용하는 것이 모범 사례입니다.

개발 공부를 시작하고 매일 1일 1커밋을 다짐하며 깃허브(GitHub) 프로필에 초록색 잔디를 채워나가는 재미를 붙이던 중이었습니다.

어느 날 평소처럼 터미널에서 코드를 수정하고 git add, git commit, git push를 마친 뒤 뿌듯한 마음으로 깃허브 프로필을 열었는데, 커밋 기록은 분명 원격 저장소에 올라가 있는데 프로필 잔디밭에는 초록불이 전혀 심어지지 않는 당혹스러운 상황을 마주쳤습니다.

처음에는 "깃허브 서버 반영이 늦나?" 싶어 몇 시간을 기다려보기도 하고, 컴퓨터를 껐다 켜보기도 했지만 잔디는 여전히 하얗게 비어 있었습니다.

오늘은 제가 깃허브 잔디 누락 문제로 멘붕을 겪으며 찾아낸 진짜 원인(로컬 Git 이메일 불일치)과, 5분 만에 깔끔하게 해결했던 설정 방법을 정리해 보려고 합니다.

1. 내가 마주친 문제: "커밋은 올라갔는데 왜 내 프로필에 반영이 안 될까?"

깃허브 저장소(Repository)의 Commits 탭에 들어가 보니, 제가 푸시한 커밋 내역 자체는 정상적으로 올라와 있었습니다.

하지만 커밋 작성자 프로필 사진이 제 깃허브 아바타가 아니라 기본 회색 실루엣 아이콘으로 떠 있었고, 작성자 이름을 클릭해도 제 깃허브 프로필 페이지로 링크가 연결되지 않았습니다.

💡 깃허브가 잔디를 심어주는 핵심 원리

깃허브는 푸시된 커밋의 작성자 이메일 주소(Author Email)를 보고 해당 계정의 잔디밭에 초록불을 심어줍니다.

  • 내 컴퓨터(로컬)의 Git에 설정된 이메일 주소
  • 실제 깃허브 계정에 등록된 이메일 주소

이 둘이 단 한 글자라도 다르면 깃허브는 "이 커밋은 다른 사람이 작성한 것"으로 인식하여 내 프로필 잔디를 채워주지 않는 것입니다.

2. 해결 단계 1: 내 로컬 Git 설정 이메일 확인하기

터미널을 열고 현재 내 컴퓨터의 Git 전역 설정(Global Config)에 어떤 이메일이 등록되어 있는지 확인해 보았습니다.

Bash
 
# 터미널에서 현재 설정된 Git 유저 이름과 이메일 확인 명령어
git config --global user.name
git config --global user.email

확인해 보니 예전에 임의로 입력했던 오타 섞인 이메일이나, 현재 깃허브 계정과는 다른 보조 이메일이 등록되어 있었습니다.

3. 해결 단계 2: 깃허브 실제 이메일과 일치시키기

원인을 알았으니 로컬 Git의 이메일을 깃허브 계정에 등록된 실제 이메일과 완벽하게 일치하도록 수정해 주었습니다.

Bash
 
# 1. 깃허브 계정 이메일과 동일하게 전역 설정 업데이트
git config --global user.name "내_깃허브_닉네임"
git config --global user.email "내_깃허브_가입이메일@example.com"

# 2. 변경된 내용이 잘 반영되었는지 확인
git config --global --list

💡 꿀팁: 깃허브 이메일 비공개(Keep my email address private) 설정 유저라면?

만약 깃허브 설정(Settings > Emails)에서 개인정보 보호를 위해 이메일 비공개 옵션을 켜두셨다면, 깃허브에서 발급해 주는 ID+username@users.noreply.github.com 형태의 노리플라이 이메일을 user.email에 등록하셔야 정상적으로 잔디가 심어집니다!

4. 해결 확인: 새 커밋 푸시 후 잔디밭 확인하기

설정을 올바르게 바꾼 뒤, 가볍게 README.md 파일을 수정하고 다시 커밋 및 푸시를 진행해 보았습니다.

Bash
 
git add README.md
git commit -m "docs: 깃 잔디 연동 테스트 커밋"
git push origin main

푸시 완료 후 깃허브 프로필 페이지를 새로고침하자마자, 비어 있던 칸에 선명한 초록색 잔디가 즉시 채워지는 것을 확인할 수 있었습니다! 커밋 내역의 프로필 아이콘도 제 프로필 사진으로 정상 연결되었습니다.

깃허브 잔디밭

5. 깨달은 점 및 정리

처음에는 깃허브 시스템 오류인 줄 알고 헛걸음을 많이 했지만, Git의 사용자 식별 방식(이메일 기반 매칭)을 이해하고 나니 아주 단순하게 해결할 수 있었던 문제였습니다.

  1. 커밋을 올렸는데 잔디가 안 심어진다면 가장 먼저 git config --global user.email 을 확인할 것!
  2. 로컬 이메일과 깃허브 계정 이메일을 100% 일치시킬 것!

1일 1커밋을 실천 중인데 잔디가 심어지지 않아 스트레스받고 계신 분들이 계시다면, 지금 바로 터미널에서 이메일 설정부터 점검해 보시길 바랍니다!

리액트(React)로 프로젝트를 진행하며 API 데이터를 불러오거나 컴포넌트 생명주기(Lifecycle)를 다룰 때 가장 먼저 접하게 되는 훅(Hook)이 바로 useEffect입니다.

처음 useEffect를 배울 때는 "그냥 컴포넌트가 화면에 나타날 때 실행하고 싶은 코드를 넣어두는 함수" 정도로 가볍게 생각하곤 했습니다. 하지만 프로젝트 규모가 커지면서 어느 날 브라우저 탭이 갑자기 렉을 먹고 먹통이 되거나, 개발자 도구 콘솔에 수백 개의 API 요청 로그가 쏟아지는 '무한 리렌더링(Infinite Loop)' 참사를 겪게 되었습니다.

오늘은 제가 초보 시절 useEffect를 오용하면서 겪었던 무한 리렌더링의 원인과, 이를 안전하고 올바르게 다루기 위한 의존성 배열(Dependency Array) 관리 노하우를 공유해 보려고 합니다.

1. 내가 마주친 문제: "브라우저 콘솔창이 터져나간 이유"

특정 유저 정보를 서버에서 받아와 상태(State)에 저장하는 흔한 로직을 작성하던 중이었습니다.

별생각 없이 useEffect 내부에서 데이터를 받아와 setState를 호출했는데, 페이지에 진입하자마자 브라우저가 느려지더니 서버로 동일한 API 요청이 초당 수십 번씩 계속 날아가는 현상이 발생했습니다.

TypeScript
 
// ❌ 무한 리렌더링을 유발했던 과거의 위험한 코드
import { useState, useEffect } from 'react';

export default function UserProfile() {
  const [user, setUser] = useState(null);

  // 🚨 두 번째 인자인 의존성 배열(Dependency Array)을 빠뜨림!
  useEffect(() => {
    fetch('/api/user')
      .then((res) => res.json())
      .then((data) => {
        setUser(data); // 상태 변경 -> 컴포넌트 리렌더링 -> useEffect 재실행 -> 무한 반복!
      });
  });

  return <div>{user?.name}</div>;
}

💡 무한 루프가 발생한 원인

useEffect의 두 번째 인자인 의존성 배열([])을 완전히 생략해 버리면, 리액트는 컴포넌트가 리렌더링 될 때마다 useEffect 내부 콜백 함수를 매번 실행합니다.

useEffect 내부에서 setUser로 상태를 바꾸면 ➔ 리액트는 상태가 바뀌었으니 컴포넌트를 리렌더링하고 ➔ 리렌더링 되었으니 useEffect가 또 실행되어 setUser를 호출하는 지옥의 악순환이 만들어진 것입니다.

2. 해결책 1: 빈 의존성 배열([])로 '마운트 시 1회 실행' 보장하기

이 문제를 가장 쉽게 해결하는 방법은 useEffect 두 번째 인자로 빈 배열([])을 넘겨주는 것입니다.

TypeScript
 
// ⭕ 빈 배열 []을 넣어 컴포넌트 마운트 시 최초 1회만 실행되도록 수정
useEffect(() => {
  fetch('/api/user')
    .then((res) => res.json())
    .then((data) => setUser(data));
}, []); // 👈 빈 의존성 배열 추가!

이렇게 설정하면 컴포넌트가 화면에 처음 그려질 때(Mount) 딱 한 번만 API 요청을 수행하므로 안전합니다.

useEffect 두 번째 인자로 빈 배열 [] 이 들어간 코드

3. 해결책 2: 객체/함수를 의존성 배열에 넣을 때 발생하는 함정 피하기

빈 배열을 넣는 것만으로 모든 문제가 해결되지는 않았습니다. 특정 조건(예: 검색어 ID 변경)에 따라 데이터를 다시 불러와야 할 때는 의존성 배열에 변수를 넣어주어야 합니다.

이때 객체(Object)나 함수(Function)를 의존성 배열에 그대로 넣으면 다시 무한 루프가 터지는 함정에 빠질 수 있습니다.

TypeScript
 
// ❌ 객체를 참조 타입으로 넣어 무한 루프가 발생하는 잘못된 예시
export default function SearchList({ options }) { // options가 객체인 경우
  const [list, setList] = useState([]);

  useEffect(() => {
    fetchData(options).then((data) => setList(data));
  }, [options]); // 🚨 부모가 리렌더링될 때마다 options 객체의 참조값이 새로 바뀌어 계속 실행됨!
}

자바스크립트에서 객체나 함수는 리렌더링될 때마다 메모리 주소(참조값)가 매번 새로 생성됩니다. 따라서 내용물이 똑같아도 리액트는 "의존성 값이 바뀌었네?" 하고 useEffect를 또 실행해 버립니다.

💡 해결 방법 (원시 값 전달 또는 useCallback / useMemo 활용)

  • 객체 전체 대신 객체 내부의 원시 값(options.id 등)을 의존성 배열에 넣기
  • 함수를 의존성 배열에 넣어야 할 경우 useCallback으로 감싸 메모이제이션하기
TypeScript
 
// ⭕ 객체 전체가 아닌 원시 값(id)을 의존성 배열에 넣어 안전하게 관리
useEffect(() => {
  fetchData(options.id).then((data) => setList(data));
}, [options.id]); // 👈 객체의 속성값(number, string)을 지정!

의존성 배열 관련 코드

4. 정리 및 깨달은 점

useEffect는 단순히 코드를 실행해 주는 부수 효과 도구가 아닙니다. "컴포넌트의 상태 및 렌더링 주기와 외부 시스템을 동기화하는 도구"입니다.

  1. useEffect 내부에서 상태(state)를 변경한다면 의존성 배열이 제대로 설정되어 있는지 필수 점검할 것
  2. 의존성 배열에 참조 타입(객체, 배열, 함수)을 넣을 때는 참조값 변경에 유의할 것

이 두 가지만 명확히 기억해 두셔도 무한 리렌더링으로 브라우저가 다운되는 불상사를 완벽히 예방할 수 있습니다!

프런트엔드 개발을 처음 시작했을 때, 기본 설정 상태의 VS Code(Visual Studio Code)만 사용하며 일일이 닫는 태그를 직접 타이핑하고 코드 들여 쓰기(Indent) 줄 맞춤을 일일이 Tab키로 맞추곤 했습니다.

그러다 보니 단순 오타나 태그 짝이 맞지 않아 발생하는 소소한 에러를 잡는 데 많은 시간을 허비했고, 자연스럽게 개발 피로도도 높아졌습니다.

이후 실무 프로젝트를 거치며 나에게 딱 맞는 VS Code 확장 프로그램(Extensions)들을 하나씩 찾아 세팅하게 되었고, 반복적이고 소모적인 단순 작업량이 획기적으로 줄어드는 것을 경험했습니다.

오늘은 프론트엔드 개발자로서 실제 코딩 생산성을 2배 이상 끌어올려 주었던 VS Code 필수 익스텐션 5가지와 실무 추천 설정을 소개해 드립니다.

1. Prettier - Code formatter (코드 스타일 자동 정돈)

프런트엔드 개발자에게 가장 기본이자 필수적인 익스텐션입니다. 홑따옴표/큰따옴표 혼용, 세미콜론 누락, 들여 쓰기 불일치 등 지저분한 코드 스타일을 저장(Ctrl + S / Cmd + S) 한 번으로

깔끔하게 자동 정돈해 줍니다.

JSON
 

 

// .prettierrc (프로젝트 루트에 추가하는 추천 설정 파일)
{
  "singleQuote": true,
  "semi": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 80
}

💡 실무 팁: Format On Save 설정

VS Code 설정(Ctrl + ,)에서 Format On Save 항목을 체크해 두면, 저장할 때마다 자동으로 코드가 이쁘게 정렬되어 코드 리뷰 시 스타일 차이로 인한 소모적 논쟁을 완벽히 방지해 줍니다.

2. ESLint (자바스크립트/리액트 코드 오류 사전 감지)

Prettier가 코드 '모양(스타일)'을 잡아준다면, ESLint는 코드의 '문법 오류 및 잠재적 버그'를 사전에 감지해 주는 파수꾼입니다.

선언만 하고 쓰지 않은 변수, 리액트 useEffect 의존성 배열 누락, 잘못된 비동기 처리 등을 에디터 상에서 실시간 빨간 줄로 알려주어 런타임 에러를 획기적으로 줄여줍니다.

Prettier와 ESLint 설정

3. Auto Rename Tag & Auto Close Tag (HTML/JSX 태그 자동 관리)

HTML이나 리액트 JSX 작성 시 생산성을 폭발적으로 올려주는 효자 익스텐션 커플입니다.

  • Auto Close Tag: <div를 입력하면 자동으로 </div> 닫는 태그를 만들어 줍니다.
  • Auto Rename Tag: 여는 태그를 <div>에서 <section>으로 수정하면, 대응되는 닫는 태그도 자동으로 </section>으로 변경됩니다.

태그 이름 하나 바꾸려고 스크롤 내려서 닫는 태그를 찾아 다닐 필요가 없어져 타자 속도가 대폭 단축됩니다.

4. GitLens — Git supercharged (코드 작성자 및 변경 이력 추적)

팀 프로젝트나 협업 진행 시 강력한 빛을 발하는 도구입니다.

코드 각 라인 끝에 "누가, 언제, 어떤 커밋 메시지로 이 코드를 수정했는지" 가볍게 표시해 줍니다. 과거 코드를 고치거나 왜 이 로직이 들어갔는지 궁금할 때 Git 커밋 히스토리를 찾아볼 필요 없이 마우스만 올려도 맥락을 바로 파악할 수 있어 협업 생산성이 대폭 증가합니다.

5. Tailwind CSS IntelliSense (Tailwind 클래스 자동완성)

Tailwind CSS를 사용하는 프론트엔드 개발자라면 필수로 깔아야 할 확장 프로그램입니다.

className="flex items-center ..." 작성 시 사용 가능한 유틸리티 클래스 목록을 자동으로 추천해 주고, 해당 클래스가 실제 어떤 CSS 속성을 가졌는지 마우스를 올리면 미리 보기 패널로 보여줍니다.

Tailwind CSS 클래스 자동완성(IntelliSense) 팝업 창

💡 회고 및 요약

좋은 도구를 선택하고 내 개발 환경에 맞춰 세팅하는 것 역시 개발자의 중요한 역량 중 하나입니다.

오늘 소개해 드린 익스텐션들을 적극 활용하여 단순 오타나 스타일 정돈에 드는 시간을 아끼고, 진짜 중요한 핵심 비즈니스 로직 작성에 집중하는 쾌적한 개발 환경을 만들어 보시길 강력히 추천합니다!

자바스크립트(JavaScript)로만 프로젝트를 진행하다가 안정성을 높이기 위해 타입스크립트(TypeScript)를 처음 도입했을 때, 기쁨보다 당혹감이 훨씬 컸습니다.

분명 자바스크립트에서는 아무 문제 없이 잘 돌아가던 코드인데, 타입스크립트를 적용하자마자 VS Code 에디터 온 동네에 빨간 줄(Type Error)이 그어지며 화면이 온통 경고 메시지로 뒤덮였기 때문입니다.

당시에는 "개발 속도만 늦어지는 것 같은데 왜 쓸까?" 싶었지만, 타입 에러를 하나씩 해결해 나가면서 런타임(실행 시점)에서 터질 위험한 버그를 코드 작성 단계에서 미리 잡아주는 강력함을 체감하게 되었습니다.

오늘은 제가 타입스크립트 초보 시절 가장 자주 마주치며 식은땀을 흘렸던 대표적인 타입 에러 3가지와 완벽한 해결법을 정리해 보려고 합니다.

1. 에러 1: Object is possibly 'null' or 'undefined'

API 데이터를 받아오거나 DOM 요소를 참조할 때 가장 흔하게 만나는 에러입니다.

TypeScript
 
// ❌ 빨간 줄이 뜨는 과거의 코드
const nameLength = response.user.name.length;
// 🚨 Error: 'response.user' is possibly 'undefined'.

💡 원인 및 해결책

타입스크립트는 response.user 데이터가 서버 응답 지연이나 에러로 인해 undefined 상태일 가능성을 미리 경고해 주는 것입니다. 만약 이 상태로 자바스크립트에서 실행되었다면 런타임에 그 악명 높은 Cannot read properties of undefined 에러가 터졌을 것입니다.

해결을 위해 옵셔널 체이닝(?.)이나 타입 가드(Type Guard)를 활용하면 깔끔하게 해결됩니다.

TypeScript
 
// ⭕ 옵셔널 체이닝으로 안전하게 접근
const nameLength = response.user?.name?.length;

// ⭕ 또는 타입 가드(if문)로 안전성 보장
if (response.user && response.user.name) {
  const nameLength = response.user.name.length;
}

2. 에러 2: Property 'X' does not exist on type 'Y' (이벤트 객체 / 객체 키 접근 에러)

리액트에서 onChange 이벤트를 처리하거나, 객체의 키(Key)로 동적 접근을 시도할 때 자주 발생하는 에러입니다.

TypeScript
 
// ❌ e.target.value에서 빨간 줄이 뜨는 코드
const handleChange = (e) => {
  console.log(e.target.value);
  // 🚨 Error: Parameter 'e' implicitly has an 'any' type.
};

 

타입을 잘 지정해 에러가 안나는 모습

💡 원인 및 해결책

타입스크립트는 매개변수 e가 어떤 이벤트 객체인지 알지 못하므로 암묵적인 any 타입 경고를 보냅니다. 리액트에서 제공하는 이벤트 타입(React.ChangeEvent 등)을 명시해 주어야 합니다.

TypeScript
 
// ⭕ React의 HTMLInputElement 타입을 정확히 명시
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  console.log(e.target.value); // 이제 에러 없이 자동완성까지 지원됨!
};

3. 에러 3: Type 'X' is not assignable to type 'Y' (상태 초기값 타입 불일치)

useState를 사용할 때 초기값 타입과 나중에 넣어줄 API 데이터의 타입이 달라서 발생하는 에러입니다.

TypeScript
 
// ❌ null로 초기화해 두었더니 나중에 객체를 넣을 때 에러 발생
interface UserProfile {
  id: number;
  name: string;
}

const [user, setUser] = useState(null);

// 나중에 API 응답 후 setUser 호출 시 에러!
setUser({ id: 1, name: '김개발' }); 
// 🚨 Error: Argument of type '{ id: number; name: string; }' is not assignable to parameter of type 'null'.

💡 원인 및 해결책

useState(null)로 작성하면 타입스크립트는 user 변수의 타입을 오직 null로만 추론해 버립니다. 제네릭(<T>)을 사용해 "초기엔 null이지만 나중에 UserProfile 객체가 들어올 수 있다"라고 명시해 주어야 합니다.

TypeScript
 
// ⭕ 제네릭과 유니온 타입을 활용해 두 가지 가능성을 모두 열어둠
const [user, setUser] = useState<UserProfile | null>(null);

// 이제 객체를 넣어도 빨간 줄 없이 정상 동작!
setUser({ id: 1, name: '김개발' });

 

useState에 타입추가

4. 회고 및 느낀 점

타입스크립트를 처음 시작했을 때는 일일이 타입을 적어주는 행위가 개발 속도를 갉아먹는 걸림돌처럼 느껴졌습니다.

하지만 수많은 빨간 줄과 싸우고 해결해 나가는 과정에서 "내가 작성하는 코드의 데이터 흐름을 훨씬 객관적이고 안전하게 파악하는 습관"을 가질 수 있게 되었습니다.

지금 타입스크립트의 빨간 줄 때문에 스트레스를 받고 계신 초보 개발자분들이 계시다면, 에러 메시지를 무서워하지 마시고 "타입스크립트가 미래의 런타임 에러를 대신 막아주고 있구나!" 하고 긍정적으로 바라보시면 될 것 같습니다!

+ Recent posts