TypeScript 기본 타입: 객체와 함수까지 strict로 익히기

··설명 약 8분·작성: 해비·블로그 소개

TypeScript strict에서 객체·배열·함수의 타입 계약은 어떻게 지정하나요?

TypeScript의 객체·배열·함수 타입은 어떤 값을 받고 돌려주는지 정하는 계약입니다. Task의 속성, Task[] 목록, 함수의 매개변수와 반환 타입을 맞추면 잘못된 title을 호출 단계에서 찾습니다.

적용 조건·범위: 기존 최소 예제 확인 기록: 별도 최소 파일을 TypeScript 7.0.2에서 --strict --noEmit으로 검사: 오류 예제 TS2345, 수정 예제 exit 0. 원래 TaskFlow 전체 파일 및 브라우저 실행은 미확인.

공식 문서 참고: TypeScript 핸드북: 기본형·배열·객체·함수 타입 · TypeScript 핸드북: 정적 검사와 런타임의 경계

직접 확인: 타입 오류와 수정

TypeScript 기본 타입: 객체와 함수까지 strict로 익히기 · 독립 실습

얻을 결과: Task[]와 title: string을 연결해 숫자 title을 TS2345로 막고 정상 입력을 통과시킵니다.

예제 크게 보기 (새 탭)
type Task = { id: string; title: string; completed: boolean };
function addTask(tasks: Task[], title: string): Task[] {
  return [...tasks, { id: '1', title, completed: false }];
}
addTask([], 42);

예상 오류: TS2345: number 인수는 string 매개변수에 전달할 수 없습니다. 문자열 title을 넘깁니다.

type Task = { id: string; title: string; completed: boolean };
function addTask(tasks: Task[], title: string): Task[] {
  return [...tasks, { id: '1', title, completed: false }];
}
const updated = addTask([], '공부하기');
console.log(updated[0]?.title);
직접 실행: 환경 준비와 전체 파일

단독 검사는 tsconfig.json이 없는 새 폴더에서 example.ts 한 파일씩 교체하며 진행합니다. TypeScript가 없다면 설치 글에서 Node.js/npm·TypeScript 준비를 확인하세요. 타입 검사 성공은 코드 실행 성공과 구분합니다.

독자 질문: Task와 함수를 타입으로 묶으면 잘못된 title을 어디서 발견할 수 있을까요?

얻을 결과: Task[]와 title: string을 연결해 숫자 title을 TS2345로 막고 정상 입력을 통과시킵니다.

다음 코드를 별도 example.ts에 복사합니다.

오류 예제를 아래 코드로 교체합니다.

npx tsc --strict --noEmit example.ts

기존 최소 예제 확인 기록: 별도 최소 파일을 TypeScript 7.0.2에서 --strict --noEmit으로 검사: 오류 예제 TS2345, 수정 예제 exit 0. 원래 TaskFlow 전체 파일 및 브라우저 실행은 미확인.

이 예제의 id는 설명용 고정값입니다. 실제 앱에서는 중복되지 않는 ID 생성 규칙을 별도로 적용하세요.

다음 학습: 05회차에서 같은 Task 객체 구조를 type과 interface로 표현해 선택 기준을 비교합니다.

primitive 타입

const title: string =
  '공부하기';


const count: number =
  3;


const completed: boolean =
  false;

실제 코드에서는 초기값으로 충분히 추론되는 지역 변수에 타입을 반복 작성할 필요는 없습니다.

const title =
  '공부하기';

배열 타입

const titles: string[] = [
  'TypeScript',
  'React',
];

Generic Array 문법도 가능합니다.

const titles:
  Array<string> = [
    'TypeScript',
  ];

프로젝트에서는 한 스타일을 일관되게 사용합니다.

객체 타입

type Task = {
  id: string;
  title: string;
  completed: boolean;
};

객체:

const task: Task = {
  id: 'task-1',
  title: 'TypeScript 공부',
  completed: false,
};

property가 빠지거나 타입이 다르면 오류가 발생합니다.

함수 매개변수

function getTaskTitle(
  task: Task,
): string {
  return task.title;
}

task는 Task 구조만 받을 수 있습니다.

반환 타입

