Skip to main content
Summary

Todo 앱 다음에 날씨 앱을 만드는 이유

할 일 관리 앱은 프론트엔드의 기본기를 익히기에 좋습니다. 사용자가 입력하고, 목록이 바뀌고, 삭제와 수정이 일어납니다. 하지만 실제 서비스는 거기서 한 단계 더 나아갑니다. 서버에서 데이터를 가져오고, 네트워크가 실패하고, 응답이 늦고, 사용자가 잘못된 값을 입력하고, 가끔은 API 사용량 제한에 걸립니다.

그래서 두 번째 실전 예제로 날씨 대시보드 앱이 딱 좋습니다. 화면은 비교적 친숙하지만, 그 안에는 실무 프론트엔드에서 자주 만나는 핵심 요소가 모두 들어 있습니다. 외부 API 호출, 비동기 요청, 로딩 상태, 오류 처리, 빈 결과 처리, 환경 변수 관리, 반응형 UI까지 한 번에 연습할 수 있습니다.

이번 장에서는 Codex에게 모든 것을 한 번에 맡기지 않습니다. 초보자에게 가장 안전한 방식으로 작업을 잘게 쪼갭니다. 먼저 정적 더미 데이터로 화면 흐름을 확인하고, 그다음 실제 날씨 API를 연결합니다. 마지막에는 오류 메시지와 접근성, 모바일 화면까지 다듬어 "연습용 장난감"이 아니라 배포 가능한 작은 앱처럼 완성해 보겠습니다.

Codex는 기획, 기능 구현, 리팩터링, 리뷰, 릴리스까지 개발 흐름 전반을 돕는 에이전트형 코딩 도구입니다.1 이 장에서는 그 장점을 살려 "계획 수립 → 구현 → 실행 확인 → 오류 수정 → 리팩터링" 순서로 Codex와 협업합니다.

실전 예제: 날씨 대시보드 앱 만들기

완성할 앱의 모습

이번에 만들 앱의 이름은 weather-dashboard라고 하겠습니다. 사용자는 도시 이름을 검색할 수 있고, 서울·부산·제주·도쿄·런던 같은 빠른 선택 버튼도 누를 수 있습니다. 검색이 성공하면 현재 날씨 카드와 5일 예보 리스트가 나타납니다. 최근 검색한 도시는 브라우저에 저장되어 다음에 다시 들어와도 바로 선택할 수 있습니다.

처음부터 복잡하게 만들 필요는 없습니다. 기능을 다음처럼 독립 요구사항으로 나눕니다.

  • 도시 검색: 입력창에 도시 이름을 쓰고 Enter 또는 검색 버튼으로 조회한다.

  • 현재 날씨: 도시명, 현재 온도, 체감 온도, 습도, 풍속, 날씨 설명, 아이콘을 보여 준다.

  • 5일 예보: 날짜별 대표 예보를 카드 리스트로 보여 준다.

  • 최근 검색어: 성공한 검색어를 최대 5개까지 저장하고 다시 클릭할 수 있게 한다.

  • 상태 처리: 로딩, 성공, 빈 결과, 네트워크 실패, 잘못된 도시명, API 한도 초과를 구분한다.

  • UI 개선: 모바일에서도 자연스럽게 보이고, 키보드만으로도 사용할 수 있게 한다.

초보자가 흔히 하는 실수는 "날씨 앱 만들어 줘"라고 Codex에게 한 문장만 던지는 것입니다. 그러면 Codex는 나름대로 코드를 만들어 주지만, 어떤 API를 쓰는지, 키는 어디에 넣는지, 오류는 어떻게 처리하는지, UI는 어떤 순서로 검증해야 하는지 흐름이 불분명해집니다.

이번에는 반대로 갑니다. 우리가 앱을 작게 설계하고, Codex가 각 조각을 구현하게 합니다.

프로젝트 만들기

앞 장에서 이미 개발 환경을 준비했다면 새 Vite 프로젝트를 만들 수 있습니다. 여기서는 React와 TypeScript를 기준으로 설명하겠습니다. TypeScript는 처음에는 조금 낯설지만, 외부 API 응답처럼 구조가 중요한 데이터를 다룰 때 실수를 줄여 줍니다.

npm create vite@latest weather-dashboard -- --template react-ts
cd weather-dashboard
npm install
npm run dev

브라우저에서 개발 서버가 뜨는지 먼저 확인합니다. 아직 날씨 기능은 없습니다. 하지만 이 단계에서 실행이 되는지 확인하는 습관이 중요합니다. 시작부터 문제가 있으면 나중에 Codex가 만든 코드가 문제인지, 프로젝트 설정이 문제인지 구분하기 어렵습니다.

이제 Git 상태를 깨끗하게 만듭니다.

git init
git add .
git commit -m "Initialize weather dashboard project"

Codex에게 실습을 맡기기 전에는 항상 이 상태를 만들어 두면 좋습니다. 실패해도 되돌아갈 기준점이 생기기 때문입니다.

AGENTS.md에 이번 프로젝트 규칙 적기

날씨 앱은 외부 API 키와 네트워크 요청을 다룹니다. Codex가 아무 파일이나 수정하거나 API 키를 코드에 직접 넣지 않도록 프로젝트 규칙을 먼저 적어 둡니다.

