Frontend 20
JSX 마스터하기: HTML과 JavaScript의 조화
HTML처럼 보이는데 className, 중괄호, map, onClick 같은 JavaScript 규칙이 섞여서 무엇이 HTML이고 무엇이 JS인지 헷갈린다.
근거 · 교안 p109-p119
1장: JSX의 정체성: HTML인가, JavaScript인가?
솔라의 시선이 모니터 화면의 코드 한 조각에 머물러 있었다. 며칠 전, 짧은 강의 영상에서 들었던 한 문장이 귓가에 맴돌았다. “JSX는 React에서 UI를 작성하는 문법입니다.” 간결하고 명확한 정의라고 생각했다. 하지만 지금 눈앞의 코드는 그 정의를 비웃는 듯 복잡한 얼굴을 하고 있었다.
분명 <div나 <h1>처럼 익숙한 HTML 태그로 시작하는데, 곳곳에 박힌 이질적인 요소들이 자꾸만 눈에 걸렸다. 이건 HTML인가? 아니면 JavaScript인가? 둘 다 아니라면, 대체 어떤 규칙을 따라야 하는 걸까. 솔라는 마우스 커서를 className="profile-card"라는 부분 위로 가져갔다. ‘HTML에서는 그냥 class인데, 왜 여기서는 className이지?’
생각은 꼬리에 꼬리를 물었다. 이름만 다른 건 애교였다. 그 아래에는 {user.name}처럼 뜬금없이 중괄호가 나타나 변수를 보여주고, 심지어 profileItems.map(...) 같은 JavaScript 함수가 HTML 태그를 찍어내고 있었다. 이건 HTML에 JavaScript를 몇 스푼 섞은 게 아니었다. 아예 다른 두 언어를 거칠게 비벼 놓은 잡탕밥처럼 보였다. 머릿속에서 두 개의 목소리가 싸우는 기분이었다. ‘이건 태그니까 HTML 규칙을 따라야 해!’ ‘아니야, 저건 함수잖아. JavaScript 규칙이야!’
“그 코드, 아까부터 계속 보고 있네.”
언니 루나가 조용히 옆에 서서 화면을 들여다보며 말했다.
“언니, 이거 봐. JSX라는 건데, 도대체 정체를 모르겠어. 분명 UI를 만드는 거라고 했는데, 생긴 건 HTML인데 작동은 JavaScript 같아. className은 뭐고, 이 중괄호는 뭐며, map 함수는 또 뭐야? 어느 장단에 맞춰야 할지 모르겠어.”
솔라는 답답한 마음을 쏟아냈다. 이건 마치 한국어 문장 중간중간에 영어 단어뿐만 아니라 영문법까지 뒤섞여 있는 글을 읽는 기분이었다.
루나는 잠시 코드를 살펴보다가, 솔라의 의자를 자기 쪽으로 살짝 돌렸다. “어떤 규칙을 따라야 할지 혼란스러운 거구나. 그럼 규칙을 찾기 전에, 우리가 눈으로 본 것부터 나눠볼까?”
“나눈다고?”
“응. 저 코드에서 ‘이건 완전 HTML인데?’ 싶은 부분이랑, ‘이건 아무리 봐도 JavaScript인데?’ 싶은 부분을 각자 구분해보는 거야. 정답을 찾으려고 하지 말고, 그냥 솔라 네가 느끼는 대로.”
흥미로운 제안이었다. 솔라는 다시 모니터 앞으로 몸을 돌렸다. 복잡하게 얽혀 있던 코드를 해체하는 작업이라고 생각하니, 막막함이 조금은 가셨다.
솔라는 먼저 HTML처럼 보이는 것들을 짚어 나갔다.
“음… <div>, <h1>, <ul>, <li> 같은 태그들. 그리고 이렇게 태그 안에 다른 태그를 넣어서 구조를 짜는 방식. 이건 영락없는 HTML이지.”
“좋아. 그럼 JavaScript처럼 보이는 건?”
“일단 className. HTML class랑 다르니까. 그리고 변수 값을 보여주는 {user.name}. 이건 확실히 JavaScript 영역이야. 아, 그리고 결정적으로 이거. profileItems.map(...). 배열을 순회하면서 <li> 태그를 만들어내는 부분이잖아. 이건 순수한 JavaScript 로직이지. onClick={handleClick} 이것도. HTML에선 보통 문자열을 넣는데, 이건 함수 자체를 넘기는 거니까.”
하나씩 분리하고 나니 무언가 보이기 시작했다. 솔라는 자기가 나열한 두 목록을 머릿속으로 비교했다. HTML처럼 보이는 것들은 대부분 뼈대, 즉 ‘구조’에 관한 것들이었다. 반면 JavaScript처럼 보이는 것들은 데이터를 화면에 ‘표시’하고, 데이터를 기반으로 여러 요소를 ‘생성’하고, 사용자의 행동에 ‘반응’하는, 살아있는 기능들이었다.
그 순간, 머릿속을 짓누르던 안개가 걷히는 느낌이 들었다.
“아…!”
솔라는 나지막이 탄성을 내뱉었다. “이거, HTML과 JavaScript를 반반 섞은 게 아니었어. 그냥 JavaScript였네.”
루나가 조용히 고개를 끄덕였다.
솔라는 흥분하며 말을 이었다. “그러니까… 뼈대만 HTML 모양을 빌려온 JavaScript인 거야. UI가 어떻게 생겼는지 설명하기 편하니까 태그 모양을 쓰는 거고, 본질은 전부 JavaScript인 거지. 그래서 class가 아니라 className을 쓰는 거야. class는 JavaScript에서 이미 클래스를 만드는 키워드로 쓰이고 있으니까. 중괄호를 쓰면 JavaScript 세상으로 잠시 돌아가서 변수나 함수를 마음대로 쓸 수 있는 거고!”
스스로 내린 결론에 감탄하며 솔라는 의기양양하게 루나를 쳐다봤다. 이제야 “JSX는 React에서 UI를 작성하는 문법”이라는 첫 문장이 다르게 읽혔다. ‘JavaScript 안에서 UI 구조를 쉽게 쓰기 위한 특별한 약속’이라는 뜻이었구나. 복잡한 잡탕밥이 아니라, 잘 설계된 주방에서 익숙한 도구를 쓰는 것과 같았다.
“이제 좀 알 것 같아. 한번 간단하게 만들어 볼래.”
자신감이 붙은 솔라는 새 파일을 열고 코드를 작성하기 시작했다. 배운 대로라면, JavaScript 함수 안에서 HTML처럼 생긴 구조를 반환하면 될 터였다.
function Greeting() {
return (
<h1>안녕!</h1>
<p>만나서 반가워.</p>
);
}
완벽하다고 생각했다. 하지만 코드를 저장하자마자 편집기 화면에 빨간 밑줄이 그어졌다. 예상치 못한 에러 메시지가 솔라의 자신감에 찬물을 끼얹었다.
“어? 왜 안 되지? 그냥 태그 두 개를 나란히 쓴 것뿐인데. HTML에서는 이게 당연한 거잖아. 본질이 JavaScript라면서, 왜 갑자기 이런 HTML에도 없는 이상한 규칙을 강요하는 거야?”
2장: UI 구조의 기본 규칙: 단일 부모와 Self-Closing
솔라의 자신감 넘치던 손가락이 키보드 위에서 멈칫했다. 방금 전까지만 해도 완벽해 보였던 코드에 선명한 빨간 밑줄이 그어져 있었다. 편집기는 집요하게 에러 메시지를 띄웠다. ‘Adjacent JSX elements must be wrapped in an enclosing tag.’ (인접한 JSX 요소는 반드시 감싸는 태그 안에 있어야 합니다.)
“이게 무슨 소리야…”
솔라는 혼란스러운 목소리로 중얼거렸다. HTML에서는 너무나도 당연한 코드였다. 제목(<h1>)과 단락(<p>)을 차례로 나열하는 것. 하지만 JSX는 그것을 허용하지 않았다. ‘본질은 JavaScript’라며 스스로 내렸던 결론이 무색해지는 순간이었다. 정체를 파악했다고 생각했는데, 아직도 예측할 수 없는 규칙이 숨어있었다.
옆에서 지켜보던 루나가 솔라의 화면을 가리켰다. “아까 솔라 네가 그랬지. JSX는 HTML 모양을 빌려온 JavaScript라고.”
“응. 근데 아니었나 봐. JavaScript라면서 왜 HTML보다 규칙이 더 까다로운 거야? HTML에선 저게 아무 문제 없잖아.” 솔라의 목소리엔 실망과 짜증이 섞여 있었다.
루나는 솔라의 불평을 가만히 듣고 있다가, 마우스 커서를 return 키워드 위로 옮겼다. “HTML 모양이라는 점은 잠시 잊어보자. 이건 Greeting이라는 이름의 JavaScript 함수잖아. 그리고 함수는 값을 return, 즉 반환하지. JavaScript 함수가 한 번에 여러 개의 값을 반환할 수 있던가?”
“한 번에 여러 개를?” 솔라는 루나의 질문을 되뇌었다. JavaScript 함수는 언제나 단 하나의 값만 반환할 수 있었다. 숫자 하나, 문자열 하나, 혹은 여러 값을 담은 객체나 배열 하나. return 1, 2; 같은 코드는 문법적으로 불가능했다.
그 순간, 무언가 번뜩하고 머리를 스쳤다.
“아…! 설마.”
솔라는 자신의 코드를 새로운 눈으로 다시 쳐다봤다. <h1> 태그와 <p> 태그. HTML 관점에서는 그저 나란히 놓인 두 요소일 뿐이었다. 하지만 JavaScript 함수의 관점에서 보자, 이건 마치 두 개의 독립적인 값을 한 번에 반환하려는 시도처럼 보였다.
“함수는 값을 하나만 반환할 수 있으니까… 지금 이 코드는 h1이라는 값과 p라는 값을 동시에 반환하려고 해서 에러가 나는 거구나!”
“그럼 어떻게 해야 하나만 반환하는 것처럼 만들 수 있을까?” 루나가 조용히 물었다.
“하나로… 묶으면 돼!”
솔라는 망설임 없이 코드 수정을 시작했다. 두 요소를 하나의 꾸러미로 만들기 위해 가장 만만한 <div> 태그로 감쌌다.
function Greeting() {
return (
<div>
<h1>안녕!</h1>
<p>만나서 반가워.</p>
</div>
);
}
코드를 수정하자마자 마법처럼 빨간 밑줄이 사라졌다. 이제 Greeting 함수는 여러 개가 아닌, div라는 단 하나의 요소(Element)만을 반환하고 있었다. 그 안에 몇 개의 자식 요소가 있는지는 상관없었다. 중요한 것은 최상위 레벨에 단 하나의 ‘뿌리’만 존재한다는 사실이었다. HTML의 문법이 아니라 JavaScript의 반환 값 규칙이 우선이었던 것이다.
“됐어! 이제 이해했어. UI의 구조를 짜는 건 맞지만, 결국 함수가 반환하는 값은 하나여야 한다는 JavaScript 규칙을 따라야 하는 거였어.”
솔라는 만족스럽게 웃으며 한 줄을 더 추가했다. 환영의 의미로 이미지를 하나 넣고 싶었다.
function Greeting() {
return (
<div>
<h1>안녕!</h1>
<p>만나서 반가워.</p>
<img src="welcome.png">
</div>
);
}
그런데 이번에도 편집기가 희미하게 경고 표시를 띄웠다. 에러는 아니었지만, 무언가 잘못되었다는 신호였다. ‘JSX elements must be closed.’ (JSX 요소는 닫혀야 합니다.)
“이번엔 또 뭐야? img 태그는 원래 닫는 태그가 없잖아.” 솔라가 투덜거렸다.
루나가 이번에는 키보드를 넘겨받아 <img>를 <img></img>로 고쳐보았다. 경고가 사라졌다. 이어서 다시 <img />로 수정했다. 여전히 코드는 깨끗했다.
“JSX는 HTML보다 좀 더 엄격해. XML이라는 형식과 비슷한데, 모든 태그는 반드시 닫혀야 한다는 규칙이 있거든. div처럼 여는 태그와 닫는 태그가 쌍을 이루거나, img처럼 내용이 없는 태그는 스스로 닫아줘야 해.”
솔라는 <img /> 끝에 붙은 슬래시를 보았다. HTML의 유연함에 익숙해져 그냥 지나쳤던 부분이었다. JSX의 세계에서는 모든 문이 열렸으면 반드시 닫혀야 했다. 혼자 있는 문이라면 스스로 빗장을 걸어야 했다.
이제 솔라는 JSX의 두 가지 중요한 구조 규칙을 몸으로 깨달았다. 첫째, 모든 컴포넌트는 단 하나의 부모 요소만 반환해야 한다. 둘째, 모든 태그는 반드시 닫혀야 한다. 이 규칙들은 HTML이 아니라, JavaScript의 동작 원리와 JSX 자체의 엄격함에서 비롯된 것이었다.
“좋아, 이제 기본 구조는 알겠어. 그럼 여기에 진짜 내 이름을 넣으려면 어떻게 해야 하지?”
자신감이 회복된 솔라는 변수를 만들어 이름 부분에 넣어보려고 했다.
function Greeting() {
const name = "솔라";
return (
<div>
<h1>안녕! name</h1>
<p>만나서 반가워.</p>
<img src="welcome.png" />
</div>
);
}
하지만 화면에는 ‘안녕! 솔라’가 아닌, ‘안녕! name’이라는 글자가 그대로 나타났다. JSX는 name을 변수로 인식하지 못하고 평범한 텍스트로 취급해버렸다. 구조를 만드는 법은 알았지만, 그 안에 살아있는 데이터를 불어넣는 방법은 아직 미지의 영역이었다.
3장: JavaScript 삽입: 중괄호와 className의 역할
솔라의 화면에는 방금 전의 성공과 새로운 좌절이 나란히 떠 있었다. div로 감싸고 img 태그를 셀프 클로징으로 수정하자 모든 에러와 경고가 사라진 깨끗한 코드. 하지만 브라우저에 렌더링된 결과물은 솔라의 기대를 배신했다. 화면에는 ‘안녕! 솔라’가 아닌, ‘안녕! name’이라는 글자가 밋밋하게 박혀 있었다.
분명 코드 상단에 const name = "솔라"; 라고 버젓이 선언해 두었다. 하지만 JSX는 name이라는 단어를 변수로 인식하지 않고, 그저 ‘n’, ‘a’, ‘m’, ‘e’라는 네 개의 글자로 이루어진 평범한 텍스트로 취급하고 있었다. 구조를 만드는 규칙은 알아냈지만, 그 안에 살아있는 데이터를 불어넣는 방법을 찾지 못한 셈이었다. 마치 멋지게 지은 집에 문패를 달았는데, 문패에 이름이 새겨지지 않고 ‘이름’이라는 단어만 적혀있는 꼴이었다.
“이건 또 왜 이러는 거야. name이라는 변수가 있다고 알려줬는데 왜 글자로만 취급하는 거지? 이럴 거면 JavaScript라고 할 수도 없잖아.”
솔라의 불평에 루나가 키보드를 가리켰다. “지금 <h1> 태그 안에 있는 모든 건, JSX가 보기에 그냥 ‘문자열’일 뿐이야. ‘안녕! ’도 문자열, ‘name’도 문자열. ‘이건 문자열이 아니라, 저기 위에 있는 JavaScript 변수를 가져와서 그 값을 보여줘’라고 특별히 알려줘야 해.”
“특별히? 어떻게?”
루나는 말없이 손가락으로 중괄호 {} 모양을 만들어 보였다.
“중괄호?”
“응. JSX 세상 안에서 ‘잠깐! 여기는 JavaScript 코드를 실행할 시간이야!’라고 외치는 신호 같은 거지.”
신호. 그 말에 솔라는 h1 태그 안의 name이라는 단어를 지우고, 그 자리에 루나가 알려준 대로 {name}을 입력했다.
function Greeting() {
const name = "솔라";
return (
<div>
<h1>안녕! {name}</h1>
<p>만나서 반가워.</p>
<img src="welcome.png" />
</div>
);
}
결과는 즉각적이었다. 화면의 ‘안녕! name’이 마침내 ‘안녕! 솔라’로 바뀌었다. 중괄호가 마치 HTML처럼 보이는 JSX의 세계와 순수한 JavaScript의 세계를 연결하는 작은 통로처럼 느껴졌다.
“와, 됐어! 중괄호로 감싸주니까 변수로 알아듣네. 그럼 혹시… 계산 같은 것도 돼?”
솔라는 호기심이 발동해 코드를 살짝 바꿔봤다.
<h1>1 더하기 1은 {1 + 1}</h1>
화면에는 ‘1 더하기 1은 2’가 나타났다. 중괄호 안에서는 어떤 JavaScript 표현식이든 자유롭게 실행할 수 있었다. 변수를 부르거나, 간단한 계산을 하거나, 심지어 함수를 호출하는 것까지. {name.toUpperCase()}라고 입력하자 ‘솔라’는 ‘솔라’가 아닌 ‘SOLA’로 출력되었다. 이제 딱딱하게 고정된 구조가 아니라, 데이터에 따라 변하는 동적인 UI를 만들 수 있는 열쇠를 손에 쥔 기분이었다.
자신감이 붙은 솔라는 환영 인사에 스타일을 좀 더해주고 싶었다. 미리 작성해 둔 CSS 클래스 greeting-card를 적용하기로 마음먹었다. HTML에서 하던 방식 그대로, 최상위 div에 class 속성을 추가했다.
function Greeting() {
const name = "솔라";
return (
<div class="greeting-card">
<h1>안녕! {name}</h1>
<p>만나서 반가워.</p>
<img src="welcome.png" />
</div>
);
}
하지만 코드를 저장하자마자, 편집기는 또다시 경고 메시지를 띄웠다. ‘Invalid DOM property class. Did you mean className?’ (잘못된 DOM 프로퍼티 class입니다. className을 사용하시겠습니까?)
“아니, 이건 또 왜? class는 HTML의 기본 속성이잖아. 이건 변수도 아니고 그냥 문자열인데, 왜 이것까지 태클을 거는 거야?” 솔라는 어이가 없었다.
“첫날 우리가 나눴던 대화를 다시 떠올려봐.” 루나가 차분하게 말했다. “JSX의 본질이 뭐라고 했지?”
“HTML 모양을 한 JavaScript…” 솔라는 중얼거리다 무언가 깨달았다. “아! class!”
JavaScript에서 class라는 단어는 이미 중요한 용도로 예약되어 있었다. 객체 지향 프로그래밍에서 클래스를 선언할 때 쓰는 핵심 키워드였다. 만약 JSX가 class를 HTML 속성으로 그대로 허용한다면, JSX 코드를 JavaScript로 변환하는 과정에서 혼란이 생길 수밖에 없었다.
“맞아. JavaScript의 예약어와 충돌하니까. 그래서 React 개발자들은 약속을 정한 거야. HTML의 class 속성은 JSX에서 className이라고 쓰기로.”
솔라는 즉시 class를 className으로 수정했다. 그러자 모든 경고가 말끔히 사라졌다.
<div className="greeting-card">
이제야 모든 조각이 맞춰지는 느낌이었다. JSX는 JavaScript의 영토 안에서 HTML의 문법을 빌려 쓰는 것. 따라서 그 영토의 규칙을 존중해야 했다. 동적인 값을 넣고 싶을 땐 중괄호 {}를 써서 ‘여기는 JavaScript 영역’임을 선언해야 하고, 속성 이름이 JavaScript의 예약어와 겹칠 땐(class) 대체 이름(className)을 사용해야 했다.
“이제 알겠어. 중괄호는 JSX 세계의 ‘자바스크립트 호출 버튼’ 같은 거고, className은 ‘자리 겹치지 않게 이름표 바꿔 단 것’이구나.”
스스로 내린 결론에 만족한 솔라는 잠시 완성된 코드를 바라봤다. 이제 한 사람에게 멋지게 인사하는 컴포넌트를 만들 수 있게 되었다. 그러자 새로운 욕심이 생겼다.
“좋아, 한 명한테 인사하는 건 이제 완벽해. 그런데 만약 내가 환영하고 싶은 친구들 목록이 있다면 어떡하지? 여러 명한테 한꺼번에 인사하려면, 이 div 덩어리를 친구 수만큼 복사해서 붙여넣어야 하나?”
4장: 목록 렌더링: map을 활용한 JSX 요소 변환
솔라의 손가락이 경쾌하게 키보드를 두드렸다. 한 사람에게 멋지게 인사하는 Greeting 컴포넌트는 이제 과거의 유산이었다. 솔라의 머릿속에는 이미 다음 목표가 선명했다. 환영할 친구가 한 명이 아니라 여러 명이라면?
솔라는 friends라는 이름의 배열을 만들고, 그 안에 초대하고 싶은 친구들의 이름을 넣었다.
const friends = ["루나", "코코", "파이"];
가장 직관적인 방법이 머리를 스쳤다. 이전에 만들었던 인사 카드 div 덩어리를 친구 수만큼 복사해서 붙여넣는 것이다. 솔라는 일단 코드를 작성해 보았다.
function FriendList() {
return (
<div>
<div className="greeting-card">
<h1>안녕! 루나</h1>
</div>
<div className="greeting-card">
<h1>안녕! 코코</h1>
</div>
<div className="greeting-card">
<h1>안녕! 파이</h1>
</div>
</div>
);
}
화면에는 세 개의 인사 카드가 나란히 나타났다. 작동은 했지만, 솔라는 곧바로 미간을 찌푸렸다. 코드가 끔찍하게 반복적이었다. 친구가 백 명이라면 이 짓을 백 번 해야 한다는 말인가? 데이터가 바뀌면 코드 전체를 뜯어고쳐야 했다. 이건 아니었다.
“이건 너무 무식한 방법이야. 프로그래밍의 기본은 반복 작업을 자동화하는 거잖아. 반복문… 그래, for 반복문을 쓰면 되겠네!”
솔라는 기존의 반복적인 코드를 모두 지우고, JavaScript의 가장 익숙한 무기인 for 반복문을 꺼내 들었다. JSX 안에서도 당연히 작동할 것이라고 믿었다. 중괄호 {}가 JavaScript 세상으로 통하는 문이라는 건 이미 배웠으니까.
function FriendList() {
const friends = ["루나", "코코", "파이"];
return (
<ul>
{
for (let i = 0; i < friends.length; i++) {
<li>{friends[i]}</li>
}
}
</ul>
);
}
그러나 코드를 저장하자마자, 편집기는 단호한 에러를 뿜어냈다. for 키워드 아래에 선명한 빨간 밑줄이 그어졌다. 기대와 달리, JSX의 중괄호는 for 반복문을 품어주지 않았다.
“아니, 왜! for 문은 가장 기본적인 JavaScript 문법인데 왜 안 되는 거야? 중괄호 안에서는 뭐든지 할 수 있는 거 아니었어?”
솔라의 외침에 옆에서 책을 읽던 루나가 화면을 흘끗 보았다. “중괄호 안에서 뭐든지 할 수 있다고? 정말 그랬나?”
루나는 솔라의 코드 옆에 작은 주석 창을 열어 두 줄의 코드를 적었다.
// A: 값으로 남는 것
const a = 1 + 1;
// B: 그냥 실행되는 것
for (let i = 0; i < 3; i++) {
console.log(i);
}
“A와 B 중에, 변수 x에 할당할 수 있는 건 어느 쪽일까? const x = ... 이렇게.”
솔라는 잠시 생각했다. “A는 당연히 되지. 1 + 1은 2라는 값으로 남으니까. 하지만 B는… const x = for(...) 같은 건 본 적이 없어. for문은 값을 남기는 게 아니라 그냥 시키는 일을 할 뿐이니까.”
“바로 그거야.” 루나가 말했다. “JSX의 중괄호 안에 들어갈 수 있는 건 A처럼 값으로 평가될 수 있는 ‘표현식(expression)’이야. 하지만 for 반복문은 B처럼 어떤 동작을 수행할 뿐, 그 자체가 값이 되지는 않는 ‘문(statement)’이지.”
솔라의 머릿속에서 조각들이 맞춰졌다. 중괄호는 마법의 문이 아니라, 결과적으로 하나의 값을 뱉어내는 JavaScript 표현식을 넣는 자리였다. for문은 값을 뱉어내지 않으니 그 자리에 들어갈 자격이 없었던 것이다.
“그럼… 반복은 어떻게 해? 배열의 각 항목을 하나씩 꺼내서 뭔가로 바꾼 다음, 그 결과물들을 모아서 하나의 값으로 돌려주는 그런 JavaScript 기능이 필요한데…”
솔라가 중얼거리는 순간, 예전에 어렴풋이 봤던 함수 하나가 떠올랐다. map.
“설마, map 함수?”
루나는 대답 대신 조용히 고개를 끄덕였다. 솔라는 곧장 코드를 수정했다. friends 배열에 .map()을 붙이고, 화살표 함수를 이용해 각 친구의 이름을 <li> 태그로 감싸주었다.
function FriendList() {
const friends = ["루나", "코코", "파이"];
return (
<ul>
{friends.map(friend => <li>{friend}</li>)}
</ul>
);
}
friends.map(...) 전체가 [<li>루나</li>, <li>코코</li>, <li>파이</li>] 라는 JSX 요소의 배열, 즉 하나의 ‘값’으로 평가되었다. 그리고 JSX는 이 배열을 알아서 풀어서 화면에 그려주었다. 에러는 사라졌고, 화면에는 깔끔한 친구 목록이 나타났다.
“됐어! for 문은 문이고 map은 값을 반환하는 표현식이라서 되는 거였구나!”
솔라는 완벽하게 작동하는 코드를 보며 기쁨을 만끽했다. 그런데 그때, 브라우저의 개발자 콘솔 창에 작은 경고 메시지가 떠 있는 것을 발견했다. ‘Warning: Each child in a list should have a unique “key” prop.’ (경고: 목록의 각 자식 요소는 고유한 “key” 속성을 가져야 합니다.)
“또야? 잘 나오는데 이건 또 무슨 경고람.”
“작동은 하지만, React가 조금 불편하다고 말하는 거야.” 루나가 말했다. “지금은 친구가 세 명뿐이지만, 만약 목록이 바뀌면 React는 어떤 친구가 추가되거나 사라졌는지 알아야 해. 이름표가 없으면 누가 누군지 구분하기 어렵지 않겠어? key는 React가 각 항목을 구별하기 위해 붙여주는 이름표 같은 거야.”
이해는 갔다. React가 효율적으로 변경 사항을 추적하려면 각 항목에 고유한 식별자가 필요하다는 뜻이었다. 솔라는 map 함수가 제공하는 두 번째 인자, index를 key로 사용하기로 했다.
<ul>
{friends.map((friend, index) => <li key={index}>{friend}</li>)}
</ul>
key를 추가하자 경고 메시지가 마법처럼 사라졌다. 이제야 코드는 완벽해졌다. 데이터를 기반으로 UI 목록을 동적으로, 그리고 효율적으로 생성하는 강력한 패턴을 손에 넣은 것이다.
솔라는 잠시 완성된 친구 목록을 바라보았다. 깔끔하고, 동적이며, 효율적이었다. 하지만 이내 새로운 궁금증이 고개를 들었다.
“좋아, 목록은 완벽해. 그런데 이 목록은 그냥 글자일 뿐이잖아. 만약 내가 ‘코코’라는 이름을 클릭했을 때, 코코의 프로필을 보여주는 것처럼 어떤 행동을 하게 만들려면 어떻게 해야 하지?”
5장: 이벤트 처리: camelCase 속성과 함수 참조
완벽하게 동적인 친구 목록이 화면에 그려졌지만, 솔라의 시선은 정적인 결과물 너머의 가능성을 쫓고 있었다. 목록은 그저 눈으로 보는 것을 넘어, 손으로 만지고 반응을 이끌어내야 진정한 UI라고 할 수 있었다. ‘코코’라는 이름을 클릭하면 ‘코코 안녕!’하고 알림 창이 뜨게 만들고 싶었다.
“좋아, 여기에 생명을 불어넣어 보자.”
솔라는 익숙한 HTML의 기억을 더듬었다. 클릭 이벤트를 처리하는 건 onclick 속성이었다. 자신 있게 map 함수 안의 <li> 태그에 코드를 추가했다. 먼저 클릭했을 때 실행될 간단한 함수를 컴포넌트 위에 정의했다.
function FriendList() {
const friends = ["루나", "코코", "파이"];
function handleFriendClick() {
alert("친구를 클릭했어요!");
}
return (
<ul>
{friends.map((friend, index) => (
<li key={index} onclick="handleFriendClick()">
{friend}
</li>
))}
</ul>
);
}
이보다 더 명확할 순 없다고 생각했다. 하지만 코드를 저장하자마자, 브라우저 콘솔에 경고 메시지가 나타났다. ‘Invalid DOM property onclick. Did you mean onClick?’ className 때와 똑같은 경고였다.
“아, 또 이거구나. JavaScript 예약어랑 겹치지 않게 하려는 규칙.”
솔라는 투덜거리면서도 onclick을 onClick으로 재빨리 수정했다. 이제 소문자 c가 대문자 C로 바뀌었다. 이런 ‘카멜 케이스(camelCase)’ 표기법이 JSX의 이벤트 처리 규칙 중 하나임을 직감했다. 하지만 수정한 뒤에도 친구 이름을 클릭했을 때 아무 일도 일어나지 않았다. 코드는 조용했고, 알림 창은 뜨지 않았다. 마치 버튼이 고장 난 리모컨 같았다.
“onClick으로 바꿨는데도 왜 안 되지? HTML에서는 이렇게 문자열로 함수 이름을 넣어주면 됐는데…”
솔라는 고개를 갸웃거리며 코드를 이리저리 바꿔보았다. 중괄호가 JavaScript 세상으로 가는 문이라는 걸 기억해내고는, 문자열을 중괄호로 감싸봤다. onClick={"handleFriendClick()"}. 여전히 결과는 같았다.
옆에서 지켜보던 루나가 조용히 입을 열었다. “솔라, 지금 네가 onClick에게 주고 있는 게 뭐야?”
“‘이 함수를 실행해’라는 명령이지.”
“정말? ‘명령’을 주고 있어, 아니면 ‘명령이 적힌 종이’를 주고 있어?”
루나의 비유에 솔라는 잠시 생각에 잠겼다. ‘명령이 적힌 종이’라. 루나는 솔라의 코드 아래에 두 가지 예시를 나란히 적었다.
// A: 함수를 실행한 결과(return 값)를 전달
onClick={handleFriendClick()}
// B: 함수 자체(정의)를 전달
onClick={handleFriendClick}
“A를 봐. handleFriendClick()처럼 괄호가 붙으면, JavaScript는 이걸 보는 즉시 함수를 실행시켜 버려. 그리고 그 자리에 함수의 결과물(return 값)을 남기지. handleFriendClick 함수는 아무것도 반환하지 않으니까 undefined가 남을 거야. 즉, ‘클릭하면 undefined를 실행해’라고 말하는 셈이야. 게다가 함수는 렌더링될 때 이미 실행돼 버렸고.”
그 말에 솔라는 머리를 한 대 맞은 듯한 기분이었다. map이 목록을 그리는 동안 handleFriendClick()이 친구 수만큼 실행되고, 정작 클릭할 때는 아무것도 연결되어 있지 않은 상태였던 것이다.
“아…! 괄호 때문에 그랬구나. 나는 ‘클릭하면 실행해’라고 예약하고 싶었는데, 괄호 때문에 ‘지금 당장 실행해’가 되어버린 거네.”
“맞아. 그럼 B는 어떨까?”
솔라는 handleFriendClick이라고만 쓰인 B 코드를 보았다. 괄호가 없었다. 이건 함수를 실행하라는 뜻이 아니었다. 변수에 값을 담듯, 함수라는 값 자체를 가리키고 있었다.
“B는… 함수를 실행하는 게 아니라, handleFriendClick이라는 함수 그 자체, 즉 ‘명령이 적힌 종이’를 onClick에게 건네주는 거구나! 그러면 React가 그 종이를 가지고 있다가, 사용자가 진짜로 클릭하는 순간에 그걸 보고 함수를 실행시켜 주는 거고.”
솔라는 즉시 자신의 코드를 수정했다. 따옴표도, 괄호도 없는 순수한 함수 이름만 중괄호 안에 남겼다.
<li key={index} onClick={handleFriendClick}>
{friend}
</li>
이제 브라우저에서 ‘코코’를 클릭하자, 마침내 ‘친구를 클릭했어요!’라는 알림 창이 환하게 나타났다. 드디어 정적인 목록에 상호작용이라는 숨결을 불어넣는 데 성공했다. JSX에서 이벤트를 다룰 때는 두 가지를 기억해야 했다. 속성 이름은 onClick처럼 카멜 케이스로, 값은 {handleClick}처럼 함수 자체를 참조로 넘겨야 한다는 것.
“이제 모든 규칙을 배운 것 같아.”
솔라는 자신감에 찬 목소리로 말했다. 처음 JSX를 봤을 때의 막막함은 사라지고, 이제는 어떤 코드든 해석하고 작성할 수 있을 것 같은 기분이 들었다.
루나가 미소 지으며 오래된 웹페이지 소스 코드 조각을 화면에 띄웠다. 순수한 HTML과 약간의 JavaScript가 섞인, 예전에 흔히 볼 수 있던 코드였다.
<div class="profile-card">
<img src="avatar.png">
<h2>솔라</h2>
<p>To see my favorite fruits, click the button.</p>
<ul id="fruit-list">
<!-- JavaScript로 채워질 예정 -->
</ul>
<button onclick="showFruits()">Show Fruits</button>
</div>
“그럼, 지금까지 배운 걸 전부 사용해서 이걸 완벽한 React 컴포넌트로 바꿔볼래?” 루나가 제안했다.
솔라는 망설임 없이 키보드에 손을 올렸다. 이것은 마지막 시험이자, 스스로의 성장을 확인하는 무대였다.
“좋아. 먼저 함수 컴포넌트로 전체를 감싸고… return 안에 넣어야지.”
솔라는 차근차근 변환을 시작했다.
“div의 class는 className으로… img는 내용이 없으니까 스스로 닫는 태그 <img />로… button의 onclick은 카멜 케이스 onClick으로 바꾸고, 문자열이 아니라 함수 참조 {showFruits}를 직접 넘겨줘야 해.”
하나씩 코드를 고쳐나갈 때마다, 머릿속에 흩어져 있던 규칙들이 제자리를 찾아 하나의 그림으로 완성되었다. map을 이용해 과일 목록을 동적으로 생성하는 부분까지 막힘없이 추가했다. 잠시 후, 솔라의 화면에는 모든 JSX 규칙이 조화롭게 적용된, 깔끔하고 현대적인 React 컴포넌트가 나타났다.
function ProfileCard() {
const fruits = ["딸기", "바나나", "사과"];
function showFruits() {
console.log(fruits);
}
return (
<div className="profile-card">
<img src="avatar.png" />
<h2>솔라</h2>
<p>To see my favorite fruits, click the button.</p>
<ul>
{fruits.map((fruit, index) => <li key={index}>{fruit}</li>)}
</ul>
<button onClick={showFruits}>Show Fruits</button>
</div>
);
}
솔라는 자신이 완성한 코드를 뿌듯하게 바라보았다. 더 이상 HTML과 JavaScript의 경계에서 혼란스러워하던 모습은 없었다. JSX는 더 이상 정체불명의 잡탕밥이 아니었다. UI 구조를 명료하게 표현하고, 데이터와 상호작용을 우아하게 결합하는, 잘 설계된 JavaScript의 확장 문법이라는 것을 온전히 이해하게 된 것이다.