JSX를 대체하려는 새로운 문법의 등장

BBetter Stack
Computing/SoftwareInternet Technology

Transcript

00:00:00처음에는 JSX가 있었고, 그 다음에는 TSX가 나왔지만, 우리는 수년간 여기에 머물러 있었습니다.
00:00:04이걸 더 개선할 수는 없을까요? 어쩌면 TSRX가 답이 될지도 모릅니다.
00:00:08기존 것과 비슷하면서도 다릅니다.
00:00:10함수(function) 대신 컴포넌트(component)를 쓰고, 텍스트에는 문자열을 사용하며,
00:00:14안에는 일반적인 if문이 있고, return문도 따로 없습니다.
00:00:17그렇다면 이것은 무엇이고, 왜 등장했으며, 사용해야 할까요? 함께 알아봅시다.
00:00:21[음악]
00:00:26아마 여러분 중 몇 분은 이런 코드를 실제로 본 적이 있을 텐데요,
00:00:29이것은 사실 Ripple의 제작자가 만든 것이기 때문입니다.
00:00:31Rich가 6개월 전에 이 채널에서 다뤘던 새로운 프론트엔드 프레임워크입니다.
00:00:35그러니 이런 소식을 계속 접하려면 구독해 주세요.
00:00:38이들이 한 일은 Ripple에서 사용되던 구문을 추출하여,
00:00:41React, Preact, Solid, Vue, 그리고 당연히 Ripple에서도 작동하게 만든 것입니다.
00:00:45많은 사람들이 이에 대해 꽤나 열광했습니다.
00:00:47TSRX는 가독성을 유지하면서 코드를 한곳에 모아 UI 컴포넌트를 작성하는 방법이라고 소개합니다.
00:00:52즉, 구조, 스타일링, 제어 흐름이 함께 공존하며,
00:00:55그 결과물은 TypeScript와 완전히 하위 호환성을 유지합니다.
00:00:58하지만 이전에 Ripple을 사용해 본 적이 없다면 여전히 그게 무슨 뜻인지 헷갈리실 테니,
00:01:01기능들을 하나씩 살펴보겠습니다.
00:01:03우선 TSRX 파일을 사용하기 때문에 컴파일 단계가 필요하지만,
00:01:07Vite 플러그인을 사용하면 설정이 정말 간단합니다.
00:01:10또한 다른 프레임워크나 런타임을 위한 다양한 옵션도 준비되어 있습니다.
00:01:13실제 컴포넌트의 경우, 여기에 function 대신 component라고 작성하는데,
00:01:17이것은 주로 컴파일러 자체를 위한 키워드이지만,
00:01:20여기에 렌더링 로직이 포함될 것임을 명확하게 보여주기도 합니다.
00:01:24개발자 경험을 높여주는 소소한 개선이라고 볼 수 있겠네요.
00:01:27한 가지 눈에 띄는 점은 여기에 return문이 없다는 것입니다.
00:01:30그 이유는 TSRX가 선언문 기반(statement-based) JSX를 사용하기 때문인데,
00:01:33따라서 컴포넌트 트리를 반환할 필요 없이,
00:01:35렌더링하고 싶은 위치에 마크업을 그냥 작성하면 됩니다.
00:01:37즉, 컴포넌트 상단의 이 카드 위에 다른 p 태그를 그냥 툭 던져 놓으면,
00:01:42작성된 위치에 그대로 렌더링이 된다는 의미입니다.
00:01:44컴포넌트 내에서 여전히 return을 사용할 수 있지만, 값 없이 단독으로만 써야 하며,
00:01:47조기 반환(early return) 목적으로만 사용되어 그 이후의 UI와 로직은 건너뛰게 됩니다.
00:01:51TSRX 컴포넌트는 매우 선형적이라고 생각하면 이해하기 쉽습니다.
00:01:54소스 코드를 작성한 순서가 곧 렌더링 순서가 되며,
00:01:57단순히 위에서 아래로 읽어 내려가면 됩니다.
00:01:59하지만 이로 인해 컴포넌트가 무엇을 렌더링하는지 한눈에 파악하기가 더 어려워질 수도 있습니다.
00:02:03React 같은 곳에서는 보통 바로 return문으로 건너뛰어 확인하니까요.
00:02:06선언문 기반 JSX의 또 다른 장점은 순수 자바스크립트를 훨씬 많이 사용할 수 있다는 점입니다.
00:02:10예를 들어, 조건부 렌더링이 정말 간단해집니다.
00:02:13필요하다면 else-if와 else가 포함된 그냥 일반 if문입니다.
00:02:17조건 안에는 그저 JSX를 하나의 선언문으로 배치하기만 하면 됩니다.
00:02:20React에서 이와 동일한 형태는 종종 중첩된 삼항 연산자로 변하곤 합니다.
00:02:23JSX에서는 모든 분기가 식(expression)이어야 하기 때문입니다.
00:02:26그래서 저는 TSRX 버전이 때로는 더 읽기 편하다고 느낍니다.
00:02:29특히 더 복잡한 조건문이 있을 때 말이죠.
00:02:31하지만 반대로, 아주 단순한 조건만 필요한 경우에는
00:02:35오히려 코드가 더 장황해질 수도 있겠다는 생각도 듭니다.
00:02:37switch문 역시 마찬가지입니다.
00:02:39일반 자바스크립트 switch문을 사용하여 각 케이스와
00:02:41그에 맞춰 렌더링할 JSX를 작성하면 됩니다.
00:02:44이는 동일한 패턴을 구현하기 위해 별도의 함수가 필요한
00:02:47React에서의 처리 방식보다 조금 더 간단합니다.
00:02:49그래서 이 부분은 TSRX가 조금 더 깔끔해 보입니다.
00:02:51하지만 개인적으로 리스트 렌더링 부분은 TSRX가 덜 마음에 듭니다.
00:02:55여기서는 .map을 버리고 대신 for-of 루프를 사용하는데,
00:02:58TSRX는 이 루프를 확장하여 인덱스뿐만 아니라
00:03:01key와 함께 고유한 식별자(identity)도 얻을 수 있게 만들었습니다.
00:03:03그리고 특정 아이템을 건너뛰고 싶을 때는 단순히 continue를 사용하면 됩니다.
00:03:06이 역시 순수 자바스크립트의 흐름에 더 가깝습니다.
00:03:08하지만 앞서 말했듯이, 저는 .map이나 filter 등을 사용하는 데 너무 익숙해져 있어서,
00:03:12기존 방식을 고수하게 될 것 같습니다.
00:03:14그리고 다른 루프 타입인
00:03:17for, for-in, while, do-while 등은 사용할 수 없다는 점도 유의해야 합니다.
00:03:19이 방식은 오직 for-of 루프에서만 작동합니다.
00:03:21순수 자바스크립트를 사용하는 흐름을 이어가자면,
00:03:23TSRX에서 에러 바운더리(error boundary)를 처리하는 방법은 단순한 try-catch 블록입니다.
00:03:27기교가 들어가지 않아 꽤 직관적입니다.
00:03:30그리고 비동기 바운더리(async boundary)가 필요한 경우에도 이 try-catch 블록을 그대로 사용할 수 있는데,
00:03:33단순히 pending 블록을 추가하고
00:03:35그 안에 로딩 컴포넌트를 작성하기만 하면 됩니다.
00:03:38컴파일러는 실제로 이 코드를 가져가서
00:03:40사용 중인 프레임워크에 맞게 변환해 줍니다.
00:03:42React, Preact, Solid의 경우 실제로는 lazy를 사용하고,
00:03:45Ripple에서는 그에 상응하는 Ripple 기능으로 변환됩니다.
00:03:47특히 React에 대해 이야기하자면,
00:03:48지금까지 살펴본 기능들은
00:03:50React의 핵심 규칙 중 하나를 어길 수 있게 해주는 것처럼 보입니다.
00:03:53바로 Hook의 규칙(rule of hooks)입니다.
00:03:54이제 Hook을 조건문이나 조기 반환 뒤에 배치할 수 있으며,
00:03:57심지어 루프 내부에도 둘 수 있습니다.
00:03:58모두 정상적으로 작동할 것입니다.
00:04:00덕분에 코드를 실제로 필요한 위치에 더 잘 모아둘 수 있으며,
00:04:03최종 출력물은 심지어 규칙을 깨뜨리지도 않습니다.
00:04:06컴파일러가 생성된 함수의 최상단으로 모든 Hook을 조용히 끌어올려(hoisting) 주기 때문에,
00:04:09React는 여전히 안정적인 순서로 Hook을 보게 되지만,
00:04:11개발자는 실제로 소속되어야 할 위치에 코드를 작성할 수 있는 것입니다.
00:04:14수년 동안 React를 사용해 온 제 입장에서는,
00:04:16이 기능이 가장 적응하기 힘든 것 중 하나였습니다.
00:04:18또한 이 기능은 컴파일러가
00:04:20보이지 않는 곳에서 많은 마법을 부리게 만듭니다.
00:04:22특히 특정 프레임워크에 맞춰서 말이죠.
00:04:24그래서 만약 이 코드를 디버깅해야 한다면,
00:04:26어떤 코드가 어디에 있는지 헤맬 수도 있을 것 같습니다.
00:04:28하지만 다음으로 소개할 렉시컬 스코핑(lexical scoping)의 경우,
00:04:30모든 중첩된 요소가 자신만의 스코프를 생성합니다.
00:04:32그래서 여기 세 개의 서로 다른 div 블록 안에 const label을 선언할 수 있고,
00:04:36이들은 서로 충돌하지 않습니다.
00:04:37심지어 여기 함수 최상단에 아무도 읽지 않는 label이 하나 더 있어도,
00:04:40마찬가지로 충돌이 발생하지 않습니다.
00:04:41모든 if, for, switch, try문에서도 동일하게 적용됩니다.
00:04:44각각 고유한 스코프를 가지므로,
00:04:46선언한 변수, 실행하는 함수,
00:04:48그리고 도출해 낸 값들이 다른 스코프로 흘러 들어가지 않습니다.
00:04:51이 역시 코드를 한곳에 모으는 데 중점을 둔 기능이며,
00:04:54컴포넌트를 위에서 아래로 읽히는 선형적인 방식으로 만들어 줍니다.
00:04:57자바스크립트 이야기에서 분위기를 바꾸어 스타일링에 대해 이야기해 봅시다.
00:05:00TSRX에서는 실제로 스코프 스타일(scoped style)을 제공합니다.
00:05:02컴포넌트 안에 style 블록을 그냥 넣으면,
00:05:04그 안에 작성한 CSS는 오직 해당 컴포넌트에만 제한(scoped)되며,
00:05:08컴파일 시 클래스 이름에 고유한 해시값이 부여됩니다.
00:05:11이 카드 컴포넌트는 card 클래스를 가지고 있으며,
00:05:13여기서도 card 클래스를 사용하려 하고 있지만,
00:05:16카드 스타일을 전혀 적용받지 못합니다.
00:05:17자체적인 style 블록을 가지고 있지 않기 때문입니다.
00:05:19부모로부터 스타일을 물려받지 않는 이유는
00:05:21그 고유한 해시값을 가지고 있지 않기 때문입니다.
00:05:22만약 컴포넌트 간에 스타일을 공유하고 싶다면,
00:05:24TSRX에 style 키워드가 있습니다.
00:05:26부모가 이 키워드를 사용해 스타일 이름을 전달하면,
00:05:29className을 prop으로 받는 컴포넌트에서
00:05:31생성된 그 고유 해시값이 함께 전달되도록 보장합니다.
00:05:35이제 부모와 동일한 스타일을 가지게 된 것을 확인할 수 있습니다.
00:05:37기술적으로 스타일 주변에 global 셀렉터를 사용하여
00:05:40스코프를 벗어나 스타일을 전역으로 적용할 수도 있지만,
00:05:42그렇게 하면 코드가 조금 지저분해지고
00:05:44스타일이 어디서 오는지 파악하기 어려워질 것 같습니다.
00:05:46개인적으로 저는 완전히 뼈속까지 Tailwind파라서,
00:05:48이 기능을 많이 쓰지는 않고
00:05:50그냥 Tailwind를 계속 사용할 것 같지만,
00:05:51그럼에도 꽤 멋진 기능입니다.
00:05:53다음은 집중해서 보신 분들을 위한 기능입니다.
00:05:56제가 보여드린 코드 블록에서
00:05:57문들이 텍스트를 처리하는 방식에 미세한 차이가 있었습니다.
00:06:01요소 내부의 순수 텍스트는 반드시 큰따옴표로 감싸야 합니다.
00:06:04JSX에서처럼 그냥 텍스트만 적을 수는 없습니다.
00:06:07하지만 여전히 동적 값을 사용할 수 있으며,
00:06:08이 줄처럼 말이죠,
00:06:10이것은 두 개의 큰따옴표 문자열 사이에 있고,
00:06:12TSRX는 컴파일할 때 이를 단순히 하나의 문자열로 결합합니다.
00:06:16또 다른 옵션은 단순히 템플릿 리터럴을 사용하는 것입니다.
00:06:19결과는 동일합니다.
00:06:20저에게는 이 부분이 TSRX를 사용하면서 겪은 소소한 불편함 중 하나였는데,
00:06:23텍스트에 따옴표를 쓰지 않는 습관이 너무 강력하게 몸에 배어 있기 때문입니다.
00:06:26또 다른 텍스트 관련 기능으로는,
00:06:27TSRX가 실제 HTML 마크업이 포함된 문자열을 처리할 수 있다는 점인데,
00:06:31이를 렌더링하는 두 가지 방법이 있습니다.
00:06:33첫 번째는 단순히 text 키워드를 사용하는 것으로,
00:06:35이스케이프된 텍스트를 렌더링하여
00:06:38글자 그대로의 HTML 문자열을 볼 수 있게 해주며,
00:06:40크로스 사이트 스크립팅(XSS)으로부터도 안전합니다.
00:06:42따라서 어떤 값이 확실히 문자열임을 보장하고 싶고,
00:06:45그 문자열의 출처가 다소 모호하여
00:06:48코드를 작성할 때 그 타입을 명확히 알 수 없는 경우에 유용합니다.
00:06:51두 번째 옵션은 문자열을 실제 HTML로 렌더링하고 싶은 경우로,
00:06:54단순히 html 키워드를 사용하면 되고,
00:06:56이것은 문자열을 실제 HTML로 파싱합니다.
00:06:58하지만 이 기능은 오직 Ripple에서만 작동하며, Vue는 이의 아주 제한적인 부분만 지원합니다.
00:07:02React와는 관련이 없지만,
00:07:03Ripple, Vue, 또는 Solid를 사용하시는 분들이
00:07:06흥미로워할 만한 또 다른 기능은 지연 구조분해 할당(lazy destructuring)입니다.
00:07:08이러한 프레임워크에서 평소처럼 props를 구조분해 할당하면,
00:07:10호출 시점에 각 값을 스냅샷으로 캡처하게 되어,
00:07:12접근할 때마다 발생하는 반응성(per-access reactivity)이 깨지게 됩니다.
00:07:14그래서 TSRX에서는 props 앞에 앰퍼샌드(&)를 붙이기만 하면 되는데,
00:07:18구조분해 할당처럼 보이지만,
00:07:20실제 각 바인딩은 컴파일될 때 사용되는 위치의 프로퍼티 조회 코드로 다시 변환됩니다.
00:07:23따라서 런타임은 각각의 접근을 개별적으로 감지하고,
00:07:25시그널(signal) 업데이트가 여전히 리렌더링을 트리거하므로,
00:07:28구조분해 할당이 주는 편리함을 그대로 유지하면서도
00:07:30프레임워크의 반응성도 지켜낼 수 있습니다.
00:07:32마지막으로 보여드릴 기능은 간단하고 유용한 편의 기능입니다.
00:07:35prop에 전달하는 값의 이름이 그 prop과 동일한 이름을 가졌던 적이 있으신가요?
00:07:40가장 흔하게는 on-change 함수 같은 곳에서 말이죠.
00:07:42TSRX를 사용하면 이를 자바스크립트 객체와 유사하게
00:07:45단축 표기법(shorthand)으로 간단히 작성할 수 있습니다.
00:07:47깔끔하고 단순하죠.
00:07:49종합적으로 볼 때, TSRX는 일반적인 자바스크립트의 흐름을 다시 JSX에 녹여내고
00:07:53소소한 편의 기능을 추가하려는 시도로 보이며,
00:07:55꽤 많은 요소가 마음에 듭니다.
00:07:57진짜 아쉬운 점은, AI가 대부분의 코드를 작성하는 현시점에
00:08:01이 기술이 너무 마이너하고 너무 늦게 나왔다는 것입니다.
00:08:03그리고 AI가 잘 작성하는 코드는 주로 JSX와 React이니까요.
00:08:07그렇긴 하지만, Claude에게 TSRX를 사용한 데모를 몇 개 던져주었더니,
00:08:10공식 사이트의 LLM.txt 파일만 보고도 코드를 잘 작성해 내긴 했습니다.
00:08:14그럼에도 저는 그냥 일반 React를 계속 사용할 것 같습니다.
00:08:17또 다른 단점은, 특히 React의 경우,
00:08:19그 위에 더 많은 컴파일러 마법을 얹는 느낌이 들고,
00:08:21제가 수년 동안 익혀온 개발 흐름을 깨뜨리는 것 같기도 합니다.
00:08:24그래서 개인적으로 저에게는 맞지 않지만, 그렇다고 이것이 나쁘다는 뜻은 아닙니다.
00:08:27Svelte에서 넘어오신 분들은 이 방식을 아주 좋아하실 것 같고,
00:08:30이미 Ripple을 사용하고 계신 분들이라면 이것을 이미 사랑하고 계실 겁니다.
00:08:33그러니 마음에 드신다면 아래 댓글로 알려주시고,
00:08:35구독도 해주시고, 늘 그렇듯 다음 영상에서 뵙겠습니다.
00:08:40[음악]