프로젝트 루트에 AGENTS.md를 만들고 다음처럼 작성합니다.

# Weather Dashboard 작업 지침

## 기술 스택
- React + TypeScript + Vite를 사용한다.
- 스타일은 기본 CSS 또는 CSS Module을 사용한다. 새 UI 라이브러리는 사용자 승인 없이 추가하지 않는다.
- 외부 날씨 API는 OpenWeather API를 기준으로 구현한다.

## 보안 규칙
- API 키를 소스 코드에 직접 작성하지 않는다.
- `.env` 또는 `.env.local` 파일은 커밋하지 않는다.
- 예시 키는 `.env.example`에만 작성한다.
- API 키가 없을 때는 앱이 깨지지 않고 한국어 안내 메시지를 보여 준다.

## 구현 규칙
- 먼저 더미 데이터로 UI를 완성한 뒤 실제 API 연동으로 확장한다.
- 로딩, 성공, 빈 결과, 네트워크 실패, 잘못된 도시명, API 한도 초과 상태를 구분한다.
- 사용자가 이해할 수 있는 한국어 오류 메시지를 사용한다.
- 변경 후에는 `npm run build`로 확인한다.

## 접근성 규칙
- 검색 입력창에는 label을 연결한다.
- 로딩 표시에는 적절한 aria 속성을 추가한다.
- 버튼은 키보드로 이동하고 실행할 수 있어야 한다.

이 파일은 Codex에게 "우리 프로젝트에서는 이렇게 일하자"라고 알려 주는 메모입니다. 특히 보안 규칙이 중요합니다. 날씨 API 키가 엄청난 비밀처럼 보이지 않을 수도 있지만, 키가 공개 저장소에 올라가면 누군가 마음대로 호출해서 사용량을 소모할 수 있습니다.

첫 번째 프롬프트: 바로 API를 붙이지 말고 화면부터 만들기

이제 Codex에게 첫 작업을 맡깁니다. 핵심은 "정적 더미 데이터로 먼저 UI를 검증하라"는 점입니다. API 연동까지 한 번에 시키면 화면 오류와 네트워크 오류가 섞여 디버깅이 어려워집니다.

Codex App, CLI, IDE 중 편한 곳에서 다음 프롬프트를 입력합니다.

현재 React + TypeScript + Vite 프로젝트에서 날씨 대시보드 UI를 먼저 구현해줘.

조건:
- 아직 실제 API는 연결하지 말고 더미 데이터만 사용해.
- 도시 검색 폼을 만든다.
- 서울, 부산, 제주, 도쿄, 런던 빠른 선택 버튼을 만든다.
- 현재 날씨 카드에는 도시명, 온도, 체감 온도, 습도, 풍속, 날씨 설명, 아이콘 영역을 표시한다.
- 5일 예보 리스트를 카드 형태로 표시한다.
- 최근 검색어 영역을 만든다. 지금은 더미 배열로 표시해도 된다.
- 모바일 화면에서 카드가 한 줄로 찌그러지지 않도록 반응형 CSS를 작성한다.
- 접근성을 위해 검색 input에 label을 연결하고 버튼 focus 스타일을 추가한다.
- 구현 후 변경 파일과 실행 방법을 요약해줘.

여기서 "아직 실제 API는 연결하지 말라"는 문장이 중요합니다. Codex는 종종 의욕적으로 API 호출 코드까지 넣으려고 합니다. 초보자 입장에서는 멋져 보이지만, 첫 단계에서는 오히려 복잡합니다.

Codex가 작업을 마치면 다음 명령어로 확인합니다.

npm run dev

브라우저에서 다음을 확인합니다.

  • 검색창과 버튼이 보이는가?

  • 빠른 도시 버튼을 눌렀을 때 카드 내용이 바뀌는가?

  • 5일 예보가 카드 리스트로 보이는가?

  • 브라우저 폭을 줄였을 때 화면이 자연스럽게 접히는가?

  • 개발자 도구 콘솔에 빨간 오류가 없는가?

이 단계에서 API 데이터는 중요하지 않습니다. 사용자가 앱을 어떻게 조작할지, 어떤 정보가 어디에 나타날지 먼저 확인하는 것이 목적입니다.

더미 데이터 구조를 실전 API에 가깝게 만들기

더미 데이터는 아무렇게나 만들어도 되지만, 실제 API와 너무 다르면 나중에 연결할 때 고생합니다. 그래서 Codex에게 데이터 구조를 정리하게 합니다.

현재 더미 날씨 데이터를 실제 API 연동에 적합한 내부 타입으로 정리해줘.

요구사항:
- `src/types/weather.ts`를 만들고 앱에서 사용할 WeatherSummary, ForecastDay 타입을 정의해.
- UI 컴포넌트는 OpenWeather 원본 응답을 직접 사용하지 말고, 앱 내부 타입만 사용하게 해.
- 더미 데이터는 `src/data/mockWeather.ts`로 분리해.
- 현재 날씨와 5일 예보 UI가 깨지지 않게 유지해.
- 타입 이름과 필드 이름은 초보자가 이해하기 쉽게 작성해.

이 작업은 매우 중요합니다. 외부 API 응답을 화면 컴포넌트에서 직접 사용하면 API 응답 구조가 조금만 바뀌어도 화면 코드 전체를 고쳐야 합니다. 반대로 API 응답을 우리 앱 내부 타입으로 한 번 변환하면, 화면은 안정적으로 유지됩니다.