TypeScript가 반환 타입을 추론할 수도 있습니다.

function isCompleted(
  task: Task,
) {
  return task.completed;
}

boolean으로 추론됩니다.

하지만 공개 helper나 중요한 함수 계약에서는 반환 타입을 명시하면 의도가 더 분명할 수 있습니다.

function isCompleted(
  task: Task,
): boolean {
  return task.completed;
}

void

값을 반환하지 않는 함수:

function logTask(
  task: Task,
): void {
  console.log(task.title);
}

void는 “함수가 runtime에서 절대 어떤 값도 반환할 수 없다”는 단순 의미보다 호출자가 반환값을 사용하지 않는 함수 타입 계약으로 이해하는 편이 좋습니다.

함수 타입

함수 자체의 타입도 정의할 수 있습니다.

type TaskPredicate = (
  task: Task,
) => boolean;
const isActive:
  TaskPredicate =
  (task) =>
    !task.completed;

Task 배열

const tasks: Task[] = [
  {
    id: '1',
    title: 'TypeScript',
    completed: false,
  },
];

map에서도 task가 Task로 추론됩니다.

const titles =
  tasks.map(
    (task) =>
      task.title,
  );

implicit any 오류

strict 환경에서 다음 함수는 문제가 됩니다.

function deleteTask(id) {
  ...
}

id 타입이 없고 추론 근거도 없습니다.

수정:

function deleteTask(
  id: string,
): void {
  ...
}

unknown과 any는 아직 구분만 하기

외부 값에 any를 쓰면 타입 검사가 사라집니다.

function read(value: any) {
  return value.title;
}

unknown을 사용하면 검사 전 접근을 막을 수 있습니다.

function read(
  value: unknown,
) {
  // 바로 value.title 사용 불가
}

자세한 narrowing은 6회차에서 배웁니다.

자주 하는 실수

모든 변수에 타입 명시

지역 상수까지 타입을 반복하면 코드가 장황해집니다.

반환 타입을 무조건 생략

공개 함수나 중요한 도메인 로직은 반환 계약을 명시하는 것이 읽기 쉬울 수 있습니다.

객체를 object 타입으로 작성

const task: object = {
  ...
};

object는 구체적인 property 구조를 표현하지 않습니다.

Task처럼 실제 shape를 정의합니다.

학습 완료 체크리스트

숫자 title과 빈 문자열 title은 왜 다르게 검사될까요? number는 string 계약에 어긋나지만 빈 문자열도 string이므로 유효한 제목 정책은 실행 시 확인해야 합니다.


작은 변형으로 확인하기

addTask의 두 번째 인수를 빈 문자열로 바꾸면 타입 검사는 통과합니다. string 타입과 비어 있지 않은 제목이라는 실행 시 규칙이 다른 이유를 설명하세요.

선택 실습: 할 일 앱에 적용하기

TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 JavaScript 코드를 TypeScript로 확장합니다.

TaskFlow의 핵심 Task 객체와 CRUD 함수에 첫 타입 계약을 추가합니다.

선택: 연재 프로젝트와 전체 자료

연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.

TaskFlow 프로젝트 적용

TypeScript 기본 타입: 객체와 함수까지 strict로 익히기 · 단계별 TaskFlow

타입을 적용한 TaskFlow에서 작업을 추가·완료 전환·삭제하세요. 타입만 바뀐 단계는 이전과 화면이 같습니다.

예제 크게 보기 (새 탭)

변경 설명과 부분 예제를 읽은 뒤, 시작·완료 파일과 적용 순서에서 필요한 선언·import·화면 연결을 포함한 전체 파일을 확인하세요. 이번 단계의 변경 이유와 적용 위치도 함께 확인합니다.

시작본과 이번 변경

TypeScript 7 설치와 tsconfig strict 첫 실행의 완료 체크포인트를 기준으로 이어갑니다. 변경하지 않는 파일은 그 시작 상태를 유지합니다.

src/types/task.ts에 Task를 정의하고 src/core/tasks.ts에서 import type으로 가져옵니다. addTask·toggleTask·deleteTask의 입력과 Task[] 반환 계약을 적용하고 숫자 title 인수를 거절하는지 검사합니다.