Key Takeaway

TSRX는 return문 없는 선언문 기반 마크업, 자유로운 Hook 배치, 내장 스코프 스타일을 통해 순수 자바스크립트에 가까운 제어 흐름으로 컴포넌트를 작성하게 해주는 새로운 문법 대안입니다.

Highlights

  • TSRX는 Ripple 프레임워크 제작자가 개발하였으며 React, Preact, Solid, Vue, Ripple 모두에서 작동하는 범용 UI 컴포넌트 규격입니다.

  • TSRX 컴포넌트는 return문 없이 작성하는 선언문 기반(statement-based) JSX를 사용하여 소스 코드를 위에서 아래로 작성한 순서대로 화면에 렌더링합니다.

  • React의 Hook 규칙을 우회하여 조건문, 조기 반환(early return), 루프 내부 등 원하는 위치에 Hook을 배치해도 컴파일러가 파일 최상단으로 자동 호이스팅(hoisting)합니다.

  • 컴포넌트 내부에 style 블록을 선언하면 컴파일 시 클래스 이름에 고유 해시값이 부여되어 부모의 CSS가 자식에게 자동 유입되지 않는 스코프 스타일(scoped style)을 제공합니다.

  • 텍스트를 표현할 때 일반 JSX와 달리 요소 내부의 순수 텍스트는 반드시 큰따옴표("")나 템플릿 리터럴로 감싸야 컴파일 에러를 방지할 수 있습니다.