예를 들어 내부 타입은 이런 모습이면 충분합니다.

export type WeatherSummary = {
  city: string;
  country: string;
  temperature: number;
  feelsLike: number;
  humidity: number;
  windSpeed: number;
  description: string;
  iconUrl: string;
};

export type ForecastDay = {
  date: string;
  minTemperature: number;
  maxTemperature: number;
  description: string;
  iconUrl: string;
};

여기서 포인트는 WeatherSummary가 OpenWeather의 원본 응답과 같지 않다는 것입니다. 원본 응답에는 main.temp, main.feels_like, weather[0].description처럼 중첩된 값이 많습니다. 화면 컴포넌트가 이런 구조를 직접 알 필요는 없습니다. API 담당 코드가 "번역가" 역할을 하고, 화면은 번역된 결과만 받으면 됩니다.

날씨 API 선택하기

이번 예제에서는 OpenWeather API를 기준으로 설명합니다. OpenWeather는 현재 날씨 데이터와 예보 데이터를 제공하며, 현재 날씨 API는 전 세계 도시의 실시간 날씨 정보를 가져오는 용도로 사용할 수 있습니다.2 5일 예보 API는 5일 동안의 예보를 제공하고, 일반적으로 3시간 간격의 예보 목록을 다루게 됩니다.3

실습에서 사용할 대표 엔드포인트는 다음과 같습니다.

현재 날씨:
https://api.openweathermap.org/data/2.5/weather

5일 예보:
https://api.openweathermap.org/data/2.5/forecast

도시 이름으로 조회할 때는 보통 이런 쿼리 파라미터를 붙입니다.

?q=Seoul&appid=YOUR_API_KEY&units=metric&lang=kr

units=metric은 섭씨 온도를 받기 위해 사용합니다. 이 옵션을 빼면 온도가 켈빈 단위로 올 수 있어 초보자가 "왜 서울이 290도지?" 하고 놀라게 됩니다. lang=kr은 날씨 설명을 한국어에 가깝게 받기 위한 설정입니다.

다만 API 문서는 시간이 지나면서 세부 필드나 권장 방식이 바뀔 수 있습니다. 그래서 Codex에게 API 코드를 만들게 할 때는 "최신 공식 문서를 확인하고 반영하라"고 지시하는 습관이 좋습니다.

OpenWeather 공식 문서를 확인해서 현재 날씨와 5일 예보 API 요청 URL, 필수 쿼리 파라미터, 주요 응답 필드를 확인한 뒤 `src/api/weatherApi.ts`를 구현해줘.

조건:
- fetch를 사용한다. axios는 추가하지 않는다.
- API 키는 `import.meta.env.VITE_OPENWEATHER_API_KEY`에서 읽는다.
- 현재 날씨와 5일 예보를 각각 가져오는 함수를 만든다.
- 원본 API 응답 타입을 최소한으로 정의하고, 앱 내부 타입으로 변환하는 mapper 함수를 만든다.
- API 키가 없으면 네트워크 요청을 보내지 말고 친절한 한국어 오류를 반환한다.
- 404, 401, 429, 네트워크 실패를 구분해서 처리한다.
- 구현 후 어떤 문서를 기준으로 했는지 요약해줘.

환경 변수로 API 키 분리하기

Vite에서는 브라우저 코드에서 환경 변수를 읽을 때 import.meta.env를 사용합니다. 그리고 클라이언트 코드에 노출할 변수는 기본적으로 VITE_ 접두사가 붙어야 합니다.4

프로젝트 루트에 .env.local 파일을 만듭니다.

VITE_OPENWEATHER_API_KEY=여기에_발급받은_API_KEY

그리고 .env.example도 만듭니다.

VITE_OPENWEATHER_API_KEY=your_openweather_api_key_here

.env.example은 팀원이나 미래의 내가 "어떤 환경 변수가 필요한지" 알 수 있게 해 주는 설명서입니다. 실제 키는 넣지 않습니다.

.gitignore에는 다음이 포함되어 있어야 합니다.

.env
.env.local
.env.*.local

여기서 초보자가 꼭 알아야 할 점이 있습니다. Vite의 VITE_ 환경 변수는 브라우저 번들에 포함될 수 있습니다. 즉, 완전한 의미의 서버 비밀 키 저장소가 아닙니다. Vite 공식 문서도 VITE_* 값에는 민감한 비밀을 넣지 말라고 안내합니다.4

그러면 "날씨 API 키를 쓰면 안 되나요?"라는 질문이 생깁니다. 학습용 프로젝트에서는 .env.local로 분리하고 Git에 올리지 않는 것만으로도 충분한 연습이 됩니다. 하지만 실서비스에서 호출량과 비용이 중요한 API 키라면 프론트엔드에서 직접 호출하지 말고, 서버나 서버리스 함수에서 대신 호출하는 구조가 더 안전합니다.

이번 장에서는 초보자 실습을 위해 프론트엔드에서 직접 호출하되, 코드에 하드코딩하지 않고 .env.local로 분리하는 방식으로 진행합니다.

fetch 함수 만들기

Codex가 만든 코드가 다음과 비슷한 구조인지 확인해 봅니다. 꼭 완전히 같을 필요는 없지만, 역할이 나뉘어 있어야 합니다.