이전 체크포인트

typescript-step-03-install

이번 목표

TaskFlow의 핵심 Task 객체와 CRUD 함수에 첫 타입 계약을 추가합니다.

전체 파일·코드 보기

src/types/task.ts

export type Task = {
  id: string;
  title: string;
  completed: boolean;
};

src/core/tasks.ts

import type {
  Task,
} from '../types/task';


export function addTask(
  tasks: Task[],
  title: string,
): Task[] {
  const task: Task = {
    id:
      crypto.randomUUID(),
    title,
    completed: false,
  };


  return [
    ...tasks,
    task,
  ];
}


export function toggleTask(
  tasks: Task[],
  id: string,
): Task[] {
  return tasks.map(
    (task) =>
      task.id === id
        ? {
            ...task,
            completed:
              !task.completed,
          }
        : task,
  );
}


export function deleteTask(
  tasks: Task[],
  id: string,
): Task[] {
  return tasks.filter(
    (task) =>
      task.id !== id,
  );
}

Before / After

Before
tasks와 id, title의 타입 계약 없음


After
Task[]
string id
string title
Task[] 반환

실행 확인

npx tsc --noEmit

의도적으로 addTask에 number title을 전달하면 오류가 나는지 확인한 뒤 정상 코드로 복원합니다.

중간부터 시작하는 전체 파일과 적용 순서

이번 글 시작 ZIP · 이번 글 완료 ZIP

시작 ZIP은 직전 프로젝트 단계 typescript-step-03-install의 실제 완료 파일입니다. 프로젝트 순서를 이어가는 기준이며 사이트의 기술 읽기 순서와는 구분합니다.

  1. 시작 ZIP을 새 폴더에 풉니다. 이 폴더가 작업 루트이며 기존 체크포인트 파일을 별도로 찾을 필요가 없습니다.
  2. Task 계약을 core/tasks.ts의 추가·완료·삭제 함수에 적용합니다. typed main을 index.html에 연결하고 원래 전체 JavaScript 화면은 javascript.html로 보존합니다.
  3. 아래 변경 파일을 같은 상대 경로에 추가하거나 전체 교체합니다. 표시하지 않은 파일과 images는 시작본 그대로 유지합니다. package-lock.json은 완료 ZIP에 있는 파일을 함께 적용합니다.
  4. 작업 루트에서 명령을 실행하고 예상 결과를 확인합니다. 완료 ZIP은 직접 적용한 결과를 비교하는 기준입니다.

추가·교체 파일

  • index.html — 전체 교체
  • CHANGES.md — 전체 교체
  • README.md — 전체 교체
  • src/core/tasks.ts — 추가
  • src/main.ts — 추가
  • javascript.html — 추가
  • vite.config.mjs — 추가

실행과 확인

npm ci
npm run dev
npm run typecheck
npm test
npm run build

개발 서버는 실행 상태로 두고 다른 터미널에서 검사 명령을 실행하세요. Node.js 24.18.0에서 검증했습니다. JavaScript는 http://127.0.0.1:4173, TypeScript는 Vite 실행 출력의 주소로 접속합니다.

예상 결과: 타입 화면에서 2개 초기 작업의 추가·완료·삭제가 작동합니다. 원본 JS의 날짜·URL·비동기 기능은 별도 원본 링크에서 유지됩니다.

기존 ZIP 검증 기록: TypeScript strict 검사·긍정/부정 타입 계약·런타임 검사·Vite 빌드 통과. 시작 ZIP에 이 글의 변경 파일만 적용한 결과와 완료 ZIP의 모든 파일 내용이 일치했습니다. 브라우저 화면 배치·실제 기기·외부 CDN 가용성은 이 검사에 포함하지 않았습니다.

이 절의 전체 파일은 ZIP과 일치합니다. 위쪽 원문의 짧은 예제나 생략 표시가 있는 부분 코드는 개념·변경 위치 설명용입니다. 서버 저장과 인증은 제공하지 않으며 JSON fixture는 로컬 읽기 실습용입니다.

복사할 변경 파일 전체 코드

index.html — 전체 파일

