C-5: 모던 문법과 모듈
안녕하세요, 홍순구 튜터입니다. 지난 시간 우리는 배열과 객체를 자유자재로 다루는 도구를 손에 넣었어요. map·filter·reduce로 데이터를 돌리고 거르고 합쳤고, 객체와 구조 분해, 스프레드까지 배웠죠. 마지막엔 피드 데이터를 카드 문자열로 바꿔 화면에 뿌리는 흐름을 만들었어요.
그때 카드 문자열을 이렇게 이어 붙였던 거 기억하세요?
caption + " — 좋아요 " + likeCount + "개"
글자와 변수를 섞을 때마다 따옴표와 +가 줄줄이 붙어서 슬슬 눈이 어지러웠죠. 오늘 첫 시간에 이걸 훨씬 깔끔하게 바꾸는 템플릿 리터럴을 배워요. 그리고 데이터가 비어 있을 때 코드가 터지지 않게 막아주는 **옵셔널 체이닝(?.)**과 Nullish 병합(??), 마지막으로 지금까지 한 파일에 쌓아온 main.js를 역할별로 나누는 모듈(import/export)까지 다룹니다.
지난 시간 (C-4) 오늘 (C-5)
┌────────────────────────┐ ┌────────────────────────┐
│ caption + " " + likes │ ──▶ │ `${caption} ${likes}개` │
│ post.author.name (터짐) │ │ post.author?.name ?? "" │
│ main.js 한 파일 (493줄) │ │ data.js · format.js 분리 │
└────────────────────────┘ └────────────────────────┘
오늘 끝나면 우리 코드는 한 파일에 다 쌓여 있던 모양에서 벗어나, 데이터는 데이터 파일에, 화면 표현은 표현 파일에 나뉘어요. 진짜 프로젝트의 구조를 처음으로 갖추는 시간이에요.
💡 오늘 수업의 핵심 — "글자와 변수는 백틱 ${}로 깔끔하게 섞고, 없을지 모르는 값은 ?.와 ??로 안전하게 다루며, 길어진 코드는 역할별 파일로 나눠 import/export로 잇는다." 🎯
🎯 학습 목표
- 템플릿 리터럴(백틱
`과${})로 글자 안에 변수와 식을 바로 끼워 넣습니다. - 백틱으로 여러 줄 문자열을 줄바꿈 그대로 만듭니다.
- 옵셔널 체이닝(
?.)으로 없을지 모르는 중첩 값을 에러 없이 읽습니다. - Nullish 병합(
??)으로 값이 비었을 때만 기본값을 채우고,||와의 차이를 구분합니다. export로 데이터·함수를 내보내고,import로 다른 파일에서 불러옵니다.<script type="module">이 왜 필요한지, 왜 Live Server로 열어야 하는지 이해합니다.data.js·format.js·main.js로 코드를 나눠, 세 파일이 협력하는 구조를 만듭니다.
Step 1: 템플릿 리터럴 — 백틱과 ${}
지난 시간 카드 문자열을 만들 때 +로 글자와 변수를 이어 붙였죠. 변수가 하나둘 늘면 따옴표를 열고 닫고, +를 끼우고, 다시 따옴표... 실수하기 딱 좋아요. 그래서 모던 JavaScript는 템플릿 리터럴(template literal)이라는 더 편한 문법을 줘요. "템플릿"은 빈칸에 값을 끼워 넣는 틀이라는 뜻이에요.
핵심은 두 가지예요. 따옴표(' ") 대신 **백틱(`)으로 감싸고, 변수를 넣을 자리에 ${변수}**라고 적어요. 백틱은 키보드 숫자 1 왼쪽, ~와 같은 키에 있어요.
// instagram-clone-frontend/js/main.js
const nickname = "minji";
const heartCount = 120;
console.log(nickname + " — 좋아요 " + heartCount + "개"); // C-4 방식 (+ 연결)
console.log(`${nickname} — 좋아요 ${heartCount}개`); // C-5 방식 (백틱 + ${})
두 줄 다 결과는 똑같이 minji — 좋아요 120개예요. 그런데 아래쪽이 훨씬 읽기 좋죠? 따옴표를 열고 닫을 필요도, +를 끼울 필요도 없어요. 글자는 글자대로 쭉 쓰고, 변수가 들어갈 자리에만 ${}로 구멍을 뚫는 거예요.
`${nickname} — 좋아요 ${heartCount}개`
└─┬─────┘ └──┬──────┘
변수 끼울 구멍 변수 끼울 구멍
│ │
"minji" 120
▼ ▼
"minji — 좋아요 120개"
${ } 안에는 식도 들어가요
${} 안에는 변수만이 아니라 식(표현식)도 넣을 수 있어요. 계산이든 비교든, 값 하나로 정리되는 거라면 뭐든요.
console.log(`이건 ${heartCount >= 100 ? "인기" : "일반"} 게시물이에요`);
console.log(`좋아요를 누르면 ${heartCount + 1}개가 돼요`);
첫 줄은 heartCount >= 100이 참이라 "인기"가 들어가서 이건 인기 게시물이에요가 돼요. 둘째 줄은 ${heartCount + 1}이 121로 계산돼서 좋아요를 누르면 121개가 돼요가 나오죠. 글자 한가운데서 바로 계산이 되니까, 따로 변수를 만들 필요도 없어요.
🙋 한번 직접 바꿔보세요.
heartCount를80으로 바꾸면 첫 줄이 어떻게 나올까요?"인기"가"일반"으로 바뀌겠죠. 숫자를 바꿔가며 콘솔에서 확인해보세요.
Step 2: 여러 줄 문자열 — 줄바꿈을 그대로
따옴표 문자열에는 불편한 점이 하나 더 있었어요. 여러 줄로 쓰려면 \n이라는 줄바꿈 기호를 직접 끼워야 했죠. "첫 줄\n둘째 줄" 이런 식으로요. 백틱은 이것도 해결해줘요. 백틱 안에서는 엔터를 친 그대로 줄바꿈이 들어가요.
// instagram-clone-frontend/js/main.js
const card = `작성자: ${nickname}
내용: 제주 여행 다녀왔어요
좋아요: ${heartCount}개`;
console.log(card);
이 코드는 콘솔에 세 줄로 그대로 찍혀요.
작성자: minji
내용: 제주 여행 다녀왔어요
좋아요: 120개
코드에서 엔터를 친 자리가 결과에서도 줄바꿈이 돼요. \n을 일일이 적을 필요가 없죠. 게다가 중간중간 ${}로 변수도 자유롭게 끼웠어요. 나중에 화면에 여러 줄짜리 내용을 그릴 때 아주 유용해요.
따옴표도 자유롭게
백틱 안에서는 작은따옴표(')나 큰따옴표(")를 그냥 써도 돼요. 백틱이 경계라서, 안쪽 따옴표는 그냥 글자로 취급되거든요.
console.log(`그녀는 "정말 예쁘다"고 말했어요`);
따옴표 문자열이었다면 "그녀는 \"정말 예쁘다\"고..."처럼 따옴표마다 \를 붙여 escape(이스케이프 — 특수문자를 글자로 취급하게 표시)해야 했어요. 백틱이면 그냥 쓰면 되죠. 큰따옴표가 자주 들어가는 문장일수록 백틱이 편해요.
Step 3: 옵셔널 체이닝 ?. — 없는 길을 안전하게
이제 데이터를 안전하게 다루는 문법으로 넘어가요. 실제 데이터는 항상 완벽하지 않아요. 어떤 게시물엔 작성자 정보가 있고, 어떤 건 없을 수 있죠. 이런 상황에서 코드가 터지는 걸 막는 게 오늘의 두 번째 주제예요.
게시물 객체가 작성자 정보를 중첩 객체로 담고 있다고 해볼게요. post.author.name처럼 점을 두 번 찍어 안쪽 값을 꺼내요.
// instagram-clone-frontend/js/main.js
const postA = { caption: "제주 여행", author: { name: "minji" } };
const postB = { caption: "평범한 일상" }; // author 가 아예 없어요
console.log(postA.author.name); // "minji"
// console.log(postB.author.name); // ❌ TypeError! author 가 undefined 라서 .name 에서 터져요
postA는 author가 있으니 postA.author.name이 "minji"로 잘 나와요. 그런데 postB엔 author가 아예 없어요. postB.author는 undefined인데, 거기에 또 .name을 찍으면 어떻게 될까요?
postB.author.name
│ │
undefined .name ← undefined 에는 .name 이 없어요!
▼
❌ TypeError: Cannot read properties of undefined (reading 'name')
undefined에는 꺼낼 속성이 없으니 그 자리에서 프로그램이 멈춰버려요(TypeError — 타입 에러). 게시물 하나 때문에 화면 전체가 안 그려지는 거죠. 이걸 막아주는 게 옵셔널 체이닝(optional chaining), 물음표 점 ?.이에요.
// ?. 를 끼우면, 중간이 없을 때 멈추고 undefined 를 돌려줘요 (에러 없음)
console.log(postB.author?.name); // undefined (터지지 않아요)
console.log(postA.author?.name); // "minji"
post.author?.name은 이렇게 읽어요. "author가 있으면 .name을 꺼내고, 없으면(null이나 undefined면) 거기서 멈추고 undefined를 돌려줘." 에러로 터지는 대신 조용히 undefined를 주니까, 게시물 하나가 비어 있어도 나머지는 멀쩡히 굴러가요.
postA.author?.name → author 있음 → "minji"
postB.author?.name → author 없음 → undefined (멈춤, 에러 X)
"이 값은 있을 수도, 없을 수도 있어(optional)"라는 자리에 ?.를 붙인다고 기억하면 돼요.
Step 4: Nullish 병합 ?? — 비었을 때만 기본값
?.로 안전하게 읽었더니 undefined가 나왔어요. 그런데 화면에 undefined라고 보여줄 순 없잖아요? 비어 있으면 "익명" 같은 기본값으로 바꿔주고 싶어요. 그때 쓰는 게 Nullish 병합 연산자, 물음표 둘 ??예요.
??는 이렇게 동작해요. 왼쪽이 null이거나 undefined면 오른쪽 기본값을, 아니면 왼쪽 값을 그대로 써요.
// instagram-clone-frontend/js/main.js
console.log(postB.author?.name ?? "익명"); // "익명" (undefined 라서 기본값)
console.log(postA.author?.name ?? "익명"); // "minji" (값이 있어서 그대로)
postB는 author가 없어 ?.가 undefined를 줬고, ??가 그걸 받아 "익명"으로 바꿨어요. postA는 "minji"라는 멀쩡한 값이 있으니 그대로 두죠. ?.로 안전하게 꺼내고 ??로 빈자리를 채우는, 둘은 단짝이에요.
?? 와 || 의 결정적 차이
"비었으면 기본값"이라면 C-1에서 배운 ||도 비슷하게 쓸 수 있을 것 같죠? 그런데 둘은 결정적으로 달라요. ||는 falsy한 값을 전부 '비었다'고 봐요. falsy에는 0, 빈 문자열 "", false도 포함돼요. 좋아요가 정확히 0개인 새 게시물을 생각해볼게요.
const zeroLikes = 0;
console.log(zeroLikes || "없음"); // "없음" ← 0 을 '비었다'고 오해 (틀림)
console.log(zeroLikes ?? "없음"); // 0 ← 0 은 멀쩡한 값으로 살림 (맞음)
zeroLikes || "없음"은 0을 falsy로 보고 "없음"으로 바꿔버려요. 좋아요가 진짜 0개인 게시물인데 "없음"이라고 나오니 틀렸죠. 반면 zeroLikes ?? "없음"은 0을 멀쩡한 숫자로 인정해서 그대로 0을 줘요. ??는 오직 null과 undefined만 "비었다"고 보거든요.
값이 0 일 때
┌──────────────┬─────────┬──────────────────────────┐
│ 연산자 │ 결과 │ 왜? │
├──────────────┼─────────┼──────────────────────────┤
│ 0 || "없음" │ "없음" │ 0 도 falsy → 비었다고 오해 │
│ 0 ?? "없음" │ 0 │ null/undefined 만 빈 값 │
└──────────────┴─────────┴──────────────────────────┘
숫자나 빈 문자열이 멀쩡한 값일 수 있다면 ??를 쓰는 게 안전해요. 좋아요 수, 댓글 수, 가격처럼 0이 의미 있는 데이터라면 더더욱요.
⚠️
?.(ES2020)와??(ES2020)는 비교적 최근에 표준에 들어온 문법이에요. 다행히 2026년 현재 모든 최신 브라우저에서 잘 돌아가니 마음 놓고 써도 돼요.
Step 5: 모듈 ① — 왜 나눌까 + export
이제 오늘의 큰 산, 모듈이에요. 지금까지 우리는 모든 코드를 main.js 한 파일에 차곡차곡 쌓아왔어요. C-1의 console.log부터 C-4의 피드 렌더링까지, 어느새 493줄이 됐죠. 파일이 길어질수록 원하는 코드를 찾기 어렵고, 로그인 고치려다 피드 코드를 건드리는 실수도 나기 쉬워요.
진짜 프로젝트는 코드를 역할별로 여러 파일에 나눠요. 데이터는 데이터 파일에, 화면 표현은 표현 파일에. 이렇게 나눈 각 파일을 모듈(module)이라고 불러요. 오늘은 첫걸음으로 피드 데이터를 data.js라는 새 파일로 떼어낼게요.
다른 파일에서 쓰려면, 그 값 앞에 export(내보내기)를 붙여야 해요. "이 값은 바깥에서 가져다 써도 돼요"라고 문을 열어주는 거예요.
// instagram-clone-frontend/js/data.js
// 피드 데이터를 main.js 에서 떼어낸 파일. 데이터만 모아둬요.
export const feedPosts = [
{
id: 1,
caption: "제주 여행 다녀왔어요",
likeCount: 120,
author: { name: "minji", verified: true },
location: "제주도"
},
{
id: 2,
caption: "오늘의 맛집 발견",
likeCount: 0, // 아직 아무도 안 누른 새 게시물
author: { name: "jaehoon" } // location 이 없어요
},
// ... 게시물 3, 4 (id 3 은 author 가 통째로 없어요 → 익명)
];
export const feedPosts = [...] — 평소처럼 const로 배열을 만들고, 앞에 export만 붙였어요. 이제 feedPosts는 다른 파일에서 불러다 쓸 수 있는 값이 됐어요. 데이터를 일부러 다양하게 만들었어요. 2번 게시물은 location이 없고 좋아요가 0개, 3번은 author가 통째로 없죠. 조금 전 배운 ?.와 ??가 진가를 발휘할 데이터예요.
type="module" — 모듈을 켜는 스위치
그런데 import/export는 아무 데서나 되는 게 아니에요. HTML에서 스크립트를 불러올 때 **type="module"**이라고 표시해줘야 켜져요.
<!-- instagram-clone-frontend/feed.html -->
<script type="module" src="js/main.js"></script>
지금까지는 <script src="js/main.js">였는데, type="module"을 더했어요. 이 한 마디가 "이 파일은 모듈이고, import/export를 쓸 거예요"라고 브라우저에 알려줘요.
⚠️ 여기 아주 중요한 함정이 있어요.
type="module"은 파일을 그냥 더블클릭해서 여는 방식(file://)에선 안 돌아가요. 보안 규칙 때문이에요. 반드시 Live Server로 열어야 모듈이 작동해요. VS Code에서feed.html우클릭 → "Open with Live Server"로 여세요. 안 그러면 콘솔에 CORS 어쩌고 하는 빨간 에러가 떠요.
모듈에는 좋은 점이 하나 더 있어요. C-1에서 두 파일에 똑같이 let likeCount를 선언했다가 충돌 났던 사고, 기억하세요? 모듈은 파일마다 스코프가 따로라서, 여러 파일에 같은 이름 변수를 둬도 서로 안 부딪혀요. 그때 우리를 괴롭히던 전역 충돌이 모듈에선 자연스럽게 사라지는 거예요.
Step 6: 모듈 ② — import로 불러오기
데이터를 data.js로 내보냈으니, 이제 main.js에서 가져와 써야겠죠. 가져올 땐 import(불러오기)를 써요. 그리고 import는 파일 맨 위에 적는 게 규칙이에요.
// instagram-clone-frontend/js/main.js (맨 위)
import { feedPosts } from "./data.js";
import { formatCard } from "./format.js";
읽는 법은 이래요. "./data.js에서 feedPosts를 가져와." 중괄호 { } 안에 가져올 이름을 적고, from 뒤에 어느 파일인지 경로를 써요. 내보낼 때 feedPosts라는 이름으로 내보냈으니, 가져올 때도 똑같은 이름으로 받아요. 이런 방식을 named export/import(이름으로 주고받기)라고 불러요.
data.js main.js
┌──────────────────────┐ ┌──────────────────────────┐
│ export const │ │ import { feedPosts } │
│ feedPosts = [...] │ ─────▶ │ from "./data.js"; │
└──────────────────────┘ 같은 └──────────────────────────┘
이름으로
경로에서 두 가지를 꼭 지켜요. 같은 폴더(js/) 안의 파일이면 **./**로 시작하고(같은 자리라는 뜻), 끝에 .js 확장자까지 적어요. 브라우저 모듈은 .js를 생략하면 못 찾아요. "./data.js"처럼 정확히요.
🙋 한 파일이 값을 딱 하나만 내보낼 땐
export default라는 방식도 있어요. 그땐 가져올 때 중괄호 없이import 이름 from "..."으로 받죠. 우리는 한 파일에서 여러 개를 내보낼 거라 중괄호를 쓰는 named 방식을 기본으로 써요. default는 "이런 것도 있구나" 정도만 알아두세요.
Step 7: 실전 — format.js로 표현 로직 분리
데이터는 data.js로 나눴어요. 이번엔 게시물을 화면용 문자열로 바꾸는 표현 로직을 format.js라는 또 다른 파일로 떼어낼게요. 데이터(무엇을)와 표현(어떻게 보여줄지)을 나눠 두면, 나중에 카드 모양만 바꾸고 싶을 때 format.js만 열면 되거든요.
여기서 오늘 배운 게 전부 모여요. 템플릿 리터럴로 문자열을 짜고, ?.로 없을지 모르는 작성자를 안전하게 읽고, ??로 빈자리를 채워요.
// instagram-clone-frontend/js/format.js
// 게시물을 화면에 보여줄 "문자열"로 바꾸는 함수만 모아둔 파일.
export function formatCard(post) {
const name = post.author?.name ?? "익명"; // author 없으면 "익명"
const place = post.location ?? "어딘가"; // location 없으면 "어딘가"
const badge = post.author?.verified ? " ✔" : ""; // 인증 계정만 ✔
return `${name}${badge} — ${post.caption} (좋아요 ${post.likeCount}개) @${place}`;
}
formatCard는 게시물 객체 하나를 받아 카드 한 줄로 바꿔요. post.author?.name ?? "익명" — author가 있으면 이름을, 없으면 "익명"을. post.location ?? "어딘가"도 같은 짝꿍이죠. post.author?.verified가 참일 때만 ✔ 뱃지를 붙이고, 마지막에 템플릿 리터럴로 전부 한 줄에 엮어요. 오늘 배운 세 문법이 이 한 함수에 다 들어 있어요.
이제 main.js에서 두 파일을 불러와 조립해요.
// instagram-clone-frontend/js/main.js
// feedPosts 는 data.js 에서, formatCard 는 format.js 에서 import 했어요 (맨 위 참고).
const feedCards = feedPosts.map(formatCard);
feedCards.forEach((line) => console.log(line));
feedPosts.map(formatCard)는 지난 시간 배운 map이에요. 게시물 배열의 각 요소를 formatCard에 넣어, 카드 문자열 배열로 변환하죠. 그걸 forEach로 한 줄씩 콘솔에 찍어요. Live Server로 feed.html을 열고 콘솔을 보면 이렇게 나와요.
minji ✔ — 제주 여행 다녀왔어요 (좋아요 120개) @제주도
jaehoon — 오늘의 맛집 발견 (좋아요 0개) @어딘가
익명 — 평범한 일상 (좋아요 56개) @어딘가
yuna — 발리 서핑 도전 (좋아요 340개) @발리
하나하나 뜯어볼까요. 1번은 minji에 인증 뱃지 ✔, 위치 @제주도. 2번은 location이 없어 @어딘가로, 좋아요가 0인데도 ?? 덕분에 0개로 멀쩡히 나왔죠(||였다면 여기서 틀렸을 거예요). 3번은 author가 통째로 없어 익명. 데이터가 제각각 비어 있어도 ?.와 ??가 다 받아내서, 한 줄도 안 터지고 카드가 완성됐어요.
data.js format.js main.js
feedPosts ──▶ formatCard() ──▶ .map() · 콘솔 출력
(데이터) (표현) (조립·실행)
└──────────── 세 파일이 협력 ────────────┘
한 파일에 다 있던 코드가 셋으로 나뉘었는데도, 아니 나뉘었기 때문에 오히려 각 파일이 무슨 일을 하는지 한눈에 들어와요. 데이터를 고치려면 data.js, 카드 모양을 바꾸려면 format.js, 흐름을 보려면 main.js. 이게 진짜 프로젝트가 코드를 정리하는 방식이에요.
마무리
오늘 우리는 모던 JavaScript의 편리한 문법들과, 코드를 여러 파일로 나누는 모듈을 배웠어요. 한 파일에 쌓여 있던 코드가 역할별로 정리되면서, 처음으로 진짜 프로젝트의 골격을 갖췄죠. 되짚어볼게요.
- 템플릿 리터럴 — 백틱(
`)과${}로 글자 안에 변수와 식을 바로 끼워요.+연결과 작별이에요. 줄바꿈도 그대로 담기죠. - 옵셔널 체이닝(
?.) — 없을지 모르는 중첩 값을, 에러로 터뜨리는 대신undefined로 안전하게 받아요. - Nullish 병합(
??) —null/undefined일 때만 기본값을 채워요.0이나""를 살려야 할 때||보다 안전하죠. - 모듈(export/import) —
export로 내보내고import로 가져와요.<script type="module">이 필요하고, Live Server로 열어야 작동해요. - 파일 분리 — 데이터는
data.js, 표현은format.js, 조립은main.js. 역할별로 나누면 찾기도 고치기도 쉬워요.
?.와 ??가 처음엔 기호처럼 보여 낯설 수 있어요. 괜찮아요. "있을지 없을지 모르면 ?., 비었으면 채우는 ??" 이 한 쌍만 기억하면, 코드를 읽다가 자연스럽게 손에 익어요. 오늘 데이터가 제각각 비어 있어도 카드가 안 터지고 완성되는 걸 직접 봤으니, 이 문법들이 왜 필요한지 느꼈을 거예요.
다음 시간 예고
오늘 우리 데이터는 data.js에 코드로 박혀 있었어요. feedPosts라는 배열을 우리가 직접 손으로 적었죠. 그런데 진짜 인스타그램의 게시물은 우리 코드 안에 있지 않아요. 멀리 떨어진 서버에 있고, 화면을 열 때마다 그걸 받아와야 하죠.
문제는, 서버에서 데이터를 받아오는 데는 시간이 걸린다는 거예요. 코드가 "데이터 줘"라고 요청하면, 답이 올 때까지 기다려야 해요. 그동안 화면이 멈춰 있으면 안 되겠죠? 다음 시간엔 이렇게 시간이 걸리는 일을 다루는 비동기(asynchronous) JavaScript를 배워요. 동기와 비동기가 뭐가 다른지, Promise와 async/await로 "기다림"을 어떻게 우아하게 다루는지요. 데이터를 진짜 서버에서 받아오는 그날을 향한 중요한 한 걸음이에요. 기대하세요!
과제
오늘 배운 템플릿 리터럴, ?., ??, 모듈을 직접 손에 익혀볼 차례예요. 기초 → 응용 → 탐구 순서로 풀어보세요. 모든 과제는 콘솔(console.log)에서 확인하고, 오늘 배운 내용만으로 충분히 풀 수 있어요.
[구현] 템플릿 리터럴로 프로필 한 줄 소개 만들기 (기초)
지난 시간 만든 account 같은 객체를 하나 두고, 백틱으로 프로필 소개 문자열을 만들어보세요.
const account = { handle: "hong_tutor", fans: 1240, posts: 150 };처럼 객체를 만드세요.- 백틱과
${}로@hong_tutor 님 — 팔로워 1240명, 게시물 150개형태의 문자열을 만들어console.log로 찍으세요. +연결은 쓰지 말고, 전부${}안에 넣어보세요.+없이 얼마나 깔끔해지는지 느껴보세요.
[구현] format.js에 함수 추가하고 import해서 쓰기 (응용)
format.js에 게시물의 짧은 요약을 만드는 함수를 새로 추가하고, main.js에서 불러와 써보세요.
format.js에export function formatSummary(post) { ... }를 추가하세요. 안에서post.author?.name ?? "익명"으로 작성자를,post.location ?? "위치 비공개"로 위치를 꺼내,`${name} 님이 ${place}에서`같은 한 줄을 돌려주게 만드세요.main.js맨 위import { formatCard } from "./format.js";를import { formatCard, formatSummary } from "./format.js";로 고쳐 두 함수를 함께 가져오세요.feedPosts[2](작성자 없는 게시물)를formatSummary에 넣어 찍어보세요.익명 님이 위치 비공개에서가 나오면?.와??가 제대로 받아낸 거예요.
[탐구] ?? 와 || 의 차이를 직접 실험하기 (탐구)
??와 ||가 언제 갈라지는지 여러 값으로 직접 확인해보세요.
0,""(빈 문자열),false,null,undefined다섯 값을 각각 변수에 담으세요.- 각 값에 대해
값 || "기본값"과값 ?? "기본값"을 둘 다console.log로 찍어 결과를 비교하세요. - 어떤 값에서
||와??의 결과가 갈라지나요? 갈라지는 값들의 공통점이 무엇인지("이 값들은 falsy하지만 비어 있진 않다") 한 문장으로 정리해보세요. - 좋아요 수처럼
0이 의미 있는 데이터라면 둘 중 무엇을 써야 하는지도 적어보세요.
생각해볼 주제
정답을 적는 문제가 아니에요. 오늘 배운 것의 "왜"를 곱씹어보는 질문들이에요. 스스로 답을 만들어본 뒤, 예시답안과 비교해보세요.
1. 코드를 한 파일에 다 두면 안 될까, 왜 굳이 나눌까?
오늘 우리는 잘 돌아가던 main.js를 굳이 data.js·format.js로 쪼갰어요. 파일이 늘면 관리할 것도 늘어나는데 말이죠. 그런데 493줄짜리 한 파일에서 "좋아요 카드 모양 바꾸는 코드"를 찾는다고 상상해보세요. 위아래로 한참 스크롤해야겠죠. 반대로 format.js 한 파일만 열면 표현 코드만 모여 있어요. "한 파일이 한 가지 일만 한다"는 게 왜 찾기 쉽고 고치기 안전한지, 여러 사람이 같은 프로젝트를 나눠 맡는 상황까지 상상하며 생각해보세요.
2. ?. 는 편한데, 아무 데나 붙이면 좋을까?
?.는 코드가 터지는 걸 막아줘요. 그래서 "그럼 모든 점에 ?.를 붙이면 절대 안 터지겠네?"라고 생각할 수 있어요. 그런데 잘 생각해보세요. 만약 어떤 값이 반드시 있어야 정상인데 없다면, 그건 진짜 문제 상황이에요. 거기에 ?.를 붙이면 에러가 안 나는 대신 조용히 undefined로 넘어가서, 진짜 버그가 어디서 시작됐는지 찾기 어려워지죠. "있을 수도 없을 수도 있는 값"과 "반드시 있어야 하는 값"을 가르는 게 왜 중요한지 곱씹어보세요.
3. 좋아요 0개 게시물, ?? 가 없었다면 어떤 버그가 생겼을까?
Step 4에서 likeCount가 0일 때 || "없음"은 "없음"으로 잘못 바꿨고, ?? "없음"은 0을 살렸어요. 둘 다 그럴듯해 보이는데 결과가 달랐죠. 만약 어떤 개발자가 별생각 없이 ||로 좋아요 기본값을 처리했다면, 좋아요가 정확히 0개인 게시물에서 무슨 일이 벌어질까요? 화면엔 뭐라고 뜨고, 사용자는 그걸 보고 어떻게 오해할까요? 0·""·false처럼 "falsy하지만 의미 있는 값"이 실제 서비스에서 얼마나 흔한지 떠올리며, 왜 이 작은 차이가 실무에서 중요한지 생각해보세요.
✅ 예시 답안정답 보기
🎯 [과제 1 예시답안] 템플릿 리터럴로 프로필 한 줄 소개 만들기
수업에서 카드 문자열을 +로 이어 붙이다가, 백틱과 ${}로 훨씬 깔끔하게 바꿨죠. 이 과제는 그 손맛을 프로필 소개에 직접 적용해보는 거예요.
핵심 접근
객체에서 값을 꺼내(account.handle) 백틱 문자열 안 ${}에 끼워 넣으면 돼요. +를 한 번도 쓰지 않는 게 이 과제의 목표예요.
예시 구현
const account = { handle: "hong_tutor", fans: 1240, posts: 150 };
const intro = `@${account.handle} 님 — 팔로워 ${account.fans}명, 게시물 ${account.posts}개`;
console.log(intro);
// @hong_tutor 님 — 팔로워 1240명, 게시물 150개
@는 그냥 글자라 백틱 안에 그대로 적고, 변수가 들어갈 자리에만 ${}로 구멍을 뚫었어요. ${account.handle}처럼 ${} 안에서 점 표기법으로 객체 값을 바로 꺼낼 수도 있어요. 따로 변수에 빼지 않아도 되죠.
채점 포인트
- 백틱(
`)으로 문자열을 감쌌는가 (작은/큰따옴표가 아니라) ${}안에 변수 또는account.handle같은 식을 넣었는가+연결을 한 번도 쓰지 않았는가- 결과 문자열이 의도한 형태로 콘솔에 찍히는가
흔한 실수
- 백틱 대신 따옴표를 쓰고
${}를 넣음 →${}가 그냥 글자로 찍혀요(${account.handle}그대로 출력). 반드시 백틱이어야${}가 작동해요. ${ account.handle }처럼 공백은 괜찮지만,$ {account.handle}처럼$와{사이를 띄우면 작동 안 해요.
🎯 [과제 2 예시답안] format.js에 함수 추가하고 import해서 쓰기
데이터가 비어 있어도 안 터지게 ?.와 ??로 받아내는 함수를, 직접 format.js에 추가하고 main.js에서 불러와 쓰는 과제예요. 모듈을 손수 늘려보는 첫 경험이에요.
핵심 접근
format.js에 export function을 하나 더 추가하고, main.js의 import { } 중괄호 안에 함수 이름을 콤마로 덧붙이면 돼요. 함수 본문은 formatCard와 같은 결로 ?.와 ??를 쓰면 되죠.
예시 구현
// instagram-clone-frontend/js/format.js
export function formatSummary(post) {
const name = post.author?.name ?? "익명";
const place = post.location ?? "위치 비공개";
return `${name} 님이 ${place}에서`;
}
// instagram-clone-frontend/js/main.js (맨 위)
import { feedPosts } from "./data.js";
import { formatCard, formatSummary } from "./format.js";
// 작성자도 위치도 없는 3번 게시물로 확인
console.log(formatSummary(feedPosts[2]));
// 익명 님이 위치 비공개에서
feedPosts[2]는 author도 location도 없는 게시물이에요. post.author?.name이 undefined를 주고, ?? "익명"이 그걸 받아 "익명"으로 바꿔요. location도 마찬가지로 "위치 비공개"가 되죠. 데이터가 비어 있어도 함수가 안 터지고 멀쩡한 문장을 돌려줘요.
채점 포인트
format.js에export를 붙여 함수를 내보냈는가main.js의import { }안에 콤마로 두 함수를 함께 가져왔는가- 함수 안에서
?.와??를 짝지어 썼는가 - 작성자 없는 게시물에서
익명이, 위치 없는 게시물에서 기본값이 나오는가
흔한 실수
format.js에서export를 빼먹음 →main.js에서formatSummary is not defined또는 import 에러가 나요. 내보내야 가져올 수 있어요.import { formatCard } { formatSummary }처럼 중괄호를 따로 씀 → 한 파일에서 가져오는 건 중괄호 하나에 콤마로 묶어요:import { formatCard, formatSummary }.- Live Server가 아니라 파일을 더블클릭으로 열어 모듈이 아예 안 돌아감 → 콘솔 빨간 에러. 반드시 Live Server로.
🎯 [과제 3 예시답안] ?? 와 || 의 차이를 직접 실험하기
??와 ||가 언제 같고 언제 갈라지는지, 다섯 가지 값으로 직접 찍어 눈으로 확인하는 탐구예요. 표 한 장이 머릿속에 남으면 성공이에요.
핵심 접근
falsy한 값 다섯 개를 각각 || "기본값"과 ?? "기본값"에 넣어 결과를 나란히 비교해요. 결과가 갈라지는 값들의 공통점을 찾는 게 핵심이에요.
예시 구현
const values = [0, "", false, null, undefined];
values.forEach((v) => {
console.log(`${JSON.stringify(v)} || "기본값" →`, v || "기본값");
console.log(`${JSON.stringify(v)} ?? "기본값" →`, v ?? "기본값");
});
결과를 정리하면 이래요.
값 || "기본값" ?? "기본값" 갈라지나?
─────────────────────────────────────────────────
0 "기본값" 0 ✅ 갈라짐
"" "기본값" "" ✅ 갈라짐
false "기본값" false ✅ 갈라짐
null "기본값" "기본값" 같음
undefined "기본값" "기본값" 같음
채점 포인트
- 다섯 값 각각에
||와??를 모두 적용해 비교했는가 0·""·false에서 둘이 갈라지는 걸 확인했는가- 갈라지는 값들의 공통점을 정리했는가 — "falsy하지만
null/undefined는 아니다" - 좋아요 수처럼
0이 의미 있는 데이터엔??를 써야 한다고 결론지었는가
흔한 실수
||와??가 항상 같다고 생각함 →null/undefined에서만 같아요.0·""·false에서 갈라지는 게 핵심이에요.- "falsy = 비어 있음"이라고 외워버림 →
0과""는 falsy지만 멀쩡한 값이에요.??는 이 둘을 살려요.
💭 [생각해볼 주제 예시답안]
1. 코드를 한 파일에 다 두면 안 될까, 왜 굳이 나눌까?
문제 상황 요약
잘 돌아가던 main.js(493줄)를 data.js·format.js로 굳이 쪼갰어요. 파일이 늘면 관리할 것도 느는데, 왜 나누는 게 더 나을까요?
튜터의 가이드 및 해설
핵심은 "찾기"와 "고치기" 예요. 493줄 한 파일에서 "카드 모양 바꾸는 코드"를 찾으려면 위아래로 한참 스크롤하고, 잘못 건드리면 엉뚱한 기능이 깨져요. 반면 표현 코드가 format.js에 모여 있으면, 그 파일만 열면 되고 다른 파일은 안 건드려요.
"한 파일은 한 가지 일만 한다"를 관심사의 분리(separation of concerns)라고 불러요. 데이터(data.js)는 "무엇을", 표현(format.js)은 "어떻게 보여줄지", 조립(main.js)은 "흐름"을 맡죠. 각자 책임이 분명하면, 데이터가 바뀌어도 표현 코드는 그대로고, 카드 모양이 바뀌어도 데이터는 안전해요.
여러 사람이 한 프로젝트를 나눠 맡을 때 더 빛나요. 한 명은 data.js, 다른 한 명은 format.js를 맡으면 같은 파일을 동시에 고치다 충돌할 일이 줄어요. 파일이 느는 건 비용이지만, 그보다 "어디를 고쳐야 할지 바로 안다"는 이득이 훨씬 커요.
🎯 면접관을 홀리는 핵심 멘트
"파일을 나누는 건 코드를 늘리려는 게 아니라, 고칠 곳을 좁히려는 거예요. 한 파일이 한 책임만 지면, 변경이 그 파일 안에 갇혀서 다른 기능을 안 건드리거든요. 관심사의 분리가 결국 '안전하게 고칠 수 있는 코드'를 만들어요."
2. ?. 는 편한데, 아무 데나 붙이면 좋을까?
문제 상황 요약
?.는 코드가 터지는 걸 막아줘요. 그럼 모든 점에 ?.를 붙이면 절대 안 터지니 더 좋은 거 아닐까요?
튜터의 가이드 및 해설
여기엔 함정이 있어요. ?.는 에러를 막는 게 아니라 조용히 넘어가게 만들어요. 만약 어떤 값이 반드시 있어야 정상인데 없다면, 그건 진짜 문제예요. 데이터를 잘못 만들었거나, 앞 단계에서 뭔가 빠뜨린 거죠.
그 자리에 ?.를 붙이면 에러가 안 나는 대신 undefined로 슬그머니 넘어가요. 그럼 화면 어딘가에 undefined가 뜨거나, 한참 뒤 엉뚱한 곳에서 버그가 터져요. 정작 진짜 원인(값이 없었던 그 지점)은 조용히 지나갔으니, 디버깅이 훨씬 어려워지죠. 에러는 사실 "여기서 뭔가 잘못됐어요"라고 알려주는 신호인데, ?.를 남발하면 그 신호를 꺼버리는 셈이에요.
그래서 기준은 이래요. "있을 수도, 없을 수도 있는 값" 에만 ?.를 붙여요. 게시물의 작성자 정보처럼 진짜로 없을 수 있는 자리요. 반대로 "반드시 있어야 하는 값" 엔 ?.를 안 붙이고, 없으면 차라리 터지게 둬요. 그래야 문제를 빨리 발견하죠.
🎯 면접관을 홀리는 핵심 멘트
"
?.는 에러를 없애는 게 아니라 미루는 거예요. 반드시 있어야 할 값에?.를 붙이면, 진짜 버그가undefined로 위장해서 한참 뒤에 터져요. 그래서 저는 '없을 수 있는 값'에만?.를 쓰고, '있어야 하는 값'은 일부러 터지게 둬서 문제를 빨리 잡아요."
3. 좋아요 0개 게시물, ?? 가 없었다면 어떤 버그가 생겼을까?
문제 상황 요약
likeCount가 0일 때 || "없음"은 "없음"으로 잘못 바꿨고, ?? "없음"은 0을 살렸어요. 만약 개발자가 별생각 없이 ||를 썼다면, 좋아요 0개 게시물에서 무슨 일이 벌어질까요?
튜터의 가이드 및 해설
좋아요가 정확히 0개인 새 게시물을 떠올려보세요. likeCount || "없음"이라고 짰다면, 0은 falsy라 ||가 "없음"으로 바꿔버려요. 화면엔 "좋아요 없음"이라고 떠요. 그런데 이건 틀린 표현이에요. 좋아요가 0개인 것과 데이터가 없는 것은 전혀 다른 상황이거든요. 사용자는 "어, 좋아요 기능이 고장 났나?"라고 오해할 수 있죠.
0만 문제가 아니에요. 빈 문자열 ""(소개 글을 안 적은 사용자), false(꺼둔 알림 설정)도 전부 falsy예요. ||로 기본값을 처리하면, 사용자가 일부러 비워둔 값이나 0이라는 의미 있는 값이 죄다 기본값으로 덮여요. 실무 데이터엔 이런 "falsy하지만 의미 있는 값"이 정말 흔해요. 좋아요 0, 가격 0(무료), 빈 자기소개, 0번째 페이지...
그래서 기본값을 채울 땐 "진짜로 비어 있을 때(null/undefined)만" 채우는 ??가 안전해요. ||는 "이 값이 falsy면 무조건"이라, 0이나 ""를 살려야 하는 자리에선 조용히 버그를 만들어요. 작은 기호 하나 차이지만, 사용자가 보는 화면을 바꾸는 결정적 차이예요.
🎯 면접관을 홀리는 핵심 멘트
"
||와??의 차이는 'falsy냐'와 '진짜 비었냐' 의 차이예요. 좋아요 0개, 무료(가격 0), 빈 자기소개처럼 0과 빈 문자열이 의미 있는 값일 때||로 기본값을 처리하면 멀쩡한 데이터가 덮여요. 그래서 기본값엔null/undefined만 거르는??를 기본으로 씁니다."