const API_KEY = import.meta.env.VITE_OPENWEATHER_API_KEY;
const BASE_URL = "https://api.openweathermap.org/data/2.5";

export class WeatherApiError extends Error {
  constructor(
    message: string,
    public status?: number,
    public code?: string
  ) {
    super(message);
    this.name = "WeatherApiError";
  }
}

async function requestWeather<T>(path: string, params: URLSearchParams): Promise<T> {
  if (!API_KEY) {
    throw new WeatherApiError(
      "날씨 API 키가 설정되어 있지 않습니다. .env.local 파일에 VITE_OPENWEATHER_API_KEY를 추가해 주세요.",
      undefined,
      "MISSING_API_KEY"
    );
  }

  params.set("appid", API_KEY);
  params.set("units", "metric");
  params.set("lang", "kr");

  let response: Response;

  try {
    response = await fetch(`${BASE_URL}${path}?${params.toString()}`);
  } catch {
    throw new WeatherApiError(
      "네트워크 연결이 불안정합니다. 인터넷 연결을 확인한 뒤 다시 시도해 주세요.",
      undefined,
      "NETWORK_ERROR"
    );
  }

  if (!response.ok) {
    if (response.status === 404) {
      throw new WeatherApiError(
        "도시를 찾을 수 없습니다. 영문 도시명 또는 더 정확한 이름으로 다시 검색해 주세요.",
        404,
        "CITY_NOT_FOUND"
      );
    }

    if (response.status === 401) {
      throw new WeatherApiError(
        "날씨 API 키가 올바르지 않습니다. .env.local의 API 키를 확인해 주세요.",
        401,
        "INVALID_API_KEY"
      );
    }

    if (response.status === 429) {
      throw new WeatherApiError(
        "날씨 API 호출 한도를 초과했습니다. 잠시 후 다시 시도해 주세요.",
        429,
        "RATE_LIMITED"
      );
    }

    throw new WeatherApiError(
      "날씨 정보를 가져오는 중 문제가 발생했습니다. 잠시 후 다시 시도해 주세요.",
      response.status,
      "UNKNOWN_API_ERROR"
    );
  }

  return response.json() as Promise<T>;
}

이 코드는 초보자에게 길어 보일 수 있습니다. 하지만 하는 일은 단순합니다. API 키가 있는지 확인하고, 요청을 보내고, 실패 이유에 따라 한국어 메시지를 만들어 줍니다.

API 호출 함수는 이렇게 나눌 수 있습니다.

export async function getCurrentWeatherByCity(city: string) {
  const data = await requestWeather<OpenWeatherCurrentResponse>(
    "/weather",
    new URLSearchParams({ q: city })
  );

  return mapCurrentWeather(data);
}

export async function getForecastByCity(city: string) {
  const data = await requestWeather<OpenWeatherForecastResponse>(
    "/forecast",
    new URLSearchParams({ q: city })
  );

  return mapForecast(data);
}

여기서 mapCurrentWeather와 mapForecast가 원본 응답을 우리 앱 타입으로 바꾸는 함수입니다.

function getIconUrl(icon: string) {
  return `https://openweathermap.org/img/wn/${icon}@2x.png`;
}

function mapCurrentWeather(data: OpenWeatherCurrentResponse): WeatherSummary {
  const weather = data.weather?.[0];

  return {
    city: data.name,
    country: data.sys.country,
    temperature: Math.round(data.main.temp),
    feelsLike: Math.round(data.main.feels_like),
    humidity: data.main.humidity,
    windSpeed: data.wind.speed,
    description: weather?.description ?? "날씨 정보 없음",
    iconUrl: weather?.icon ? getIconUrl(weather.icon) : "",
  };
}

5일 예보는 조금 더 생각이 필요합니다. OpenWeather의 5일 예보는 하루 하나가 아니라 여러 시간대 예보 목록으로 옵니다. 그래서 "각 날짜별 대표값"을 뽑아야 합니다. 가장 쉬운 방법은 날짜별로 묶고, 각 날짜의 최저·최고 온도를 계산한 뒤 정오에 가까운 데이터를 대표 설명으로 쓰는 것입니다.

Codex에게 이 부분만 따로 맡기면 좋습니다.

`mapForecast` 함수를 개선해줘.

요구사항:
- OpenWeather 5일 예보 응답의 list 배열을 날짜별로 그룹화해.
- 각 날짜의 최저 온도와 최고 온도를 계산해.
- 대표 날씨 설명과 아이콘은 가능하면 12:00에 가까운 항목을 사용해.
- 결과는 최대 5개 ForecastDay만 반환해.
- 날짜 표시는 `YYYY-MM-DD` 문자열로 유지해.
- list가 비어 있으면 빈 배열을 반환해.

이렇게 요청하면 Codex가 복잡한 배열 처리 로직을 대신 작성해 줍니다. 우리는 결과가 맞는지만 확인하면 됩니다.

비동기 상태를 한곳에서 관리하기

이제 API 함수가 생겼으니 React 컴포넌트에서 호출해야 합니다. 하지만 컴포넌트 안에 fetch, try, catch, setState가 마구 섞이면 금방 복잡해집니다. 초보자에게는 커스텀 훅으로 분리하는 방식이 이해하기 좋습니다.