<!doctype html>
<html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>TaskFlow TypeScript</title><link rel="stylesheet" href="./style.css"></head><body><h1>TaskFlow TypeScript</h1><p>Task 계약 기반 작업 관리</p><main id="ts-taskflow"></main><p><a href="./javascript.html">기존 JavaScript 최종판</a></p><script type="module" src="./src/main.ts"></script></body></html>

src/core/tasks.ts — 전체 파일

import type {
  Task,
} from '../types/task.js';


export function addTask(
  tasks: Task[],
  title: string,
): Task[] {
  const task: Task = {
    id:
      crypto.randomUUID(),
    title,
    completed: false,
  };


  return [
    ...tasks,
    task,
  ];
}


export function toggleTask(
  tasks: Task[],
  id: string,
): Task[] {
  return tasks.map(
    (task) =>
      task.id === id
        ? {
            ...task,
            completed:
              !task.completed,
          }
        : task,
  );
}


export function deleteTask(
  tasks: Task[],
  id: string,
): Task[] {
  return tasks.filter(
    (task) =>
      task.id !== id,
  );
}

src/main.ts — 전체 파일

import type { Task } from './types/task.js';
import { addTask, toggleTask, deleteTask } from './core/tasks.js';
let tasks: Task[] = [
  {id:'1',title:'TypeScript 공부',completed:false},
  {id:'2',title:'TaskFlow 화면 확인',completed:true},
];
const root = document.querySelector<HTMLElement>('#ts-taskflow');
if (!root) throw new Error('TaskFlow mount missing');
root.innerHTML = '<form id="typed-form"><label for="typed-title">작업 이름</label><input id="typed-title"><button>추가</button></form><p id="typed-summary" aria-live="polite"></p><ul id="typed-list"></ul>';
const form = root.querySelector<HTMLFormElement>('form')!;
const input = root.querySelector<HTMLInputElement>('input')!;
const list = root.querySelector<HTMLUListElement>('ul')!;
function render() {
  list.replaceChildren();
  document.querySelector('#typed-summary')!.textContent = `전체 ${tasks.length} · 완료 ${tasks.filter(t=>t.completed).length}`;
  for (const task of tasks) {
    const li = document.createElement('li');
    const text = document.createElement('span'); text.textContent=task.title;
    const toggle = document.createElement('button'); toggle.textContent=task.completed?'완료 취소':'완료';
    toggle.onclick=()=>{tasks=toggleTask(tasks,task.id);render();};
    const remove = document.createElement('button'); remove.textContent='삭제';
    remove.onclick=()=>{tasks=deleteTask(tasks,task.id);render();};
    li.append(text,toggle,remove);list.append(li);
  }
}
form.onsubmit=event=>{event.preventDefault();const title=input.value.trim();if(!title){input.focus();return;}tasks=addTask(tasks,title);input.value='';render();input.focus();};
render();

javascript.html — 전체 파일