Timeline

TSRX의 정체와 다중 프레임워크 지원

  • TSRX는 프론트엔드 프레임워크인 Ripple의 제작자가 개발한 새로운 컴포넌트 작성 표준입니다.
  • React, Preact, Solid, Vue, Ripple을 아우르는 범용 호환성을 제공합니다.
  • Vite 플러그인을 사용하여 개발 환경 구축과 빌드 컴파일 단계를 거칩니다.

구조와 스타일링, 제어 흐름을 한곳에 유기적으로 모아 컴포넌트를 작성할 수 있도록 돕는 문법입니다. TypeScript와 완벽하게 하위 호환성을 유지하여 기존 타입 시스템을 그대로 사용합니다. Vite 플러그인 등 다양한 런타임을 고려한 도구 모음 덕분에 기존 프로젝트에 도입하기 쉬운 구조를 갖췄습니다.

선언문 기반 JSX와 자바스크립트 제어 흐름의 융합

  • function 대신 component 키워드를 사용하며 UI 결과물을 반환하는 return문이 존재하지 않습니다.
  • 조건부 렌더링을 구현할 때 삼항 연산자 대신 일반적인 JavaScript의 if-else문과 switch문을 그대로 사용합니다.
  • 리스트를 순회할 때 .map 대신 key와 index 추출 및 continue 사용이 가능한 특수 구조의 for-of 루프만 지원합니다.