Codex에게 다음 작업을 맡깁니다.

날씨 조회 상태를 관리하는 `src/hooks/useWeatherSearch.ts` 훅을 만들어줘.

요구사항:
- 상태는 idle, loading, success, empty, error로 구분해.
- searchCity(city: string) 함수를 제공해.
- searchCity는 현재 날씨와 5일 예보를 함께 가져와.
- 둘 중 하나만 성공하는 애매한 상태는 만들지 말고, 실패하면 error로 처리해.
- 도시명이 빈 문자열이면 API를 호출하지 말고 안내 오류를 보여줘.
- 성공한 도시명은 최근 검색어에 저장해.
- 같은 도시가 중복 저장되지 않게 해.
- 최근 검색어는 localStorage에 최대 5개까지 저장해.
- retry() 함수로 마지막 검색을 다시 시도할 수 있게 해.

상태를 문자열로 나누면 화면 코드가 훨씬 읽기 쉬워집니다.

type WeatherStatus = "idle" | "loading" | "success" | "empty" | "error";

처음에는 idle입니다. 검색을 시작하면 loading, 성공하면 success, 결과가 없으면 empty, 실패하면 error가 됩니다. 이 구조를 만들면 UI에서는 상태에 따라 보여 줄 것만 결정하면 됩니다.

{status === "loading" && <LoadingSpinner />}

{status === "error" && (
  <ErrorMessage message={errorMessage} onRetry={retry} />
)}

{status === "success" && weather && (
  <WeatherCard weather={weather} />
)}

이렇게 쓰면 "지금 앱이 어떤 상태인가?"가 코드에서 바로 보입니다.

로딩, 빈 결과, 오류를 사용자가 이해하게 만들기

초보자가 만든 앱에서 가장 자주 빠지는 부분이 오류 처리입니다. console.error(error)만 찍고 화면에는 아무 일도 없는 것처럼 보이는 경우가 많습니다. 하지만 사용자는 콘솔을 보지 않습니다. 화면에 설명이 나와야 합니다.

Codex에게 오류 메시지를 정리하게 합니다.

날씨 앱의 상태별 안내 UI를 개선해줘.

요구사항:
- 로딩 중에는 스피너와 "날씨 정보를 가져오는 중입니다..." 문구를 보여줘.
- 스피너에는 `role="status"`와 `aria-live="polite"`를 적용해.
- API 키가 없을 때는 개발자가 해야 할 일을 알려줘.
- 도시를 찾을 수 없을 때는 예시 도시명을 안내해.
- 네트워크 실패일 때는 인터넷 연결 확인과 재시도 버튼을 보여줘.
- API 호출 한도 초과일 때는 잠시 후 다시 시도하라고 알려줘.
- 오류 박스에는 다시 시도 버튼을 넣어줘.
- 모든 메시지는 한국어로 작성해.

좋은 오류 메시지는 사용자를 탓하지 않습니다. "잘못 입력했습니다"보다 "도시를 찾을 수 없습니다. 예: Seoul, Busan, Tokyo처럼 입력해 보세요."가 낫습니다. 앱이 실패했을 때도 사용자가 다음 행동을 알 수 있어야 합니다.

예를 들어 오류 컴포넌트는 이렇게 단순할 수 있습니다.

type ErrorMessageProps = {
  message: string;
  onRetry?: () => void;
};

export function ErrorMessage({ message, onRetry }: ErrorMessageProps) {
  return (
    <section className="error-box" role="alert">
      <p>{message}</p>
      {onRetry && (
        <button type="button" onClick={onRetry}>
          다시 시도
        </button>
      )}
    </section>
  );
}

role="alert"는 보조 기술을 사용하는 사람에게 중요한 메시지가 나타났음을 알려 주는 역할을 합니다. 이런 작은 차이가 앱의 완성도를 높입니다.

최근 검색어 저장하기

최근 검색어는 작은 기능이지만 실전 앱 느낌을 크게 올려 줍니다. 사용자가 매번 Seoul을 입력하지 않아도 되기 때문입니다.

Codex에게 다음처럼 요청합니다.

최근 검색어 저장 기능을 별도 유틸로 분리해줘.

요구사항:
- `src/utils/recentSearches.ts`를 만든다.
- localStorage key는 `weather-dashboard:recent-cities`를 사용한다.
- getRecentSearches, addRecentSearch, clearRecentSearches 함수를 제공한다.
- 같은 도시는 대소문자 차이가 있어도 중복 저장하지 않는다.
- 가장 최근 검색어가 앞에 오게 한다.
- 최대 5개까지만 저장한다.
- localStorage를 사용할 수 없는 환경에서도 앱이 깨지지 않게 try/catch로 보호한다.

localStorage는 브라우저에 간단한 문자열 데이터를 저장하는 기능입니다. 서버 데이터베이스는 아니지만 최근 검색어처럼 가벼운 개인 설정을 저장하기에 좋습니다.

구현은 이런 방향이면 충분합니다.

const STORAGE_KEY = "weather-dashboard:recent-cities";
const MAX_RECENT = 5;

export function getRecentSearches(): string[] {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    return raw ? JSON.parse(raw) : [];
  } catch {
    return [];
  }
}