<!DOCTYPE html>
<html lang="ko">
 <head>
  <meta charset="utf-8"/>
  <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
  <title>
   TaskFlow - 간단한 작업 정리 서비스
  </title>
  <link href="./style.css" rel="stylesheet"/>
  <meta content="TaskFlow의 주요 기능과 요금제, 문의 방법을 확인할 수 있는 작업 관리 서비스 소개 페이지입니다." name="description"/>
 </head>
 <body>
  <a class="skip-link" href="#main">
   본문으로 바로가기
  </a>
  <header class="site-header">
   <div class="page site-header__inner">
    <a class="brand" href="#home">
     TaskFlow
    </a>
    <nav aria-label="주요 메뉴" class="nav">
     <a href="#features">
      기능
     </a>
     <a href="#pricing">
      요금제
     </a>
     <a href="#contact">
      문의
     </a>
     <a href="#faq">
      FAQ
     </a>
    </nav>
   </div>
  </header>
  <main class="page" id="main" tabindex="-1">
   <div class="service-layout">
    <div>
     <section class="hero" id="home">
      <div>
       <h1>
        해야 할 일을 한눈에 정리하세요
       </h1>
       <p>
        TaskFlow는 오늘의 작업을 빠르게 정리하는 서비스입니다.
       </p>
       <a class="button-link" href="#contact">
        시작 문의하기
       </a>
      </div>
      <img alt="TaskFlow 서비스 소개 페이지의 기능·문의 화면" class="hero__image" height="800" src="./images/hero.webp" width="1200"/>
     </section>
     <section id="features">
      <h2>
       주요 기능
      </h2>
      <div class="feature-grid">
       <article class="card feature-card">
        <span class="feature-card__badge">
         NEW
        </span>
        <h3>
         빠른 작업 추가
        </h3>
        <p>
         해야 할 일을 바로 기록할 수 있습니다.
        </p>
        <img alt="TaskFlow 기능 카드 세 개" class="feature-card__image" height="450" loading="lazy" src="./images/feature-board.webp" width="800"/>
       </article>
       <article class="card">
        <h3>
         간단한 분류
        </h3>
        <p>
         해야 할 일을 구분합니다.
        </p>
       </article>
       <article class="card">
        <h3>
         진행 확인
        </h3>
        <p>
         완료 여부를 확인합니다.
        </p>
       </article>
       <article class="card task-preview">
        <div class="task-preview__content">
         <h3>
          아주 긴 작업 제목과 URL
         </h3>
         <p>
          https://example.com/very-long-task-url-without-normal-break-points-123456789
         </p>
        </div>
        <a href="#contact">
         문의
        </a>
       </article>
      </div>
     </section>
     <section class="faq" id="faq">
      <h2>
       자주 묻는 질문
      </h2>
      <details>
       <summary>
        TaskFlow는 어떤 서비스인가요?
       </summary>
       <p>
        해야 할 일을 빠르게 정리하는 작업 관리 서비스 예제입니다.
       </p>
      </details>
      <details>
       <summary>
        모바일에서도 볼 수 있나요?
       </summary>
       <p>
        작은 화면부터 설계한 반응형 레이아웃을 사용합니다.
       </p>
      </details>
      <details>
       <summary>
        문의는 어디서 하나요?
       </summary>
       <p>
        아래 문의 폼을 이용할 수 있습니다.
       </p>
      </details>
     </section>
     <section id="pricing">
      <h2>
       요금제
      </h2>
      <div class="pricing-grid">
       <article class="card">
        <h3>
         Basic
        </h3>
        <p>
         개인 학습용
        </p>
       </article>
       <article class="card">
        <h3>
         Pro
        </h3>
        <p>
         더 많은 프로젝트
        </p>
       </article>
      </div>
      <div aria-label="요금제 비교표 가로 스크롤" class="table-scroll" role="region" tabindex="0">
       <table class="pricing-table">
        <caption>
         TaskFlow 요금제 기능 비교
        </caption>
        <thead>
         <tr>
          <th scope="col">
           항목
          </th>
          <th scope="col">
           Basic
          </th>
          <th scope="col">
           Pro
          </th>
         </tr>
        </thead>
        <tbody>
         <tr>
          <th scope="row">
           프로젝트
          </th>
          <td>
           3개
          </td>
          <td>
           무제한
          </td>
         </tr>
         <tr>
          <th scope="row">
           작업 보관
          </th>
          <td>
           30일
          </td>
          <td>
           무제한
          </td>
         </tr>
         <tr>
          <th scope="row">
           팀 공유
          </th>
          <td>
           없음
          </td>
          <td>
           지원
          </td>
         </tr>
        </tbody>
       </table>
      </div>
     </section>
     <section id="contact">
      <h2>
       문의하기
      </h2>
      <form>
       <div class="form-field">
        <label for="name">
         이름
        </label>
        <input autocomplete="name" id="name" name="name" type="text"/>
       </div>
       <div class="form-field">
        <label for="email">
         이메일
        </label>
        <input autocomplete="email" id="email" name="email" type="email"/>
       </div>
       <div class="form-field">
        <label for="message">
         문의 내용
        </label>
        <textarea id="message" name="message" rows="5"></textarea>
       </div>
       <button type="submit">
        문의 보내기
       </button>
      </form>
     </section>
     <div class="feature-slot">
      <article class="card feature-card">
       <img alt="TaskFlow 기능 카드 세 개" class="feature-card__image" height="450" loading="lazy" src="./images/feature-board.webp" width="800"/>
       <div>
        <h3>
         작업 카드
        </h3>
        <p>
         넓은 공간에서는 이미지와 설명을 나란히 배치합니다.
        </p>
       </div>
      </article>
     </div>
    </div>
    <aside aria-label="기능 미리보기">
     <div class="feature-slot">
      <article class="card feature-card">
       <img alt="TaskFlow 기능 카드 세 개" class="feature-card__image" height="450" loading="lazy" src="./images/feature-board.webp" width="800"/>
       <div>
        <h3>
         작업 카드
        </h3>
        <p>
         넓은 공간에서는 이미지와 설명을 나란히 배치합니다.
        </p>
       </div>
      </article>
     </div>
    </aside>
   </div>
  <section
  class="task-demo"
  id="task-demo"
  aria-labelledby="task-demo-title"
