시리즈 번외편
마법같이 모든 것을 엮어서 하나로 만들어주는 이 스캐폴드라는 기능은
도저히 사용할 때마다 어떻게 동작하는지를 이해할 수가 없다.
Rails의 scaffold는 단일 리소스에 대한 CRUD 전 과정을 한 번에 생성·연결해 주는 제너레이터로,
모델·마이그레이션·컨트롤러·뷰·라우트·테스트까지 일괄 생성하고 RESTful 라우팅으로 MVC를 유기적으로 묶는다.
한 줄의 명령어에 엄청난 코드들을 연결해서 생성해 버리는 것이다.
Perplexity를 통해 확인한 바로는 아래와 같은 순서대로 파일을 생성한다.
실행 예시
- bin/rails generate scaffold Post title:string body:text 를 실행하면, 다음이 순서대로 생성된다.
- db/migrate/타임스탬프_create_posts.rb: posts 테이블 생성 마이그레이션.
- app/models/post.rb: Active Record 모델(Post).
- config/routes.rb: resources :posts 라우트 추가(RESTful 7개 액션).
- app/controllers/posts_controller.rb: index/show/new/edit/create/update/destroy 액션.
- app/views/posts/…: index/show/new/edit 및 _form 등 ERB 뷰들.
- 테스트/헬퍼/Jbuilder 등 부가 파일들(프로젝트 설정에 따라 생성).
라우팅은 어떻게 연결되는가?
- scaffold는 config/routes.rb에 resources :posts를 추가한다.
- resources 선언은 다음 RESTful 경로를 한 번에 만든다: index, show, new, edit, create, update, destroy(각각 HTTP 메서드/URL 패턴과 posts_controller의 동명 액션에 매핑).
- 예시: GET /posts → PostsController#index, POST /posts → PostsController#create, GET /posts/:id → PostsController#show 같은 식으로 자동 연결된다.
Controller가 Model-View를 매개로 동작하는 방식
- scaffold 컨트롤러는 전형적인 CRUD 액션을 포함하며, strong parameters와 리다이렉트/렌더 플로우가 미리 작성되어 있다.
- index: @posts = Post.all 로 레코드를 조회하고 index.html.erb 그린다.
- show: @post = Post.find(params:id) 후 show.html.erb 그린다.
- new/edit: 폼에 바인딩할 인스턴스를 준비하고 new/edit 뷰 그린다.
- create/update: post_params로 안전한 속성만 할당해 저장/갱신, 성공 시 redirect_to @post, 실패 시 폼 뷰를 다시 그린다.
- destroy: 레코드 삭제 후 목록 경로로 리다이렉트
중요한 건, 저렇게 생성하는 것도 커스터마이즈가 가능하다는 것이다.
View 템플릿을 특정 표준에 맞게 변경하거나, 모델과 마이그레이션하는 것을 규약화할 수 있고, CRUD를 넘어서 서비스 레이어와 컨트롤러를 자동으로 삽입할 수도 있다.
커스터마이즈만 잘하면, 웬만한 기능은 거의 틀에 찍어내다시피 보일러 플레이트를 만들어둘 수 있겠다는 생각이 들었다.
command + 클릭으로는 들어갈 수 없는 것들,
예를 들면
routes.rb에 자동으로 생성되는 라우트가
온갖 주석으로 설명은 되어 있으나, 직관적으로 resources가 어떤 것을 받아오는 것인지 알 수가 없다.
그래서 이럴 때는 물어보는 것이 제일 좋다.
"LLM 기반 에디터를 사용해야 하는 이유.jpg"
Cursor를 기반으로 원래 존재하는 소스코드를 찾아냈고, 들어가서 원형의 구현체를 확인할 수 있었다.
그나마 아무것도 세팅하지 않았을 때는 정말로 확인할 수 있는 것이 거의 없었는데,
쇼피파이가 만든 Ruby LSP(https://marketplace.cursorapi.com/items/?itemName=Shopify.ruby-lsp)를 설치하니까 웬만한 것들은 들어갈 수 있게 되었다.
3. 명시적으로 참조하지 않았는데, 알아서 참조가 된다.
이 파일에서는 Todo라는 클래스가 ApplicationRecord라는 클래스를 상속받고 있다.
문제는....
ApplicationRecord라는 이름이 클래스가 엄청나게 많을 수도 있는데, 어느 파일에서 가져오는 것인가?
require, import 뭐 아무것도 없는데 그냥 단어 하나로 구현되어 있다.
Rails에는 autoload라는 개념이 있다.
Next.js에서 폴더를 기반으로 app route 되는 것처럼
파일명의 convention으로 알아서 맵핑을 해서 가져온다.
부팅할 때 autoload_paths을 Zeitwerk라는 인스턴스에 등록해 놓고 각 경로들의 Ruby 파일들을 스캔해서 파일명으로부터 기대되는 상수값을 가져와서 맵핑을 해준다.
예를 들면 이렇다.
app/models/comment.rb → Comment
app/controllers/admin/posts_controller.rb → Admin::PostsController
Zeitwerk는 이 규칙을 꽤나 빡빡하게 적용하는데,
규칙 위반이나 누락을 점검하는 기능도 가지고 있다.
Rails의 핵심 철학인 Convention over Configuration에 한 대 얻어맞는 순간이었다.
이런 것을 직접 경험해 보니 예전에 읽었던 글이 떠올랐다.
(Geeknight에서 뵀던 커피한잔의 김재호 님 링크드인에서 본 글)
https://jeho.page/essay/2025/06/13/rails-kakao-coc.html
처음엔 이런 관행이 짜증 나기도 했습니다.
왜 모든 테이블 이름이 복수형이어야만 하지?
Person 모델이 있으면 당연히 테이블 이름도 person이어야 직관적이지 않나? 왜 people이라는 복수형을 강제하는 거지?
이런 생각으로 반항하며 대들 때마다 레일즈는 고통을 돌려주었습니다.
초반에는 Rails와 많이 다투면서
이런 고집스러운 녀석과는 같이 못 살겠다 생각을 했었습니다만…
그 장점을 받아들이고 나서 드디어 친하게 지낼 수 있게 되었습니다.
오히려 제약하고 강제하면서 코딩이 만사 편해질 수 있구나 하는 걸 배웠습니다.
이거 실제로 경험해 보니까 좋긴 한데 동시에 외워야 할 것들이 너무 많아졌다.
규칙이 엄격하다 보니까 따라야 할 행동들이 너무 많아진 것 같은 느낌이다.
타입스크립트가 없는 타입스크립트 느낌이랄까.
DHH는 Rails World 2024에서 이런 이야기를 했다.
"나는 Rails를 쓰라고 강요하지 않는다. 당신이 선택하는 것이다."
그리고 다른 인터뷰에서는 이렇게 이야기했다.
"Doctrine을 쓴 이유는, 오픈소스에서 다른 사람들이 원하는 방식으로 프로젝트가 흘러가지 않는다는 것을 이야기해 주기 위해서예요.
오픈소스는 내가 시간을 써서 세상에 선물하는 것이지, 누군가에게 어떻게 하라고 강요받기 위해서 하는 것이 아닙니다."
DHH가 원하는 것이 이런 것이었구나.
극단적인 생산성.
프레임워크 내에서의 관례만 잘 따르면 모든 CRUD를 한 번에 생성하고,
그 안에서 수정하면서 기능을 개발할 수 있는.
돈을 벌고 싶으면 레일즈를 해라
당근마켓의 중고거래 부분은 Rails로 구성되어 있다.
혼자서 1인으로서 극단적인 생산성과 시장에 제품을 마구잡이로 던지기 위해서는
그것에 맞는 방법론으로 맞서 싸워야 한다.
https://news.hada.io/topic?id=20598
괜히 다른 사람들이 Rails를 찬양하는 것이 아니라는 생각이 들었다.
그리고 동시에 엄청난 책임감이 어깨를 짓누르기 시작했다.
왠지 진짜로 혼자서 제품을 쉽게 만들 수 있을 것 같다는 느낌이,
현실로 다가오기 시작했기 때문이다.
내가 만든 제품이 보안도 뛰어나야 하고,
고객들에게 가치를 부여하는 동시에
시장에서 경쟁을 해야 한다고?
"나 혼자서.....?"