export function addRecentSearch(city: string): string[] {
  const trimmed = city.trim();
  if (!trimmed) return getRecentSearches();

  const current = getRecentSearches();
  const next = [
    trimmed,
    ...current.filter(
      item => item.toLowerCase() !== trimmed.toLowerCase()
    ),
  ].slice(0, MAX_RECENT);

  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
  } catch {
    // 저장 실패는 앱 사용을 막지 않는다.
  }

  return next;
}

여기서도 중요한 원칙이 있습니다. 최근 검색어 저장에 실패했다고 앱 전체가 멈추면 안 됩니다. 날씨 조회가 핵심 기능이고, 최근 검색어는 보조 기능입니다. 보조 기능 실패는 조용히 넘어가도 됩니다.

현재 날씨와 5일 예보를 동시에 가져오기

현재 날씨와 예보는 서로 다른 API입니다. 사용자가 도시를 검색하면 둘 다 필요합니다. 순서대로 호출할 수도 있지만, 서로 의존하지 않는 요청이라면 동시에 보내는 편이 빠릅니다.

const [currentWeather, forecast] = await Promise.all([
  getCurrentWeatherByCity(city),
  getForecastByCity(city),
]);

Promise.all은 여러 비동기 작업을 동시에 실행하고 모두 끝날 때까지 기다립니다. 하나라도 실패하면 전체가 실패합니다. 이번 앱에서는 현재 날씨와 예보가 함께 있어야 대시보드가 완성되므로 이 방식이 자연스럽습니다.

다만 API 호출 한도를 생각하면 검색할 때마다 너무 많은 요청을 보내지 않도록 주의해야 합니다. 입력창의 글자가 바뀔 때마다 자동 검색하는 방식은 초보 단계에서는 피하는 편이 좋습니다. 사용자가 버튼을 누르거나 Enter를 쳤을 때만 검색하게 만들면 API 호출을 줄일 수 있습니다.

브라우저에서 Codex에게 확인시키기

Codex의 장점은 코드 작성만이 아닙니다. 실행 결과를 보고 수정하는 흐름까지 맡길 수 있습니다. 내장 브라우저나 IDE 미리보기, 또는 사용자가 찍은 스크린샷을 바탕으로 개선을 요청할 수 있습니다.

먼저 직접 실행합니다.

npm run dev

그다음 Codex에게 이렇게 요청합니다.

개발 서버가 실행 중인 상태에서 앱 동작을 점검해줘.

확인할 것:
- 초기 화면이 깨지지 않는지
- Seoul 검색 시 현재 날씨와 5일 예보가 나오는지
- 존재하지 않는 도시명 `zzzzzz-test-city` 검색 시 한국어 오류가 나오는지
- API 키가 없을 때 안내 메시지가 나오는지
- 모바일 폭에서 검색 폼과 카드 레이아웃이 자연스러운지
- 콘솔 오류가 없는지

문제가 있으면 원인을 설명하고 최소 변경으로 수정해줘.

만약 Codex가 브라우저를 직접 볼 수 없는 환경이라면, 우리가 실행 결과를 알려 주면 됩니다.

실행해 보니 모바일 화면에서 검색 버튼이 입력창 아래로 내려가면서 너무 좁게 보입니다. 검색 폼이 모바일에서는 세로로, 태블릿 이상에서는 가로로 배치되도록 CSS를 수정해줘.

또는 스크린샷을 첨부할 수 있는 환경이라면 더 좋습니다.

첨부한 스크린샷을 보고 날씨 카드의 간격, 색 대비, 모바일 가독성을 개선해줘. 기능 코드는 건드리지 말고 CSS와 마크업 클래스만 수정해줘.

이렇게 구체적으로 말하면 Codex가 불필요하게 API 코드까지 건드릴 가능성이 줄어듭니다.

반응형 카드 레이아웃 다듬기

날씨 앱은 카드 UI와 잘 어울립니다. 현재 날씨는 큰 카드 하나, 5일 예보는 작은 카드 여러 개로 구성하면 보기 좋습니다. 하지만 모바일에서는 가로 카드가 너무 많으면 화면이 좁아집니다.

CSS는 다음 원칙을 따르면 충분합니다.

.dashboard {
  width: min(100% - 32px, 1080px);
  margin: 0 auto;
  padding: 32px 0;
}

.search-form {
  display: flex;
  gap: 12px;
}

.forecast-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 16px;
}

@media (max-width: 640px) {
  .search-form {
    flex-direction: column;
  }

  .search-form button {
    width: 100%;
  }
}

repeat(auto-fit, minmax(150px, 1fr))는 카드 그리드에서 자주 쓰는 패턴입니다. 화면이 넓으면 여러 열로 배치하고, 좁으면 자동으로 줄바꿈합니다. 초보자에게는 마법처럼 보이지만, "카드의 최소 너비는 150px이고 남는 공간은 나눠 가진다" 정도로 이해하면 됩니다.

Codex에게 CSS 개선을 맡길 때는 기능 변경 금지 조건을 붙이는 것이 좋습니다.

현재 날씨 대시보드의 CSS만 개선해줘.

조건:
- 기능 로직과 API 코드는 수정하지 마.
- 현재 날씨 카드는 강조되고, 5일 예보 카드는 균일한 그리드로 보여야 해.
- 모바일에서는 검색 폼이 세로로 쌓이게 해.
- 버튼과 입력창의 focus-visible 스타일을 명확히 해.
- 배경색과 글자색의 대비를 높여 읽기 쉽게 해.