>
  <h2 id="task-demo-title">작업 관리 데모</h2>
  <p>JavaScript 과정에서 이 영역을 단계적으로 동작하게 만듭니다.</p>


  <form class="task-form" id="task-form" novalidate>
    <div class="form-field">
      <label for="task-title">작업 이름</label>
      <input
        id="task-title"
        name="title"
        type="text"
        maxlength="80"
        required
        aria-describedby="task-message"
      />
    </div>


    <button type="submit">작업 추가</button>
  <div class="form-field">
  <label for="task-due-at">마감 시각</label>
  <input
    id="task-due-at"
    name="dueAt"
    type="datetime-local"
  />
</div>
</form><div class="task-controls">
  <label for="task-status">상태</label>
  <select id="task-status">
    <option value="all">전체</option>
    <option value="pending">미완료</option>
    <option value="completed">완료</option>
  </select>


  <label for="task-search">검색</label>
  <input id="task-search" type="search" />
</div>



  <p id="task-message" aria-live="polite"></p>


  <div id="task-summary" aria-live="polite"></div>
  <ul class="task-list" id="task-list"></ul>
</section>
<p id="task-load-state" aria-live="polite"></p></main>
  <footer class="site-footer">
   <div class="page">
    <p>
     © TaskFlow
    </p>
   </div>
  </footer>
  <a class="help-button" href="#contact">
   문의
  </a>
 <script type="module" src="./js/app.js"></script></body>
</html>

vite.config.mjs — 전체 파일

import { fileURLToPath } from 'node:url';
export default {build:{rollupOptions:{input:{main:fileURLToPath(new URL('./index.html',import.meta.url)),javascript:fileURLToPath(new URL('./javascript.html',import.meta.url))}}}};

완료 체크포인트

typescript-step-04-basic-types

다음 학습

다음 글에서는 객체 타입을 작성할 때 type과 interface를 어떤 기준으로 선택할지 TaskFlow 모델을 이용해 비교합니다.

다음 글: TypeScript type vs interface: 객체 타입 설계 기준

글 소개·학습 준비·전체 목차
TypeScript 기본 타입: 객체와 함수까지 strict로 익히기

TypeScript · 4 / 22 · 필수 학습 · 기본 타입과 함수

객체와 함수를 strict 타입 계약으로 어떻게 연결할까요? Task의 속성 타입과 Task[] 배열, 함수의 매개변수·반환 타입을 맞추면 잘못된 title을 호출 단계에서 찾을 수 있습니다. 추론이 충분한 지역 변수는 그대로 두고 독립 addTask 예제로 문자열 입력과 숫자 입력을 비교합니다.

이번 글에서는 primitive 타입을 나열하는 데서 끝나지 않고 TaskFlow의 Task 객체와 addTask, toggleTask 같은 함수에 타입을 적용합니다.

이번 글에서 배우는 내용

  • string, number, boolean
  • 배열 타입
  • 객체 타입
  • 함수 매개변수 타입
  • 반환 타입
  • void
  • 타입 추론
  • strict에서 implicit any를 피하는 방법

단독 학습 선수 지식

JavaScript 배열·객체·함수와 strict 타입 검사을 알면 됩니다. 이전 TaskFlow 파일 없이 직접 확인 예제로 학습할 수 있습니다.

목차

이 글이 도움이 되었나요?

조회 중

읽음 기록

읽음 — / 전체 22개 · 필수 21개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기