#05. JSX 작성 규칙
목차10
TL;DR
- JSX = JavaScript + Markup
{ }로 감싼 부분엔 “JS 표현식(expression)“이 들어간다
- 이벤트 핸들러엔 함수를 넘긴다
onClick={fn}(O) /onClick={fn()}(X, 즉시 호출됨)
- HTML의
class/for는 JS 예약어라, JSX에선className/htmlFor사용 return은 하나의 루트로 묶음- 형제가 여럿이면
<>...</>(Fragment) 사용
- 형제가 여럿이면
{ }는 표현식만 받음- 조건부는
&&·삼항으로, 리스트나 반복은.map()+key로 대체
- 조건부는
개요
JSX는 JavaScript XML의 약자로, JavaScript와 Html이 혼용된 형태를 보인다.
이 글에서는 강의를 들으며 마주친 JSX의 기본 규칙들을 정리한다.
1. 데이터 바인딩 (a.k.a 중괄호 문법)
기본적으로 JSX는 JavaScript XML(= JS + Markup)의 약자다.
즉 한 파일 안에 JS와 마크업(주로 HTML)이 공존할 수 있고,
JS에서 선언한 값이나 표현식을 마크업 사이에 끼워 넣을 수 있다.
const title = "Card title";
const cardClassName = "card";
const buttonHandler = () => {
console.log("버튼버튼");
};
return (
<div className={cardClassName}>
<h4>{title}</h4>
<button onClick={buttonHandler}>버튼버튼</button>
</div>
);
보다 정확히 하자면, 중괄호 {가 열리면 “여기부터는 JS임”을 선언하는 것과 같다.
반대로 }로 닫으면 “다시 마크업임 ㅇㅇ”으로 돌아온다는 선언이다.
위 예시에서도 className에 JS 변수 cardClassName이, <h4>의 내용물로 title이 들어갔다.
여기서 주의할 점 하나가 있다:
{ }안에는 값으로 평가되는 표현식(expression) 이 들어간다.
표현식(Expression)은 값을 반환하는 형태로, title, cardClassName, a ? b : c 등이 그 예시이다.
문(Statement)은 특정 동작을 수행하고 끝나는 형태로, if (...) { ... } 가 대표적인 사례이다 — 이름부터가 if문(if-statement)이니까.
추가A - 이벤트 핸들러엔 함수를 ‘넘긴다’
다른 말로 하자면, 이벤트 핸들러에서는 함수를 ‘호출’하지 않는다.
위 예시에서 onClick={buttonHandler}를 눈여겨보자.
괄호가 없다 ← 이게 중요하다.
JavaScript는 함수를 일반 값처럼 자유롭게 다룰 수 있는 언어이다 — 이런 속성을 갖는 함수를 ‘일급(First-class) 함수’라고 한다.
따라서 문법적으로는 아래의 두 코드는 모두 문법적으로 문제는 없으나, 동작에서 차이가 발생한다:
-
onClick={buttonHandler}- 실행할 함수 자체(참조) 를 넘긴다.
- React가 클릭이 일어날 때 어떤 함수를 호출할지 알려준다.
- 실제로 동작함
-
onClick={buttonHandler()}- 렌더되는 순간 함수를 즉시 호출한다.
- 이벤트 핸들러는 함수가 아닌, 이 함수의 반환값을 넘겨받게 된다.
- 따라서 클릭하면 아무 일도 일어나지 않는다.
즉 이벤트 핸들러는 보통 “무엇을 실행할지”를 기대하므로,
{ } 안의 표현식이 “함수 그 자체”로 평가돼야 React가 나중에 그걸 불러줄 수 있다는 것이다.
만약 인자를 넘겨야 한다면 화살표 함수(람다식)를 사용한다:
<button onClick={ () => buttonHandler("hello") }>버튼버튼</button>
/* 아래와 동치:
<button onClick={ function tempFunction() { buttonHandler("hello"); } }>버튼버튼</button>
*/
이 규칙은 뒤에서 State의 Setter를 다룰 때 또 등장하니, 여기서 확실히 잡고 가자.
인라인 스타일은 이중 중괄호
일회성 style도 { }로 붙일 수 있다:
<div style={{ color: "red" }}> ... </div>
여기서 style은 객체를 받는데, { }는 JS 진입 기호로도 사용되므로 중괄호가 두 겹 필요하다.
2. class가 아니라 className
HTML/CSS를 배울 땐 클래스를 이렇게 붙였다.
<!-- "card"라는 class 적용 -->
<div class="card"> ... </div>
그러나 JS에는 class라는 예약어가 이미 있다.
먼 옛날, 이 충돌을 피하려고 DOM API는 HTML의 class를 className이라는 이름으로 우회했고,
이후 React가 이를 그대로 미러링했다. 따라서 JSX에서는 className을 써야 한다.
같은 이유로, <label>의 for 속성도 JS의 for 반복문과 겹쳐서 htmlFor로 바뀌었다.
React 16 이상에서는 class를 그대로 써도 동작하는 것처럼 보이는 경우가 있는데,
실제로 실행해보면 React가 경고를 띄우고 버전에 따라 오락가락하므로 얌전히 className을 쓰자.
3. return은 하나의 루트로 묶는다 (Fragment)
Compose에서는, 하나의 함수가 여러 UI 요소(함수)를 실행하고 끝낼 수 있었다:
@Composable
fun MyComposable() {
Text("ㅎㅇㅎㅇ")
Text("ㅂㅇㅂㅇ")
}
그러나 React에서는 아래처럼 형제 태그를 나란히 반환하면 에러가 난다:
function MyComponent() {
return (
<div> ... </div>
<div> ... </div>
);
}
return은 결국 하나의 값(= 하나의 element) 을 반환하는 표현식이라, 최상위에 형제가 둘 이상 올 수 없기 때문이다.
<div>로 감싸도 되지만, 그럼 불필요한 DOM 노드가 하나 늘어나게 된다.
다행히도, Fragment라 불리는 빈 태그를 쓰면, DOM 노드를 늘리지 않고 묶어줄 수 있다:
function MyComponent() {
return (
<>
<div> ... </div>
<div> ... </div>
</>
);
}
사실 프로젝트 생성 시 가장 먼저 만들어지는 App.jsx가 정확히 이 방식을 쓰고 있다.
4. 조건부 렌더링
앞서 { }엔 표현식만 들어간다고 했다.
그래서 JSX 안에서는 if 문을 직접 쓸 수 없고, 표현식으로 분기를 처리한다.
Compose에서는 if (isLoading) { CircularProgressIndicator() }처럼 일상적으로 조건’문’을 사용했기에, 나에게는 조금 낯선 부분이었다.
&&= 조건부 렌더
// isLoading이 true일때 <Spinner />가 그려짐
{isLoading && <Spinner />}
여기서, isLoading이 앞에 등장해야 false일 때 <Spinner />의 호출이 씹히므로 순서에 유의하자.
(단락 계산을 이렇게 사용할 수도 있구나…)
- 삼항 연산자 = 둘 중 하나
{isError ? <RetryButton /> : <Content />}
삼항 연산자 역시 조건의 참/거짓 여부에 따라 값을 반환하므로 사용 가능하다.
5. 리스트 렌더링 (.map() + key)
배열을 UI로 펼칠 때는 .map()으로 각 요소를 JSX로 변환한다.
const posts = ["Post1", "Post2", "Post3"];
return (
<ul>
{posts.map((post, i) => (
<li key={i}>{post}</li>
))}
</ul>
);
여기서 각 요소엔 key 를 붙여야 한다.
React가 리스트의 어떤 항목이 추가/삭제/이동됐는지 추적하는 식별자다.
(Compose의 LazyColumn에서 items(list, key = { it.id })로 key를 주던 것과 정확히 같은 개념)
key로는 가급적 고유 id를 쓰고, 배열 인덱스(i)는 순서가 바뀌지 않는 경우에만 쓰자.
순서가 바뀌는 리스트에 인덱스를 key로 쓰면 엉뚱한 항목이 재사용되는 버그가 생긴다.
결론
뭔가 이것저것 따져야 할 규칙이 많아보이긴 하는데,
원리와 이유를 생각해보면 대충 납득은 가니 잔말 말고 따르십시오.