키보드 접근성 확인하기

앱을 마우스로만 테스트하면 놓치는 부분이 많습니다. 키보드의 Tab 키만으로 검색창, 검색 버튼, 도시 버튼, 최근 검색어 버튼에 이동할 수 있어야 합니다. Enter 키로 검색도 가능해야 합니다.

검색 폼은 form으로 만들면 Enter 처리가 자연스럽습니다.

<form className="search-form" onSubmit={handleSubmit}>
  <label htmlFor="city-input">도시 검색</label>
  <input
    id="city-input"
    value={cityInput}
    onChange={event => setCityInput(event.target.value)}
    placeholder="예: Seoul, Busan, Tokyo"
  />
  <button type="submit">검색</button>
</form>

빠른 선택 도시는 div에 onClick을 붙이는 것보다 button을 쓰는 편이 좋습니다. 버튼은 기본적으로 키보드 접근성이 갖춰져 있기 때문입니다.

<button type="button" onClick={() => searchCity("Seoul")}>
  서울
</button>

Codex에게 접근성 점검을 맡길 수도 있습니다.

날씨 대시보드의 접근성을 점검하고 개선해줘.

확인할 것:
- 검색 input과 label이 올바르게 연결되어 있는지
- 클릭 가능한 요소가 div가 아니라 button인지
- Tab 이동 순서가 자연스러운지
- focus-visible 스타일이 보이는지
- 로딩 상태가 스크린 리더에 전달되는지
- 오류 메시지가 role="alert"로 전달되는지

기능 동작은 유지하고 필요한 마크업과 CSS만 수정해줘.

접근성은 나중에 붙이는 장식이 아닙니다. 처음부터 좋은 HTML을 쓰면 많은 부분이 자연스럽게 해결됩니다.

테스트 시나리오를 README에 적기

실전 프로젝트에서는 "내 컴퓨터에서 한 번 됐다"로 끝내지 않습니다. 어떻게 실행하고, 무엇을 확인해야 하는지 README에 남깁니다. Codex에게 README 작성을 맡기면 좋습니다.

README.md를 날씨 대시보드 프로젝트에 맞게 작성해줘.

포함할 내용:
- 프로젝트 소개
- 주요 기능
- 설치 방법
- `.env.local` 설정 방법
- `.env.example` 설명
- 개발 서버 실행 방법
- 빌드 방법
- 수동 테스트 시나리오
- API 키가 없거나 잘못되었을 때의 확인 방법

초보자도 따라 할 수 있게 한국어로 작성해줘.

수동 테스트 시나리오는 다음 정도면 충분합니다.

## 수동 테스트 시나리오

1. `npm run dev`로 개발 서버를 실행한다.
2. `Seoul`을 검색했을 때 현재 날씨와 5일 예보가 표시되는지 확인한다.
3. 빠른 선택 버튼 `Busan`을 눌렀을 때 도시가 바뀌는지 확인한다.
4. 존재하지 않는 도시명을 검색했을 때 한국어 오류 메시지가 나오는지 확인한다.
5. `.env.local`의 API 키를 비우고 새로고침했을 때 설정 안내 메시지가 나오는지 확인한다.
6. 브라우저 폭을 모바일 크기로 줄였을 때 검색 폼과 카드가 겹치지 않는지 확인한다.
7. Tab 키만 사용해서 검색창, 검색 버튼, 도시 버튼으로 이동할 수 있는지 확인한다.

README는 다른 사람을 위한 문서이기도 하지만, 미래의 나를 위한 문서이기도 합니다. 며칠 뒤 다시 프로젝트를 열었을 때 README가 있으면 훨씬 빨리 기억을 되살릴 수 있습니다.

빌드 오류 잡기

이제 최종 확인을 합니다.

npm run build

TypeScript 프로젝트에서는 개발 서버가 돌아가도 빌드에서 타입 오류가 드러나는 경우가 있습니다. 예를 들어 API 응답 타입에서 weather[0]이 없을 수도 있는데 무조건 접근하면 오류가 날 수 있습니다.

Codex에게 빌드 오류를 그대로 붙여 넣고 요청합니다.

`npm run build`에서 아래 오류가 발생했어.

[여기에 터미널 오류 붙여넣기]

요구사항:
- 오류 원인을 초보자도 이해할 수 있게 설명해줘.
- 최소 변경으로 수정해줘.
- API 응답에서 optional 값이 없을 때 앱이 깨지지 않게 방어 코드를 추가해줘.
- 수정 후 다시 실행할 명령어를 알려줘.

오류 메시지를 숨기지 말고 그대로 보여 주는 것이 좋습니다. Codex는 정확한 오류 메시지가 있을 때 훨씬 잘 고칩니다.

리팩터링: 파일 역할 정리하기

기능이 모두 동작하면 마지막으로 구조를 정리합니다. 리팩터링은 기능을 바꾸지 않고 코드의 모양을 개선하는 작업입니다. 초보자는 "작동하는데 왜 또 건드리지?"라고 생각할 수 있습니다. 하지만 파일 역할이 정리되어 있으면 다음 기능을 추가할 때 훨씬 편합니다.

추천 구조는 다음과 같습니다.