선언문 기반(statement-based) 설계를 채택하여 컴포넌트 내부에 적힌 순서대로 렌더링이 이루어집니다. 조기 반환(early return)이 필요할 때는 반환 값 없이 단독 return문만 써서 하위 렌더링 로직을 건너뛸 수 있습니다. JSX 특유의 복잡한 중첩 삼항 연산자를 일반 if문으로 대체할 수 있어 가독성이 높아지지만, 단순한 구조에서는 코드 줄 수가 늘어나는 단점도 존재합니다.

에러 처리 및 React Hook 규칙 자동 우회 기술

  • 에러 바운더리와 비동기 바운더리를 순수 자바스크립트의 try-catch 블록으로 처리합니다.
  • React의 핵심 규칙인 Hook의 위치 제약을 깨고 조건문이나 루프 내부에 Hook을 자유롭게 배치할 수 있습니다.
  • 블록 단위의 렉시컬 스코핑(lexical scoping)이 작동하여 중첩 요소 안에서 변수명이 충돌하지 않습니다.

비동기 로딩이 필요할 때는 try-catch에 pending 블록을 추가하여 로딩 컴포넌트를 정의합니다. 컴파일러가 각 프레임워크 규칙에 맞게 React의 lazy나 Ripple의 전용 기능으로 코드를 변환합니다. 개발자가 자유롭게 작성한 Hook 역시 컴파일 과정에서 보이지 않게 최상단으로 자동 끌어올려(hoisting) 주기 때문에 런타임 에러 없이 직관적인 코드 배치가 가능합니다.