src/
  api/
    weatherApi.ts
  components/
    ErrorMessage.tsx
    ForecastList.tsx
    LoadingSpinner.tsx
    RecentSearches.tsx
    SearchForm.tsx
    WeatherCard.tsx
  data/
    mockWeather.ts
  hooks/
    useWeatherSearch.ts
  types/
    weather.ts
  utils/
    recentSearches.ts
  App.tsx
  App.css

Codex에게 리팩터링을 맡길 때는 반드시 "기능 변경 금지"를 넣습니다.

날씨 대시보드 코드를 리팩터링해줘.

조건:
- 기능 동작은 바꾸지 마.
- 컴포넌트, API, 타입, 유틸, 훅의 역할을 분리해.
- 중복 코드를 줄여.
- 초보자가 읽기 쉬운 이름을 사용해.
- 리팩터링 후 `npm run build`가 통과하도록 해.
- 변경한 파일과 이유를 요약해줘.

리팩터링이 끝나면 직접 diff를 확인합니다.

git diff
npm run build

문제가 없으면 커밋합니다.

git add .
git commit -m "Build weather dashboard with API states"

자주 생기는 문제와 해결 프롬프트

날씨 앱을 만들다 보면 몇 가지 문제가 자주 나옵니다. 당황하지 말고 문제를 작게 나누어 Codex에게 전달하면 됩니다.

API 키를 넣었는데도 "키가 없다"는 메시지가 나온다면 개발 서버를 재시작해야 할 수 있습니다. Vite는 환경 파일 변경 후 서버를 다시 시작해야 새 값을 읽는 경우가 많습니다.4

.env.local에 VITE_OPENWEATHER_API_KEY를 추가했는데 앱에서 여전히 API 키가 없다고 나와. Vite 환경 변수 로딩 방식 기준으로 원인을 점검해줘. 코드 수정이 필요하면 최소 변경으로 수정하고, 개발 서버 재시작이 필요한지도 알려줘.

도시 검색이 영어만 잘 되고 한국어 도시명이 잘 안 될 수도 있습니다. 이때는 앱 안내 문구를 개선하거나, 자주 쓰는 한국 도시 버튼은 내부적으로 영문 도시명으로 요청하게 만들 수 있습니다.

한국어 도시명 검색이 API에서 안정적으로 동작하지 않아. 빠른 선택 버튼은 화면에는 "서울"처럼 한국어로 보여 주되, API 요청에는 "Seoul"을 사용하도록 구조를 개선해줘. 검색 입력은 기존처럼 사용자가 입력한 값을 그대로 요청하게 유지해줘.

5일 예보가 같은 날짜로 여러 개 나오면 그룹화 로직을 확인해야 합니다.

5일 예보 카드에 같은 날짜가 여러 번 표시돼. OpenWeather forecast 응답의 list를 날짜별로 그룹화하는 로직을 점검하고, 날짜당 하나의 ForecastDay만 나오게 수정해줘.

모바일 화면이 깨지면 스크린샷이나 구체적인 증상을 주는 것이 좋습니다.

모바일 너비 390px에서 예보 카드가 너무 좁고 텍스트가 줄바꿈되어 읽기 어려워. CSS만 수정해서 모바일에서는 예보 카드가 세로 리스트처럼 보이게 개선해줘.

이번 장의 완성 기준

이번 장의 목표는 단순히 "날씨가 보인다"가 아닙니다. 실제 앱에서 필요한 상태를 다루는 것입니다. 다음 기준을 만족하면 성공입니다.

  • 도시 검색과 빠른 도시 버튼이 동작한다.

  • 현재 날씨와 5일 예보가 실제 API 데이터로 표시된다.

  • API 키는 코드에 직접 들어 있지 않고 .env.local에서 읽는다.

  • .env.example과 README가 있어 다른 사람이 설정 방법을 알 수 있다.

  • 로딩, 성공, 빈 결과, 네트워크 실패, 잘못된 도시명, 호출 한도 초과를 구분한다.

  • 오류 메시지는 한국어이며 사용자가 다음 행동을 알 수 있다.

  • 최근 검색어가 저장되고 다시 선택할 수 있다.

  • 모바일 화면과 키보드 접근성이 기본 수준 이상으로 다듬어져 있다.

  • npm run build가 통과한다.

  • Git 커밋으로 작업이 저장되어 있다.

Todo 앱에서는 사용자 입력과 상태 변경을 배웠습니다. 날씨 대시보드에서는 외부 세계와 연결되는 법을 배웠습니다. 여기서부터 앱 개발은 훨씬 현실에 가까워집니다. 네트워크는 실패할 수 있고, API 응답은 예상과 다를 수 있으며, 사용자는 우리가 생각한 대로만 입력하지 않습니다.

좋은 앱은 성공했을 때만 예쁜 앱이 아닙니다. 느릴 때도, 실패했을 때도, 사용자가 길을 잃지 않게 도와주는 앱입니다. Codex를 잘 활용하면 그 복잡한 경우의 수를 하나씩 점검하며 더 단단한 프론트엔드 앱을 만들 수 있습니다.

참고

1Codex | OpenAI의 AI 코딩 파트너

2OpenWeather Current weather data

3OpenWeather 5 day weather forecast

4Vite Env Variables and Modes

책 목록

Codex로 앱 만들기 완벽 가이드: 초보자를 위한 AI 코딩 에이전트 실전 입문

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기