컴포넌트 스코프 스타일링과 텍스트 처리 규칙

  • 컴포넌트 내부 style 블록에 선언한 CSS는 고유 해시값이 매겨져 외부 유출이나 유입이 차단됩니다.
  • 요소 내부의 고정 텍스트는 JSX처럼 그냥 쓸 수 없으며 반드시 큰따옴표 문법을 지켜야 합니다.
  • XSS 공격을 막는 이스케이프 렌더링을 위해 text 키워드를 활용합니다.

style 키워드를 활용하면 부모의 고유 CSS 클래스 해시값을 자식 컴포넌트의 className prop으로 연동하여 스타일을 공유할 수 있습니다. 텍스트 바인딩 시 동적 값과 문자열을 함께 쓸 때는 템플릿 리터럴 방식을 권장합니다. 안전한 문자열 출력을 보장하는 text 구문과 더불어 실제 HTML 마크업 파싱을 위한 html 구문도 제공하지만 html 구문은 Ripple에서만 원활히 작동합니다.

지연 구조분해와 단축 표기 및 시장 적용의 한계

  • Ripple, Vue, Solid에서 반응성을 깨뜨리지 않고 props를 나누기 위해 지연 구조분해(&) 기호를 사용합니다.
  • 속성명과 전달 변수명이 같을 때 자바스크립트 객체 단축 표기법과 동일하게 표기할 수 있습니다.
  • AI 코딩 보조가 주류가 된 시점에 출시되어 시장 지배력을 얻기에는 진입 장벽이 큽니다.

props 구조분해 시 반응성이 소실되는 프레임워크 문제를 해결하기 위해 & 기호를 붙이면 컴파일 단계에서 개별 프로퍼티 조회 코드로 변환해 줍니다. on-change와 같은 핸들러 전달 시 축약어로 작성이 편해집니다. 그럼에도 이미 시장에 안착한 React 생태계와 LLM의 높은 JSX 학습 완성도 때문에 주류 기술로 안착하기보다는 특정 Svelte나 Ripple 선호 개발자층 위주로 사용될 가능성이 높습니다.

Community Posts

View all posts