Ver3.0 ๐ ๋ฃจ๋น ์จ ๋ ์ผ์ฆ ํ์คํ ๊ฐ๋ฐ๊ณผ Convention over Configuration ์ฒ ํ์ผ๋ก ์์ฐ์ฑ ๊ทน๋ํํ๊ธฐ

๐ ๋ฃจ๋น ์จ ๋ ์ผ์ฆ ํ์คํ ๊ฐ๋ฐ๊ณผ Convention over Configuration ์ฒ ํ์ผ๋ก ์์ฐ์ฑ ๊ทน๋ํํ๊ธฐ
๊ฐ๋ฐ์์ ํ๋ณต์ ์ํ ํ๋ ์์ํฌ, ๋ ์ผ์ฆ์ ๋ชจ๋ ๊ฒ
๐ฏ ๋ ์ผ์ฆ, ์ ์์ง๋ ๋ง์ ๊ฐ๋ฐ์๋ค์ด ์ฌ๋ํ ๊น?
์๋
! ์ค๋์ ์น ๊ฐ๋ฐ ์ธ๊ณ์์ ์ฌ์ ํ ๊ฐ๋ ฅํ ์ํฅ๋ ฅ์ ๋ฐํํ๊ณ ์๋ ๋ฃจ๋น ์จ ๋ ์ผ์ฆ(Ruby on Rails)์ ๋ํด ์ด์ผ๊ธฐํด๋ณด๋ ค๊ณ ํด. ํน์ "๋ ์ผ์ฆ๋ ์ด๋ฏธ ๊ตฌ์ ์๋์ผ?"๋ผ๊ณ ์๊ฐํ๋ ์น๊ตฌ๋ค์ด ์์ ์๋ ์๋๋ฐ, ์ ํ ๊ทธ๋ ์ง ์์! ๐
๋ ์ผ์ฆ๋ 2004๋
๋ฐ์ด๋น๋ ํ์ด๋ค๋ง์ด์ด ํ์จ(DHH)์ด ๋ง๋ ์ดํ๋ก ์ง๊ธ๊น์ง ์๋ง์ ์คํํธ์
๊ณผ ๋๊ธฐ์
์ ๋ฐฑ๋ณธ์ผ๋ก ํ์ฝํ๊ณ ์์ด. GitHub, Shopify, Airbnb, Basecamp ๊ฐ์ ์๋น์ค๋ค์ด ๋ชจ๋ ๋ ์ผ์ฆ๋ก ๋ง๋ค์ด์ก๊ฑฐ๋ ๋ ์ผ์ฆ๋ฅผ ํต์ฌ ๊ธฐ์ ๋ก ์ฌ์ฉํ๊ณ ์์ง.
ํนํ Convention over Configuration์ด๋ผ๋ ์ฒ ํ์ ๊ฐ๋ฐ์๋ค์ด ๋ฐ๋ณต์ ์ธ ์ค์ ์์
์ ์๊ฐ์ ๋ญ๋นํ์ง ์๊ณ , ์ค์ ๋น์ฆ๋์ค ๋ก์ง ๊ตฌํ์ ์ง์คํ ์ ์๊ฒ ํด์ฃผ๋ ํ๋ช
์ ์ธ ๊ฐ๋
์ด์์ด. ์ด ์ฒ ํ์ด ์ด๋ป๊ฒ ๊ฐ๋ฐ ์์ฐ์ฑ์ ๊ทน๋ํํ๋์ง, ๊ทธ๋ฆฌ๊ณ ํ์คํ ๊ฐ๋ฐ์์ ๋ ์ผ์ฆ๊ฐ ์ด๋ค ๊ฐ์ ์ ๊ฐ์ง๋์ง ํจ๊ป ์ดํด๋ณด์! ๐ฅ
๐ ๋ฃจ๋น ์จ ๋ ์ผ์ฆ๋ ๋ฌด์์ธ๊ฐ?
๋ฃจ๋น ์จ ๋ ์ผ์ฆ๋ ๋ฃจ๋น(Ruby) ํ๋ก๊ทธ๋๋ฐ ์ธ์ด๋ก ์์ฑ๋ ์คํ์์ค ์น ์ ํ๋ฆฌ์ผ์ด์
ํ๋ ์์ํฌ์ผ. ๊ฐ๋จํ '๋ ์ผ์ฆ(Rails)'๋ผ๊ณ ๋ถ๋ฅด๊ธฐ๋ ํ์ง. ์ด ํ๋ ์์ํฌ๋ MVC(Model-View-Controller) ์ํคํ
์ฒ ํจํด์ ๋ฐ๋ฅด๋ฉฐ, ์น ๊ฐ๋ฐ์ ํ์ํ ๊ฑฐ์ ๋ชจ๋ ๊ฒ์ ์ ๊ณตํด์ค. ๐จ
๋ ์ผ์ฆ์ ๊ฐ์ฅ ํฐ ํน์ง์ "๊ฐ๋ฐ์์ ํ๋ณต(Developer Happiness)"์ ์ต์ฐ์ ๊ฐ์น๋ก ๋๋ค๋ ๊ฑฐ์ผ. ์ด๊ฒ ๋ฌด์จ ๋ง์ด๋๋ฉด, ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ณผ์ ์์ฒด๊ฐ ์ฆ๊ฒ๊ณ ์ง๊ด์ ์ด์ด์ผ ํ๋ค๋ ์ฒ ํ์ด์ง. ์ค์ ๋ก ๋ ์ผ์ฆ๋ก ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด "์, ์ด๊ฒ ์ด๋ ๊ฒ ์ฝ๊ฒ ๋๋ค?"๋ผ๋ ๊ฐํ์ด ์ ๋ก ๋์. ๐
2. Convention over Configuration - ์ค์ ๋ณด๋ค ๊ด๋ก๋ฅผ ๋ฐ๋ฅด๋ผ
3. RESTful ๋์์ธ - ์์ ์ค์ฌ์ URL ์ค๊ณ
4. ํ ์คํธ ์ฃผ๋ ๊ฐ๋ฐ(TDD) - ํ ์คํธ๋ฅผ ๋จผ์ ์์ฑํ๋ผ
๋ ์ผ์ฆ๋ ํ์คํ ํ๋ ์์ํฌ๋ก์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ด๋ฆฌ๋ถํฐ ํ๋ก ํธ์๋ ๋ ๋๋ง๊น์ง ๋ชจ๋ ๊ฒ์ ์ฒ๋ฆฌํ ์ ์์ด. ํนํ Active Record๋ผ๋ ๊ฐ๋ ฅํ ORM(Object-Relational Mapping)์ ํตํด ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์
์ ๊ฐ์ฒด ์งํฅ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๊ณ , Action View๋ฅผ ํตํด ๋ทฐ ํ
ํ๋ฆฟ์ ์ฝ๊ฒ ๊ด๋ฆฌํ ์ ์์ง.
์์ฆ ๊ฐ์ ์๋์ ํ๋ก๊ทธ๋๋ฐ ๊ด๋ จ ์ฌ๋ฅ์ ๊ณต์ ํ๊ณ ์ถ๋ค๋ฉด ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์ ํ์ฉํ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ. ๋ ์ผ์ฆ ๊ฐ๋ฐ ๋
ธํ์ฐ๋ฅผ ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค๊ณผ ๋๋๋ฉด์ ์์ต๋ ์ฐฝ์ถํ ์ ์๊ฑฐ๋ ! ๐ฐ
๐๏ธ Convention over Configuration: ์ค์ ์ง์ฅ์์ ๋ฒ์ด๋๊ธฐ
์, ์ด์ ๋ ์ผ์ฆ์ ๊ฐ์ฅ ํ๋ช
์ ์ธ ์ฒ ํ์ธ Convention over Configuration์ ๋ํด ๊น์ด ํํค์ณ๋ณด์. ์ด ๊ฐ๋
์ ์ดํดํ๋ฉด ์ ๋ ์ผ์ฆ๊ฐ ๊ทธ๋ ๊ฒ ์์ฐ์ฑ์ด ๋์์ง ๋ฐ๋ก ์ ์ ์์ด! ๐
์ ํต์ ์ธ ์น ํ๋ ์์ํฌ๋ค์ ์ฌ์ฉํด๋ณธ ์ ์๋ค๋ฉด, ํ๋ก์ ํธ๋ฅผ ์์ํ๊ธฐ ์ ์ ์๋ง์ XML ์ค์ ํ์ผ์ด๋ config ํ์ผ๋ค์ ์์ฑํด์ผ ํ๋ ๊ฒฝํ์ด ์์ ๊ฑฐ์ผ. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์ค์ , URL ๋ผ์ฐํ
์ค์ , ๋ทฐ ๋ฆฌ์กธ๋ฒ ์ค์ ๋ฑ๋ฑ... ์ค์ ๋น์ฆ๋์ค ๋ก์ง์ ์์ฑํ๊ธฐ๋ ์ ์ ์ง์ณ๋ฒ๋ฆฌ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์์ง. ๐ซ
์ด๊ฒ์ด ๋ฐ๋ก CoC ์ฒ ํ์ ์ถ๋ฐ์ ์ด์ผ!
๐ ํ์ผ ๊ตฌ์กฐ์ ๋ค์ด๋ฐ ๊ด๋ก
๋ ์ผ์ฆ์์๋ ํ์ผ์ ์์น์ ์ด๋ฆ๋ง์ผ๋ก๋ ๊ทธ ์ญํ ์ ์๋์ผ๋ก ์ธ์ํด. ์๋ฅผ ๋ค์ด๋ณผ๊ฒ:
๋ชจ๋ธ(Model):
ํ์ผ ์์น: app/models/user.rb
ํด๋์ค ์ด๋ฆ: User
๋ฐ์ดํฐ๋ฒ ์ด์ค ํ
์ด๋ธ: users
์ปจํธ๋กค๋ฌ(Controller):
ํ์ผ ์์น: app/controllers/users_controller.rb
ํด๋์ค ์ด๋ฆ: UsersController
๋ทฐ(View):
ํ์ผ ์์น: app/views/users/index.html.erb
์๋ ์ฐ๊ฒฐ: UsersController์ index ์ก์
๊ณผ ์๋ ๋งคํ
์ด๋ ๊ฒ ์ ํด์ง ๊ท์น๋ง ๋ฐ๋ฅด๋ฉด, ๋ณ๋์ ์ค์ ํ์ผ ์์ด๋ ๋ชจ๋ ๊ฒ์ด ์๋์ผ๋ก ์ฐ๊ฒฐ๋ผ. ๋ง๋ฒ ๊ฐ์ง? โจ
๋ ์ผ์ฆ์์ ๋ธ๋ก๊ทธ ํฌ์คํธ ๊ธฐ๋ฅ์ ๋ง๋ ๋ค๊ณ ๊ฐ์ ํด๋ณด์. ๋ค๋ฅธ ํ๋ ์์ํฌ์๋ค๋ฉด ์์ญ ์ค์ ์ค์ ์ฝ๋๊ฐ ํ์ํ์ ํ ๋ฐ, ๋ ์ผ์ฆ์์๋ ์ด๋ ๊ฒ ๊ฐ๋จํด:
rails generate scaffold Post title:string content:text
rails db:migrate
๋จ ๋ ์ค์ ๋ช
๋ น์ด๋ก CRUD(Create, Read, Update, Delete) ๊ธฐ๋ฅ์ด ์๋ฒฝํ๊ฒ ์๋ํ๋ ๋ธ๋ก๊ทธ ํฌ์คํธ ์์คํ
์ด ์์ฑ๋ผ! ๐ฒ
๐ ์๋ ๋ผ์ฐํ ๊ณผ RESTful ์ค๊ณ
๋ ์ผ์ฆ์ ๋ผ์ฐํ
์์คํ
๋ Convention over Configuration ์ฒ ํ์ ์๋ฒฝํ๊ฒ ๊ตฌํํ๊ณ ์์ด. config/routes.rb ํ์ผ์ ๋จ ํ ์ค๋ง ์ถ๊ฐํ๋ฉด 7๊ฐ์ RESTful ๋ผ์ฐํธ๊ฐ ์๋์ผ๋ก ์์ฑ๋ผ:
resources :posts
์ด ํ ์ค์ด ๋ค์๊ณผ ๊ฐ์ ๋ผ์ฐํธ๋ค์ ์๋์ผ๋ก ๋ง๋ค์ด์ค:
| HTTP ๋ฉ์๋ | ๊ฒฝ๋ก | ์ปจํธ๋กค๋ฌ ์ก์ | ์ฉ๋ |
|---|---|---|---|
| GET | /posts | index | ๋ชจ๋ ํฌ์คํธ ๋ชฉ๋ก |
| GET | /posts/new | new | ์ ํฌ์คํธ ์์ฑ ํผ |
| POST | /posts | create | ํฌ์คํธ ์์ฑ |
| GET | /posts/:id | show | ํน์ ํฌ์คํธ ๋ณด๊ธฐ |
| GET | /posts/:id/edit | edit | ํฌ์คํธ ์์ ํผ |
| PATCH/PUT | /posts/:id | update | ํฌ์คํธ ์ ๋ฐ์ดํธ |
| DELETE | /posts/:id | destroy | ํฌ์คํธ ์ญ์ |
์ด๋ฐ ์๋ํ ๋๋ถ์ ๊ฐ๋ฐ์๋ URL ๊ตฌ์กฐ๋ฅผ ๊ณ ๋ฏผํ๊ฑฐ๋ ๋ผ์ฐํ ์ค์ ์ ์๊ฐ์ ์ธ ํ์๊ฐ ์์ด. ๊ทธ๋ฅ ๋น์ฆ๋์ค ๋ก์ง ๊ตฌํ์๋ง ์ง์คํ๋ฉด ๋ผ! ๐ฏ
๐๏ธ Active Record์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ด๋ก
๋ ์ผ์ฆ์ ORM์ธ Active Record๋ ๊ฐ๋ ฅํ ๊ด๋ก๋ฅผ ๋ฐ๋ผ. ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ
์ด๋ธ๊ณผ ๋ชจ๋ธ ํด๋์ค ์ฌ์ด์ ๋งคํ์ด ์๋์ผ๋ก ์ด๋ฃจ์ด์ง์ง:
ํ
์ด๋ธ ์ด๋ฆ ๊ท์น:
- ๋ชจ๋ธ ํด๋์ค๊ฐ User๋ฉด ํ
์ด๋ธ์ users (๋ณต์ํ)
- ๋ชจ๋ธ ํด๋์ค๊ฐ BlogPost๋ฉด ํ
์ด๋ธ์ blog_posts (์ค๋ค์ดํฌ ์ผ์ด์ค)
์ปฌ๋ผ ์ด๋ฆ ๊ท์น:
- id: ๊ธฐ๋ณธ ํค (์๋ ์์ฑ)
- created_at, updated_at: ํ์์คํฌํ (์๋ ๊ด๋ฆฌ)
- ์ธ๋ํค_id: ๊ด๊ณ ์ค์ (์: user_id)
์ด๋ฐ ๊ด๋ก ๋๋ถ์ ๋ชจ๋ธ์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์
์ ํ ๋ SQL์ ์ง์ ์์ฑํ ํ์๊ฐ ๊ฑฐ์ ์์ด:
# ๋ชจ๋ ์ฌ์ฉ์ ์กฐํ
users = User.all
# ํน์ ์ฌ์ฉ์ ์ฐพ๊ธฐ
user = User.find(1)
# ์กฐ๊ฑด๋ถ ๊ฒ์
active_users = User.where(active: true)
# ์ ์ฌ์ฉ์ ์์ฑ
user = User.create(name: "ํ๊ธธ๋", email: "hong@example.com")
# ์
๋ฐ์ดํธ
user.update(name: "๊น์ฒ ์")
# ์ญ์
user.destroy
๋ณด๋ค์ํผ ์ง๊ด์ ์ด๊ณ ์ฝ๊ธฐ ์ฌ์ด ์ฝ๋๋ก ๋ณต์กํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ ์ ์ํํ ์ ์์ด. ์ด๊ฒ ๋ฐ๋ก Convention over Configuration์ ํ์ด์ผ! ๐ช
๐จ ํ์คํ ๊ฐ๋ฐ: ํ๋ก ํธ์๋๋ถํฐ ๋ฐฑ์๋๊น์ง
๋ ์ผ์ฆ๋ ์ง์ ํ ์๋ฏธ์ ํ์คํ ํ๋ ์์ํฌ์ผ. ๋ฐฑ์๋ ๋ก์ง๋ถํฐ ํ๋ก ํธ์๋ ๋ ๋๋ง, ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ด๋ฆฌ, ์ฌ์ง์ด ์ด๋ฉ์ผ ๋ฐ์ก๊ณผ ๋ฐฑ๊ทธ๋ผ์ด๋ ์์
๊น์ง ๋ชจ๋ ๊ฒ์ ํ๋์ ํ๋ ์์ํฌ ์์์ ์ฒ๋ฆฌํ ์ ์์ง. ๐
์ต๊ทผ์๋ React๋ Vue.js ๊ฐ์ ํ๋ก ํธ์๋ ํ๋ ์์ํฌ์ ํจ๊ป ์ฌ์ฉํ๋ ๊ฒฝ์ฐ๋ ๋ง์ง๋ง, ๋ ์ผ์ฆ ์์ฒด๋ง์ผ๋ก๋ ์ถฉ๋ถํ ๊ฐ๋ ฅํ ์น ์ ํ๋ฆฌ์ผ์ด์
์ ๋ง๋ค ์ ์์ด. ํนํ MVP(Minimum Viable Product)๋ฅผ ๋น ๋ฅด๊ฒ ๋ง๋ค์ด์ผ ํ๋ ์คํํธ์
ํ๊ฒฝ์์๋ ๋ ์ผ์ฆ์ ํ์คํ ๋ฅ๋ ฅ์ด ๋น์ ๋ฐํด!
๐ฅ๏ธ ๋ฐฑ์๋: ๊ฐ๋ ฅํ ์๋ฒ ์ฌ์ด๋ ๋ก์ง
๋ ์ผ์ฆ์ ๋ฐฑ์๋ ๊ธฐ๋ฅ์ ์ ๋ง ๊ฐ๋ ฅํด. MVC ํจํด์ ์๋ฒฝํ๊ฒ ๊ตฌํํ๊ณ ์์ด์ ์ฝ๋ ๊ตฌ์กฐ๊ฐ ๊น๋ํ๊ณ ์ ์ง๋ณด์๊ฐ ์ฌ์.
Model (๋ชจ๋ธ)
Active Record๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ํธ์์ฉํด. ๋น์ฆ๋์ค ๋ก์ง์ ํต์ฌ์ด ์ฌ๊ธฐ์ ๋ค์ด๊ฐ์ง:
class User < ApplicationRecord
# ๊ด๊ณ ์ค์
has_many :posts
has_many :comments
# ์ ํจ์ฑ ๊ฒ์ฆ
validates :email, presence: true, uniqueness: true
validates :name, presence: true, length: { minimum: 2 }
# ๋น๋ฐ๋ฒํธ ์ํธํ
has_secure_password
# ์ปค์คํ
๋ฉ์๋
def full_name
"#{first_name} #{last_name}"
end
# ์ค์ฝํ (์์ฃผ ์ฌ์ฉํ๋ ์ฟผ๋ฆฌ)
scope :active, -> { where(active: true) }
scope :recent, -> { order(created_at: :desc) }
end
Controller (์ปจํธ๋กค๋ฌ)
HTTP ์์ฒญ์ ๋ฐ์์ ์ฒ๋ฆฌํ๊ณ ์๋ต์ ๋ฐํํ๋ ์ญํ ์ ํด. ๋ ์ผ์ฆ์ ์ปจํธ๋กค๋ฌ๋ ์ ๋ง ๊ฐ๊ฒฐํด:
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
before_action :set_post, only: [:show, :edit, :update, :destroy]
def index
@posts = Post.all.recent.page(params[:page])
end
def show
@comments = @post.comments.includes(:user)
end
def create
@post = current_user.posts.build(post_params)
if @post.save
redirect_to @post, notice: 'ํฌ์คํธ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์์ฑ๋์์ต๋๋ค.'
else
render :new, status: :unprocessable_entity
end
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :content, :published)
end
end
๐ญ ํ๋ก ํธ์๋: ๋์ ์ธ ์ฌ์ฉ์ ์ธํฐํ์ด์ค
๋ ์ผ์ฆ์ ํ๋ก ํธ์๋๋ ์ฌ๋ฌ ๋ ์ด์ด๋ก ๊ตฌ์ฑ๋์ด ์์ด:
1. ERB ํ
ํ๋ฆฟ (Embedded Ruby)
HTML ์์ ๋ฃจ๋น ์ฝ๋๋ฅผ ์ฝ์
ํ ์ ์๋ ํ
ํ๋ฆฟ ์์ง์ด์ผ:
<div class="posts-container">
<h1>์ต๊ทผ ํฌ์คํธ</h1>
<% @posts.each do |post| %>
<article class="post-card">
<h2><%= link_to post.title, post_path(post) %></h2>
<p class="author">์์ฑ์: <%= post.user.name %></p>
<p class="excerpt"><%= truncate(post.content, length: 200) %></p>
<div class="meta">
<span><%= time_ago_in_words(post.created_at) %> ์ </span>
<span>๋๊ธ <%= post.comments.count %>๊ฐ</span>
</div>
</article>
<% end %>
<%= paginate @posts %>
</div>
2. Asset Pipeline
CSS, JavaScript, ์ด๋ฏธ์ง ๋ฑ์ ์ ์ ์์ฐ์ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํด. ์๋์ผ๋ก ์์ถํ๊ณ ์ต์ ํํด์ฃผ์ง:
// app/assets/stylesheets/application.css
/*
*= require_tree .
*= require_self
*/
// app/assets/javascripts/application.js
//= require rails-ujs
//= require turbolinks
//= require_tree .
3. Stimulus์ Turbo (Hotwire)
์ต๊ทผ ๋ ์ผ์ฆ๋ Hotwire๋ผ๋ ์๋ก์ด ์ ๊ทผ ๋ฐฉ์์ ๋์
ํ์ด. ์ด๋ Turbo์ Stimulus๋ก ๊ตฌ์ฑ๋์ด ์๋๋ฐ, SPA(Single Page Application)์ฒ๋ผ ๋น ๋ฅธ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๊ณตํ๋ฉด์๋ ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง์ ์ฅ์ ์ ์ ์งํ ์ ์์ด. ๐
Turbo Drive: ํ์ด์ง ์ ํ์ AJAX๋ก ์ฒ๋ฆฌํด์ ์ ์ฒด ํ์ด์ง ์๋ก๊ณ ์นจ ์์ด ๋น ๋ฅด๊ฒ ์ด๋
Turbo Frames: ํ์ด์ง์ ํน์ ๋ถ๋ถ๋ง ์
๋ฐ์ดํธ
Turbo Streams: WebSocket์ ํตํ ์ค์๊ฐ ์
๋ฐ์ดํธ
Stimulus: ๊ฐ๋ฒผ์ด JavaScript ํ๋ ์์ํฌ๋ก DOM ์กฐ์
// Stimulus ์ปจํธ๋กค๋ฌ ์์
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["output"]
connect() {
console.log("Hello from Stimulus!")
}
greet() {
this.outputTarget.textContent = "์๋
ํ์ธ์!"
}
}
๐พ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์
๋ ์ผ์ฆ์ ๋ง์ด๊ทธ๋ ์ด์ ์์คํ ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ๋ณ๊ฒฝ์ ๋ฒ์ ๊ด๋ฆฌํ๋ฏ์ด ๊ด๋ฆฌํ ์ ์๊ฒ ํด์ค. ์ ๋ง ํธ๋ฆฌํด! ๐
# ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ ์์ฑ
rails generate migration AddPublishedToPosts published:boolean
# ์์ฑ๋ ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ
class AddPublishedToPosts < ActiveRecord::Migration[7.0]
def change
add_column :posts, :published, :boolean, default: false
add_index :posts, :published
end
end
# ๋ง์ด๊ทธ๋ ์ด์
์คํ
rails db:migrate
# ๋กค๋ฐฑ (์ค์ํ์ ๋)
rails db:rollback
๋ง์ด๊ทธ๋ ์ด์ ํ์ผ์ Git์ผ๋ก ๊ด๋ฆฌ๋๊ธฐ ๋๋ฌธ์ ํ์๋ค๊ณผ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง๋ฅผ ์ฝ๊ฒ ๋๊ธฐํํ ์ ์์ด. ๋๊ตฐ๊ฐ ์๋ก์ด ์ปฌ๋ผ์ ์ถ๊ฐํ๋ค๋ฉด, ๋ค๋ฅธ ํ์์ ๊ทธ๋ฅ rails db:migrate๋ง ์คํํ๋ฉด ๋ผ! ๐ค
โก ์์ฐ์ฑ์ ๊ทน๋ํํ๋ ๋ ์ผ์ฆ์ ๋๊ตฌ๋ค
๋ ์ผ์ฆ๊ฐ ๊ฐ๋ฐ ์์ฐ์ฑ์ ๋์ด๋ ๊ฑด Convention over Configuration ์ฒ ํ๋ง์ด ์๋์ผ. ๋ค์ํ ๋ด์ฅ ๋๊ตฌ์ Gem(๋ฃจ๋น์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ)๋ค์ด ๊ฐ๋ฐ์ ๋์ฑ ๋น ๋ฅด๊ณ ์ฆ๊ฒ๊ฒ ๋ง๋ค์ด์ค! ๐ ๏ธ
๐ฏ ์ ๋๋ ์ดํฐ (Generators)
๋ ์ผ์ฆ์ ์ ๋๋ ์ดํฐ๋ ๋ฐ๋ณต์ ์ธ ์ฝ๋ ์์ฑ์ ์๋ํํด์ค. ๋ช ๊ฐ์ง ์ ์ฉํ ์ ๋๋ ์ดํฐ๋ฅผ ์ดํด๋ณด์:
# ๋ชจ๋ธ ์์ฑ
rails generate model User name:string email:string age:integer
# ์ปจํธ๋กค๋ฌ ์์ฑ
rails generate controller Users index show new create
# ์ค์บํด๋ (CRUD ์ ์ฒด ์์ฑ)
rails generate scaffold Article title:string body:text published:boolean
# ๋ง์ด๊ทธ๋ ์ด์
์์ฑ
rails generate migration AddIndexToUsersEmail
# ๋ฉ์ผ๋ฌ ์์ฑ
rails generate mailer UserMailer welcome_email
๐ ํ์ Gem๋ค
๋ ์ผ์ฆ ์ํ๊ณ์๋ ์๋ง์ Gem๋ค์ด ์์ด. ๊ฑฐ์ ๋ชจ๋ ๊ธฐ๋ฅ์ ๋ํด ๊ฒ์ฆ๋ Gem์ด ์กด์ฌํ์ง. ์์ฃผ ์ฌ์ฉ๋๋ Gem๋ค์ ์๊ฐํ ๊ฒ:
| Gem ์ด๋ฆ | ์ฉ๋ | ํน์ง |
|---|---|---|
| Devise | ์ฌ์ฉ์ ์ธ์ฆ | ํ์๊ฐ์ , ๋ก๊ทธ์ธ, ๋น๋ฐ๋ฒํธ ์ฌ์ค์ ๋ฑ ์๋ฒฝํ ์ธ์ฆ ์์คํ |
| Pundit | ๊ถํ ๊ด๋ฆฌ | ๊ฐ์ฒด ์งํฅ์ ์ธ ๊ถํ ๊ด๋ฆฌ ์์คํ |
| Kaminari | ํ์ด์ง๋ค์ด์ | ๊น๋ํ๊ณ ์ปค์คํฐ๋ง์ด์ง ๊ฐ๋ฅํ ํ์ด์ง |
| CarrierWave | ํ์ผ ์ ๋ก๋ | ์ด๋ฏธ์ง ์ ๋ก๋ ๋ฐ ๋ฆฌ์ฌ์ด์ง |
| Sidekiq | ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ | ๋น๋๊ธฐ ์์ ์ฒ๋ฆฌ (์ด๋ฉ์ผ ๋ฐ์ก, ๋ฐ์ดํฐ ์ฒ๋ฆฌ ๋ฑ) |
| RSpec | ํ ์คํ | BDD ์คํ์ผ์ ํ ์คํธ ํ๋ ์์ํฌ |
| Ransack | ๊ฒ์ ๊ธฐ๋ฅ | ๋ณต์กํ ๊ฒ์ ์ฟผ๋ฆฌ๋ฅผ ์ฝ๊ฒ ๊ตฌํ |
| Bullet | ์ฑ๋ฅ ์ต์ ํ | N+1 ์ฟผ๋ฆฌ ๋ฌธ์ ๊ฐ์ง |
Gem์ ์ค์นํ๋ ๊ฑด ์ ๋ง ๊ฐ๋จํด. Gemfile์ ์ถ๊ฐํ๊ณ bundle install๋ง ์คํํ๋ฉด ๋ผ:
# Gemfile
gem 'devise'
gem 'pundit'
gem 'kaminari'
# ํฐ๋ฏธ๋์์
bundle install
๐งช ํ ์คํ : ์์ ์ ์ธ ์ฝ๋ ์์ฑ
๋ ์ผ์ฆ๋ ํ ์คํธ ์ฃผ๋ ๊ฐ๋ฐ(TDD)์ ๊ฐ๋ ฅํ๊ฒ ์ง์ํด. ๊ธฐ๋ณธ์ ์ผ๋ก Minitest๊ฐ ๋ด์ฅ๋์ด ์๊ณ , RSpec์ ์ฌ์ฉํ ์๋ ์์ด. ํ ์คํธ ์์ฑ์ด ์ ๋ง ์ฌ์:
# RSpec ์์
require 'rails_helper'
RSpec.describe User, type: :model do
describe 'validations' do
it { should validate_presence_of(:email) }
it { should validate_uniqueness_of(:email) }
it { should validate_length_of(:name).is_at_least(2) }
end
describe 'associations' do
it { should have_many(:posts) }
it { should have_many(:comments) }
end
describe '#full_name' do
it 'returns the full name' do
user = User.new(first_name: 'ํ', last_name: '๊ธธ๋')
expect(user.full_name).to eq('ํ ๊ธธ๋')
end
end
end
# ์ปจํธ๋กค๋ฌ ํ
์คํธ
RSpec.describe PostsController, type: :controller do
describe 'GET #index' do
it 'returns a success response' do
get :index
expect(response).to be_successful
end
it 'assigns @posts' do
post = Post.create(title: 'Test', content: 'Content')
get :index
expect(assigns(:posts)).to include(post)
end
end
end
ํ ์คํธ๋ฅผ ์คํํ๋ ๊ฒ๋ ๊ฐ๋จํด:
# ๋ชจ๋ ํ
์คํธ ์คํ
rspec
# ํน์ ํ์ผ๋ง ํ
์คํธ
rspec spec/models/user_spec.rb
# ํน์ ๋ผ์ธ๋ง ํ
์คํธ
rspec spec/models/user_spec.rb:15
๐ง ๋ ์ผ์ฆ ์ฝ์: ๊ฐ๋ ฅํ ๋๋ฒ๊น ๋๊ตฌ
๋ ์ผ์ฆ ์ฝ์์ ๊ฐ๋ฐ์์ ์ต๊ณ ์ ์น๊ตฌ์ผ. ์ค์๊ฐ์ผ๋ก ์ฝ๋๋ฅผ ํ ์คํธํ๊ณ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์กฐ์ํ ์ ์์ด:
# ๋ ์ผ์ฆ ์ฝ์ ์คํ
rails console
# ๋๋ ์งง๊ฒ
rails c
# ์ฝ์์์ ํ ์ ์๋ ๊ฒ๋ค
> User.count
=> 42
> user = User.first
=> #<User id: 1, name: "ํ๊ธธ๋", email: "hong@example.com">
> user.posts.create(title: "์ ํฌ์คํธ", content: "๋ด์ฉ์
๋๋ค")
=> #<Post id: 1, title: "์ ํฌ์คํธ", ...>
> User.where("created_at > ?", 1.week.ago).count
=> 15
# ํฌํผ ๋ฉ์๋๋ ์ฌ์ฉ ๊ฐ๋ฅ
> helper.number_to_currency(1234.56)
=> "$1,234.56"
ํ๋ก๋์ ํ๊ฒฝ์์๋ ์ฝ๊ธฐ ์ ์ฉ ๋ชจ๋๋ก ์ฝ์์ ์คํํ ์๋ ์์ด:
rails console --sandbox
# ์ด ๋ชจ๋์์๋ ๋ชจ๋ ๋ณ๊ฒฝ์ฌํญ์ด ๋กค๋ฐฑ๋ผ
๐ ์ค์ ํ๋ก์ ํธ: ๋ธ๋ก๊ทธ ํ๋ซํผ ๋ง๋ค๊ธฐ
์ด๋ก ๋ง ๋ฐฐ์ฐ๋ฉด ์ฌ๋ฏธ์์ง? ์ค์ ๋ก ๊ฐ๋จํ ๋ธ๋ก๊ทธ ํ๋ซํผ์ ๋ง๋ค์ด๋ณด๋ฉด์ ๋ ์ผ์ฆ์ ํ์คํ ๊ฐ๋ฐ ๋ฅ๋ ฅ์ ์ฒดํํด๋ณด์! ์ด ์์ ๋ฅผ ๋ฐ๋ผํ๋ฉด ๋ ์ผ์ฆ์ ํต์ฌ ๊ฐ๋ ๋ค์ ๋ชจ๋ ๊ฒฝํํ ์ ์์ด. ๐ป
๐ ํ๋ก์ ํธ ์ค์
# ์ ๋ ์ผ์ฆ ํ๋ก์ ํธ ์์ฑ
rails new blog_platform
# ํ๋ก์ ํธ ๋๋ ํ ๋ฆฌ๋ก ์ด๋
cd blog_platform
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ฑ
rails db:create
# ์๋ฒ ์คํ
rails server
# ๋ธ๋ผ์ฐ์ ์์ http://localhost:3000 ์ ์
๐ค ์ฌ์ฉ์ ์ธ์ฆ ๊ตฌํ
๋จผ์ Devise๋ฅผ ์ฌ์ฉํด์ ์ฌ์ฉ์ ์ธ์ฆ์ ๊ตฌํํด๋ณด์:
# Gemfile์ ์ถ๊ฐ
gem 'devise'
# ์ค์น
bundle install
# Devise ์ค์
rails generate devise:install
# User ๋ชจ๋ธ ์์ฑ
rails generate devise User
# ๋ง์ด๊ทธ๋ ์ด์
์คํ
rails db:migrate
# Devise ๋ทฐ ์ปค์คํฐ๋ง์ด์ง (์ ํ์ฌํญ)
rails generate devise:views
์ด์ ํ์๊ฐ์ , ๋ก๊ทธ์ธ, ๋ก๊ทธ์์ ๊ธฐ๋ฅ์ด ๋ชจ๋ ์๋ํด! ์ ๋ง ๊ฐ๋จํ์ง? ๐
๐ ํฌ์คํธ ๋ชจ๋ธ ์์ฑ
# Post ๋ชจ๋ธ ์์ฑ
rails generate model Post title:string content:text published:boolean user:references
# ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ ์์ (๊ธฐ๋ณธ๊ฐ ์ถ๊ฐ)
class CreatePosts < ActiveRecord::Migration[7.0]
def change
create_table :posts do |t|
t.string :title, null: false
t.text :content
t.boolean :published, default: false
t.references :user, null: false, foreign_key: true
t.timestamps
end
add_index :posts, :published
add_index :posts, :created_at
end
end
# ๋ง์ด๊ทธ๋ ์ด์
์คํ
rails db:migrate
๋ชจ๋ธ์ ๊ด๊ณ์ ์ ํจ์ฑ ๊ฒ์ฆ์ ์ถ๊ฐํ์:
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :posts, dependent: :destroy
has_many :comments, dependent: :destroy
validates :name, presence: true, length: { minimum: 2, maximum: 50 }
end
# app/models/post.rb
class Post < ApplicationRecord
belongs_to :user
has_many :comments, dependent: :destroy
validates :title, presence: true, length: { minimum: 5, maximum: 100 }
validates :content, presence: true, length: { minimum: 10 }
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc) }
scope :by_user, ->(user) { where(user: user) }
def excerpt(length = 200)
content.truncate(length)
end
end
๐ฎ ์ปจํธ๋กค๋ฌ์ ๋ผ์ฐํ
# config/routes.rb
Rails.application.routes.draw do
devise_for :users
root 'posts#index'
resources :posts do
resources :comments, only: [:create, :destroy]
member do
patch :publish
patch :unpublish
end
end
namespace :dashboard do
resources :posts
end
end
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
before_action :set_post, only: [:show, :edit, :update, :destroy, :publish, :unpublish]
before_action :authorize_user!, only: [:edit, :update, :destroy]
def index
@posts = Post.published.recent.includes(:user).page(params[:page])
end
def show
@comments = @post.comments.includes(:user).order(created_at: :desc)
@comment = Comment.new
end
def new
@post = current_user.posts.build
end
def create
@post = current_user.posts.build(post_params)
if @post.save
redirect_to @post, notice: 'ํฌ์คํธ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์์ฑ๋์์ต๋๋ค.'
else
render :new, status: :unprocessable_entity
end
end
def edit
end
def update
if @post.update(post_params)
redirect_to @post, notice: 'ํฌ์คํธ๊ฐ ์
๋ฐ์ดํธ๋์์ต๋๋ค.'
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@post.destroy
redirect_to posts_url, notice: 'ํฌ์คํธ๊ฐ ์ญ์ ๋์์ต๋๋ค.'
end
def publish
@post.update(published: true)
redirect_to @post, notice: 'ํฌ์คํธ๊ฐ ๋ฐํ๋์์ต๋๋ค.'
end
def unpublish
@post.update(published: false)
redirect_to @post, notice: 'ํฌ์คํธ๊ฐ ๋น๊ณต๊ฐ ์ฒ๋ฆฌ๋์์ต๋๋ค.'
end
private
def set_post
@post = Post.find(params[:id])
end
def authorize_user!
unless @post.user == current_user
redirect_to root_path, alert: '๊ถํ์ด ์์ต๋๋ค.'
end
end
def post_params
params.require(:post).permit(:title, :content, :published)
end
end
๐จ ๋ทฐ ํ ํ๋ฆฟ
<!-- app/views/posts/index.html.erb -->
<div class="container">
<div class="header">
<h1>๐ ๋ธ๋ก๊ทธ ํฌ์คํธ</h1>
<% if user_signed_in? %>
<%= link_to '์ ํฌ์คํธ ์์ฑ', new_post_path, class: 'btn btn-primary' %>
<% end %>
</div>
<div class="posts-grid">
<% @posts.each do |post| %>
<article class="post-card">
<h2><%= link_to post.title, post_path(post) %></h2>
<div class="post-meta">
<span class="author">
๐ค <%= post.user.name %>
</span>
<span class="date">
๐
<%= time_ago_in_words(post.created_at) %> ์
</span>
</div>
<p class="excerpt">
<%= post.excerpt %>
</p>
<div class="post-footer">
<span>๐ฌ ๋๊ธ <%= post.comments.count %>๊ฐ</span>
<%= link_to '์์ธํ ๋ณด๊ธฐ โ', post_path(post), class: 'read-more' %>
</div>
</article>
<% end %>
</div>
<%= paginate @posts %>
</div>
<!-- app/views/posts/show.html.erb -->
<div class="container">
<article class="post-detail">
<header>
<h1><%= @post.title %></h1>
<div class="post-meta">
<span>์์ฑ์: <%= @post.user.name %></span>
<span>์์ฑ์ผ: <%= l(@post.created_at, format: :long) %></span>
<% if @post.updated_at != @post.created_at %>
<span>์์ ์ผ: <%= l(@post.updated_at, format: :long) %></span>
<% end %>
</div>
<% if user_signed_in? && @post.user == current_user %>
<div class="post-actions">
<%= link_to '์์ ', edit_post_path(@post), class: 'btn btn-secondary' %>
<%= link_to '์ญ์ ', post_path(@post), method: :delete,
data: { confirm: '์ ๋ง ์ญ์ ํ์๊ฒ ์ต๋๊น?' },
class: 'btn btn-danger' %>
<% if @post.published? %>
<%= link_to '๋น๊ณต๊ฐ๋ก ์ ํ', unpublish_post_path(@post),
method: :patch, class: 'btn btn-warning' %>
<% else %>
<%= link_to '๋ฐํํ๊ธฐ', publish_post_path(@post),
method: :patch, class: 'btn btn-success' %>
<% end %>
</div>
<% end %>
</header>
<div class="post-content">
<%= simple_format(@post.content) %>
</div>
</article>
<section class="comments-section">
<h2>๐ฌ ๋๊ธ (<%= @comments.count %>)</h2>
<% if user_signed_in? %>
<%= form_with model: [@post, @comment], local: true do |f| %>
<div class="form-group">
<%= f.text_area :content, rows: 4,
placeholder: '๋๊ธ์ ์
๋ ฅํ์ธ์...',
class: 'form-control' %>
</div>
<%= f.submit '๋๊ธ ์์ฑ', class: 'btn btn-primary' %>
<% end %>
<% else %>
<p>๋๊ธ์ ์์ฑํ๋ ค๋ฉด <%= link_to '๋ก๊ทธ์ธ', new_user_session_path %>ํ์ธ์.</p>
<% end %>
<div class="comments-list">
<% @comments.each do |comment| %>
<div class="comment">
<div class="comment-header">
<strong><%= comment.user.name %></strong>
<span class="comment-date">
<%= time_ago_in_words(comment.created_at) %> ์
</span>
</div>
<div class="comment-content">
<%= simple_format(comment.content) %>
</div>
<% if user_signed_in? && comment.user == current_user %>
<%= link_to '์ญ์ ', post_comment_path(@post, comment),
method: :delete,
data: { confirm: '๋๊ธ์ ์ญ์ ํ์๊ฒ ์ต๋๊น?' },
class: 'comment-delete' %>
<% end %>
</div>
<% end %>
</div>
</section>
</div>
โ Devise๋ฅผ ์ฌ์ฉํ ์ฌ์ฉ์ ์ธ์ฆ
โ ๋ชจ๋ธ ๊ฐ์ ๊ด๊ณ ์ค์ (has_many, belongs_to)
โ RESTful ๋ผ์ฐํ ๊ณผ ์ปค์คํ ์ก์
โ ๊ถํ ๊ด๋ฆฌ (before_action ํํฐ)
โ ๋ทฐ ํ ํ๋ฆฟ๊ณผ ํฌํผ ๋ฉ์๋
โ ํ์ด์ง๋ค์ด์
์ด ๋ชจ๋ ๊ฒ์ 100์ค ๋ฏธ๋ง์ ํต์ฌ ์ฝ๋๋ก ๊ตฌํํ์ด! ์ด๊ฒ ๋ฐ๋ก ๋ ์ผ์ฆ์ ์์ฐ์ฑ์ด์ผ! ๐
๐ ๋ ์ผ์ฆ์ ์ฅ๋จ์ ๊ณผ ์ ํฉํ ํ๋ก์ ํธ
๋ชจ๋ ๊ธฐ์ ์ด ๊ทธ๋ ๋ฏ, ๋ ์ผ์ฆ๋ ์ฅ๋จ์ ์ด ์์ด. ํ๋ก์ ํธ์ ๋ ์ผ์ฆ๋ฅผ ์ฌ์ฉํ ์ง ๊ฒฐ์ ํ๊ธฐ ์ ์ ์ด๋ฅผ ์ ์ดํดํ๋ ๊ฒ ์ค์ํด! ๐ค
โจ ๋ ์ผ์ฆ์ ์ฅ์
2. ๐ ํ๋ถํ ์ํ๊ณ
์๋ง์ Gem๋ค์ด ์กด์ฌํด์ ๊ฑฐ์ ๋ชจ๋ ๊ธฐ๋ฅ์ ๋ฐํด๋ฅผ ์ฌ๋ฐ๋ช ํ์ง ์๊ณ ๊ตฌํํ ์ ์์ด. ์ธ์ฆ, ๊ฒฐ์ , ํ์ผ ์ ๋ก๋, ๊ฒ์ ๋ฑ ํ์ํ ๊ฑด ๋ค ์์ด!
3. ๐งช ํ ์คํธ ์นํ์
ํ ์คํธ ์์ฑ์ด ์ ๋ง ์ฝ๊ณ , ์ปค๋ฎค๋ํฐ๊ฐ TDD๋ฅผ ๊ฐ์กฐํ๊ธฐ ๋๋ฌธ์ ํ์ง ๋์ ์ฝ๋๋ฅผ ์์ฑํ๊ธฐ ์ข์.
4. ๐ฅ ํ๋ฐํ ์ปค๋ฎค๋ํฐ
20๋ ๊ฐ๊น์ด ๋ฐ์ ํด์จ ํ๋ ์์ํฌ๋ต๊ฒ ์ปค๋ฎค๋ํฐ๊ฐ ์ ๋ง ํ๋ฐํด. ๋ฌธ์ ๊ฐ ์๊ธฐ๋ฉด Stack Overflow๋ GitHub์์ ์ฝ๊ฒ ๋ต์ ์ฐพ์ ์ ์์ด.
5. ๐จ ๊ฐ๋ฐ์ ๊ฒฝํ(DX)
์ฝ๋๊ฐ ์ฝ๊ธฐ ์ฝ๊ณ ์ง๊ด์ ์ด์ผ. ๋ฃจ๋น ์ธ์ด ์์ฒด๊ฐ ์ฌ๋์ด ์ฝ๊ธฐ ์ฝ๊ฒ ์ค๊ณ๋์๊ฑฐ๋ . "์ฝ๋๋ ์ปดํจํฐ๋ณด๋ค ์ฌ๋์ด ์ฝ๊ธฐ ์ํด ์์ฑ๋๋ค"๋ ์ฒ ํ์ด ๋ น์์์ด.
6. ๐ ๋ณด์
CSRF, SQL ์ธ์ ์ , XSS ๋ฑ ์ผ๋ฐ์ ์ธ ๋ณด์ ์ํ์ ๋ํ ๋ฐฉ์ด๊ฐ ๊ธฐ๋ณธ์ผ๋ก ๋ด์ฅ๋์ด ์์ด. ๊ฐ๋ฐ์๊ฐ ์ค์ํ๊ธฐ ์ด๋ ค์ด ๊ตฌ์กฐ์ผ.
7. ๐ฑ ํ์คํ ๋ฅ๋ ฅ
ํ๋ก ํธ์๋๋ถํฐ ๋ฐฑ์๋, ๋ฐ์ดํฐ๋ฒ ์ด์ค๊น์ง ๋ชจ๋ ๊ฒ์ ํ๋์ ํ๋ ์์ํฌ๋ก ์ฒ๋ฆฌํ ์ ์์ด. ์์ ํ์์ ํฐ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค๊ธฐ ์ข์.
โ ๏ธ ๋ ์ผ์ฆ์ ๋จ์
2. ๐ฆ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋
๋ ์ผ์ฆ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์๋์ ์ผ๋ก ๋ง์ด ์ฌ์ฉํด. ํนํ ๋ง์ Gem์ ์ฌ์ฉํ๋ฉด ๋ ๊ทธ๋. ํ์ง๋ง ์์ฆ ์๋ฒ ๋ฉ๋ชจ๋ฆฌ๊ฐ ์ ๋ ดํด์ ์ค๋ฌด์์๋ ํฐ ๋ฌธ์ ๊ฐ ์ ๋ผ.
3. ๐ ํ์ต ๊ณก์
Convention over Configuration์ ์๋ ์ ๊ฒ์ด์ผ. ๊ด๋ก๋ฅผ ๋ชจ๋ฅด๋ฉด "์ด๊ฒ ์ ์๋ํ์ง?"๋ผ๊ณ ํผ๋์ค๋ฌ์ธ ์ ์์ด. ํ์ง๋ง ํ๋ฒ ์ตํ๋ฉด ์ ๋ง ์์ฐ์ ์ด์ผ!
4. ๐ง ๋ง์ดํฌ๋ก์๋น์ค์๋ ๊ณผํ ์ ์์
๋ ์ผ์ฆ๋ ๋ชจ๋๋ฆฌ์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ต์ ํ๋์ด ์์ด. ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ๋ฅผ ์ํ๋ค๋ฉด rails new --api๋ก API ์ ์ฉ ์ฑ์ ๋ง๋ค๊ฑฐ๋, ๋ค๋ฅธ ๊ฒฝ๋ ํ๋ ์์ํฌ๋ฅผ ๊ณ ๋ คํด๋ณผ ์ ์์ด.
5. ๐ฏ ํน์ ๋๋ฉ์ธ์๋ ๋ถ์ ํฉ
์ค์๊ฐ ๊ฒ์, ๊ณ ์ฑ๋ฅ ๋ฐ์ดํฐ ์ฒ๋ฆฌ, ์์คํ ํ๋ก๊ทธ๋๋ฐ ๊ฐ์ ๋ถ์ผ์๋ ๋ ์ผ์ฆ๊ฐ ์ ํฉํ์ง ์์. ๊ฐ ๋๋ฉ์ธ์ ๋ง๋ ๋๊ตฌ๋ฅผ ์ ํํ๋ ๊ฒ ์ค์ํด!
๐ฏ ๋ ์ผ์ฆ๊ฐ ์ ํฉํ ํ๋ก์ ํธ
๋ ์ผ์ฆ๋ ๋ค์๊ณผ ๊ฐ์ ํ๋ก์ ํธ์ ํนํ ์ ๋ง์:
| ํ๋ก์ ํธ ์ ํ | ์ด์ |
|---|---|
| ์คํํธ์ MVP | ๋น ๋ฅธ ๊ฐ๋ฐ ์๋๋ก ์์ด๋์ด๋ฅผ ์ ์ํ๊ฒ ๊ฒ์ฆ |
| SaaS ํ๋ซํผ | ์ฌ์ฉ์ ๊ด๋ฆฌ, ๊ตฌ๋ , ๊ฒฐ์ ๋ฑ ํ์ํ ๋ชจ๋ ๊ธฐ๋ฅ ๊ตฌํ ์ฉ์ด |
| ์ ์์๊ฑฐ๋ | Spree, Solidus ๊ฐ์ ๊ฒ์ฆ๋ ์ด์ปค๋จธ์ค Gem ํ์ฉ ๊ฐ๋ฅ |
| ์ฝํ ์ธ ๊ด๋ฆฌ ์์คํ | ๋ธ๋ก๊ทธ, ๋ด์ค ์ฌ์ดํธ, ํฌ๋ผ ๋ฑ์ ์ต์ |
| ์์ ๋คํธ์ํฌ | ์ฌ์ฉ์ ๊ฐ ๊ด๊ณ, ํผ๋, ์๋ฆผ ๋ฑ ๊ตฌํ์ด ์ฌ์ |
| ๋ด๋ถ ๊ด๋ฆฌ ๋๊ตฌ | Admin ํจ๋, ๋์๋ณด๋ ๋ฑ์ ๋น ๋ฅด๊ฒ ๊ตฌ์ถ |
| API ์๋ฒ | RESTful API๋ GraphQL ์๋ฒ๋ก ํ์ฉ ๊ฐ๋ฅ |
Shopify: ์ ์ธ๊ณ ์๋ฐฑ๋ง ์จ๋ผ์ธ ์คํ ์ด๋ฅผ ์ง์ํ๋ ์ด์ปค๋จธ์ค ํ๋ซํผ
Airbnb: ์ด๊ธฐ์ ๋ ์ผ์ฆ๋ก ๊ตฌ์ถ๋์ด ๋น ๋ฅด๊ฒ ์ฑ์ฅ
Basecamp: ๋ ์ผ์ฆ๋ฅผ ๋ง๋ ํ์ฌ์ ํ๋ก์ ํธ ๊ด๋ฆฌ ๋๊ตฌ
Twitch: ์ด๊ธฐ ๋ฒ์ ์ด ๋ ์ผ์ฆ๋ก ๋ง๋ค์ด์ก์ด
์ด๋ฐ ๋๊ท๋ชจ ์๋น์ค๋ค์ด ๋ ์ผ์ฆ๋ฅผ ์ฌ์ฉํ๋ค๋ ๊ฑด, ํ์ฅ์ฑ ๋ฌธ์ ๋ ์ถฉ๋ถํ ํด๊ฒฐ ๊ฐ๋ฅํ๋ค๋ ์ฆ๊ฑฐ์ผ! ๐
๐ฎ ๋ ์ผ์ฆ์ ๋ฏธ๋์ ์ต์ ํธ๋ ๋
"๋ ์ผ์ฆ๋ ์ด์ ๊ตฌ์ ์๋์ผ?"๋ผ๋ ์ง๋ฌธ์ ์์ฃผ ๋ฐ๋๋ฐ, ์ ํ ๊ทธ๋ ์ง ์์! ๋ ์ผ์ฆ๋ ๊ณ์ ์งํํ๊ณ ์๊ณ , ์ต์ ์น ๊ฐ๋ฐ ํธ๋ ๋๋ฅผ ์ ๊ทน์ ์ผ๋ก ์์ฉํ๊ณ ์์ด. ๐
โก Hotwire: SPA ์์ด SPA ๊ฐ์ ๊ฒฝํ
๋ ์ผ์ฆ 7์์ ๋์
๋ Hotwire๋ ๊ฒ์ ์ฒด์ธ์ ์ผ. React๋ Vue.js ์์ด๋ ๋น ๋ฅด๊ณ ๋ฐ์์ ์ธ ์ฌ์ฉ์ ์ธํฐํ์ด์ค๋ฅผ ๋ง๋ค ์ ์์ด:
Turbo Drive: ํ์ด์ง ์ ํ์ AJAX๋ก ์ฒ๋ฆฌํด์ ์ ์ฒด ํ์ด์ง ์๋ก๊ณ ์นจ ์์ด ๋น ๋ฅด๊ฒ ์ด๋
Turbo Frames: ํ์ด์ง์ ํน์ ๋ถ๋ถ๋ง ๋
๋ฆฝ์ ์ผ๋ก ์
๋ฐ์ดํธ
Turbo Streams: WebSocket์ ํตํ ์ค์๊ฐ ์
๋ฐ์ดํธ
Stimulus: ์ต์ํ์ JavaScript๋ก ์ธํฐ๋ํฐ๋ธํ UI ๊ตฌํ
<!-- Turbo Frame ์์ -->
<turbo-frame id="messages">
<%= render @messages %>
</turbo-frame>
<!-- ์ด ๋งํฌ๋ฅผ ํด๋ฆญํ๋ฉด messages ํ๋ ์๋ง ์
๋ฐ์ดํธ๋จ -->
<%= link_to "๋ ๋ณด๊ธฐ", messages_path, data: { turbo_frame: "messages" } %>
<!-- Turbo Stream์ผ๋ก ์ค์๊ฐ ์
๋ฐ์ดํธ -->
<%= turbo_stream_from "chat_room_#{@room.id}" %>
# ์ปจํธ๋กค๋ฌ์์
def create
@message = @room.messages.create(message_params)
respond_to do |format|
format.turbo_stream
format.html { redirect_to @room }
end
end
# app/views/messages/create.turbo_stream.erb
<%= turbo_stream.append "messages", @message %>
๐จ Import Maps: JavaScript ๋ฒ๋ค๋ฌ ์์ด
๋ ์ผ์ฆ 7์ Import Maps๋ฅผ ๋์ ํด์ Webpack์ด๋ esbuild ๊ฐ์ ๋ณต์กํ ๋ฒ๋ค๋ฌ ์์ด๋ ๋ชจ๋ JavaScript๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ํ์ด:
# config/importmap.rb
pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
# JavaScript ํ์ผ์์
import { Turbo } from "@hotwired/turbo-rails"
import { Application } from "@hotwired/stimulus"
๐ Credentials์ ํ๊ฒฝ ๋ณ์ ๊ด๋ฆฌ
๋ ์ผ์ฆ๋ ๋ฏผ๊ฐํ ์ ๋ณด๋ฅผ ์์ ํ๊ฒ ๊ด๋ฆฌํ๋ ๊ฐ๋ ฅํ ์์คํ ์ ์ ๊ณตํด:
# credentials ํธ์ง
rails credentials:edit
# ํ๊ฒฝ๋ณ credentials
rails credentials:edit --environment production
# credentials.yml.enc ํ์ผ ๋ด์ฉ
aws:
access_key_id: YOUR_KEY
secret_access_key: YOUR_SECRET
# ์ฝ๋์์ ์ฌ์ฉ
Rails.application.credentials.aws[:access_key_id]
๐ฑ Action Mailbox & Action Text
๋ ์ผ์ฆ 6๋ถํฐ ์ถ๊ฐ๋ ์ด ๊ธฐ๋ฅ๋ค์ ์ด๋ฉ์ผ ์ฒ๋ฆฌ์ ๋ฆฌ์น ํ
์คํธ ํธ์ง์ ์ฝ๊ฒ ๋ง๋ค์ด์ค:
Action Mailbox: ์์ ์ด๋ฉ์ผ์ ๋ผ์ฐํ
ํ๊ณ ์ฒ๋ฆฌ
Action Text: Trix ์๋ํฐ๋ฅผ ์ฌ์ฉํ ๋ฆฌ์น ํ
์คํธ ์ฝํ
์ธ ๊ด๋ฆฌ
# Action Text ์ฌ์ฉ
class Post < ApplicationRecord
has_rich_text :content
end
# ๋ทฐ์์
<%= form.rich_text_area :content %>
# Action Mailbox ๋ผ์ฐํ
class ForwardsMailbox < ApplicationMailbox
def process
# ์ด๋ฉ์ผ ์ฒ๋ฆฌ ๋ก์ง
record_forward
end
private
def record_forward
Forward.create!(
from: mail.from,
subject: mail.subject,
content: mail.body
)
end
end
โจ ๋ ๊ฐ๋ ฅํ ๋น๋๊ธฐ ์ฒ๋ฆฌ
โจ ํฅ์๋ ์ฑ๋ฅ ์ต์ ํ
โจ ๋ ๋์ TypeScript ์ง์
โจ ๊ฐ์ ๋ ๊ฐ๋ฐ์ ๋๊ตฌ
DHH์ ๋ ์ผ์ฆ ์ฝ์ด ํ์ ์ฌ์ ํ ํ๋ฐํ๊ฒ ํ๋ ์์ํฌ๋ฅผ ๋ฐ์ ์ํค๊ณ ์์ด!
๐ผ ๋ ์ผ์ฆ ๊ฐ๋ฐ์๋ก ์ฑ์ฅํ๊ธฐ
๋ ์ผ์ฆ๋ฅผ ๋ฐฐ์ฐ๊ณ ์ถ๋ค๋ฉด ์ด๋ป๊ฒ ์์ํด์ผ ํ ๊น? ๊ทธ๋ฆฌ๊ณ ๋ ์ผ์ฆ ๊ฐ๋ฐ์๋ก์ ์ด๋ป๊ฒ ์ฑ์ฅํ ์ ์์๊น? ์ค์ฉ์ ์ธ ์กฐ์ธ์ ํด์ค๊ฒ! ๐
๐ ํ์ต ๋ก๋๋งต
- ๋ธ๋ก, Proc, Lambda
- ๋ชจ๋๊ณผ ๋ฏน์ค์ธ
- ๊ธฐ๋ณธ ์๋ฃ๊ตฌ์กฐ (Array, Hash, String)
2๋จ๊ณ: ๋ ์ผ์ฆ ๊ธฐ์ด (2-3์ฃผ)
- MVC ์ํคํ ์ฒ ์ดํด
- ๋ผ์ฐํ ๊ณผ ์ปจํธ๋กค๋ฌ
- Active Record ๊ธฐ๋ณธ
- ๋ทฐ์ ํฌํผ
- ๊ฐ๋จํ CRUD ์ ํ๋ฆฌ์ผ์ด์ ๋ง๋ค๊ธฐ
3๋จ๊ณ: ์ค๊ธ ๊ฐ๋ (3-4์ฃผ)
- ๊ด๊ณ ์ค์ (has_many, belongs_to, has_many :through)
- ์ ํจ์ฑ ๊ฒ์ฆ๊ณผ ์ฝ๋ฐฑ
- ์ธ์ฆ๊ณผ ๊ถํ ๊ด๋ฆฌ
- ํ์ผ ์ ๋ก๋
- ํ์ด์ง๋ค์ด์ ๊ณผ ๊ฒ์
4๋จ๊ณ: ๊ณ ๊ธ ์ฃผ์ (์ง์์ )
- ํ ์คํ (RSpec, Capybara)
- ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ (Sidekiq)
- API ๊ฐ๋ฐ
- ์ฑ๋ฅ ์ต์ ํ
- ๋ฐฐํฌ์ DevOps
- Hotwire์ ๋ชจ๋ ํ๋ก ํธ์๋
๐ ์ถ์ฒ ํ์ต ๋ฆฌ์์ค
๊ณต์ ๋ฌธ์:
- Rails Guides (guides.rubyonrails.org) - ๊ฐ์ฅ ๊ถ์ ์๋ ๋ฌธ์
- API Documentation (api.rubyonrails.org)
์จ๋ผ์ธ ๊ฐ์:
- GoRails (gorails.com) - ์ค์ ์ค์ฌ์ ์คํฌ๋ฆฐ์บ์คํธ
- The Odin Project - ๋ฌด๋ฃ ํ์คํ ์ปค๋ฆฌํ๋ผ
- Udemy, Coursera์ ๋ ์ผ์ฆ ๊ฐ์๋ค
์ฑ
:
- "Agile Web Development with Rails" - ๋ ์ผ์ฆ ๋ฐ์ด๋ธ
- "The Rails Way" - ์ฌํ ํ์ต์ฉ
- "Eloquent Ruby" - ๋ฃจ๋น ์คํ์ผ ๊ฐ์ด๋
์ปค๋ฎค๋ํฐ:
- Ruby Korea ์ปค๋ฎค๋ํฐ
- Stack Overflow
- Reddit r/rails
- Discord ๋ ์ผ์ฆ ์๋ฒ๋ค
๐ ๏ธ ์ค์ ํ๋ก์ ํธ ์์ด๋์ด
ํ์ตํ ๋ด์ฉ์ ์ค์ ๋ก ์ ์ฉํด๋ณด๋ ๊ฒ ๊ฐ์ฅ ์ค์ํด. ๋ค์ ํ๋ก์ ํธ๋ค์ ๋ง๋ค์ด๋ณด๋ฉด์ ์ค๋ ฅ์ ํค์๋ณด์:
| ๋์ด๋ | ํ๋ก์ ํธ | ๋ฐฐ์ธ ์ ์๋ ๊ฒ |
|---|---|---|
| ์ด๊ธ | ํ ์ผ ๊ด๋ฆฌ ์ฑ | CRUD, ์ธ์ฆ, ๊ฐ๋จํ ๊ด๊ณ |
| ์ด๊ธ | ๊ฐ์ธ ๋ธ๋ก๊ทธ | ํฌ์คํธ ๊ด๋ฆฌ, ๋๊ธ, ๋งํฌ๋ค์ด |
| ์ค๊ธ | ๋ถ๋งํฌ ๊ณต์ ์๋น์ค | ํ๊ทธ, ๊ฒ์, ์์ ๊ธฐ๋ฅ |
| ์ค๊ธ | ๊ฐ๋จํ ์ ์์๊ฑฐ๋ | ์ฅ๋ฐ๊ตฌ๋, ๊ฒฐ์ , ์ฃผ๋ฌธ ๊ด๋ฆฌ |
| ๊ณ ๊ธ | ํ๋ก์ ํธ ๊ด๋ฆฌ ๋๊ตฌ | ์ค์๊ฐ ํ์ , WebSocket, ๋ณต์กํ ๊ถํ |
| ๊ณ ๊ธ | ์์ ๋คํธ์ํฌ | ํ๋ก์ฐ ์์คํ , ํผ๋, ์๋ฆผ, ๋ฉ์์ง |
โ ๊น๋ํ ์ฝ๋์ ์ฃผ์
โ ํ ์คํธ ์์ฑ
โ README ๋ฌธ์ํ
โ ์ค์ ๋ฐฐํฌ (Heroku, Render, Railway ๋ฑ)
โ GitHub์ ์ฝ๋ ๊ณต๊ฐ
์ด๋ฐ ํฌํธํด๋ฆฌ์ค๊ฐ ์์ผ๋ฉด ์ทจ์ ํ ๋ ํฐ ๋์์ด ๋ผ! ๊ทธ๋ฆฌ๊ณ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๋ ์ผ์ฆ ๊ฐ๋ฐ ์๋น์ค๋ฅผ ์ ๊ณตํ๋ฉด์ ์ค์ ๊ฒฝํ์ ์๊ณ ์์ต๋ ์ฐฝ์ถํ ์ ์์ด! ๐ฐ
๐ฏ ๋ฒ ์คํธ ํ๋ํฐ์ค
ํ๋กํ์ ๋ํ ๋ ์ผ์ฆ ๊ฐ๋ฐ์๊ฐ ๋๋ ค๋ฉด ๋ค์ ์์น๋ค์ ๋ฐ๋ผ์ผ ํด:
# 1. Fat Model, Skinny Controller
# โ ๋์ ์: ์ปจํธ๋กค๋ฌ์ ๋น์ฆ๋์ค ๋ก์ง
class OrdersController < ApplicationController
def create
@order = Order.new(order_params)
@order.total = @order.items.sum(&:price)
@order.tax = @order.total * 0.1
@order.shipping = calculate_shipping(@order)
# ... ๋ ๋ง์ ๋ก์ง
end
end
# โ
์ข์ ์: ๋ชจ๋ธ์ ๋น์ฆ๋์ค ๋ก์ง
class Order < ApplicationRecord
before_save :calculate_totals
def calculate_totals
self.total = items.sum(&:price)
self.tax = total * TAX_RATE
self.shipping = ShippingCalculator.calculate(self)
end
end
class OrdersController < ApplicationController
def create
@order = Order.new(order_params)
@order.save
end
end
# 2. Service Objects๋ก ๋ณต์กํ ๋ก์ง ๋ถ๋ฆฌ
class OrderProcessor
def initialize(order)
@order = order
end
def process
ActiveRecord::Base.transaction do
charge_payment
send_confirmation_email
update_inventory
create_shipment
end
end
private
def charge_payment
# ๊ฒฐ์ ์ฒ๋ฆฌ
end
# ...
end
# 3. Concerns๋ก ๊ณตํต ๊ธฐ๋ฅ ์ถ์ถ
module Publishable
extend ActiveSupport::Concern
included do
scope :published, -> { where(published: true) }
scope :draft, -> { where(published: false) }
end
def publish!
update(published: true, published_at: Time.current)
end
end
class Post < ApplicationRecord
include Publishable
end
# 4. N+1 ์ฟผ๋ฆฌ ๋ฐฉ์ง
# โ ๋์ ์
@posts = Post.all
@posts.each do |post|
puts post.user.name # ๊ฐ ํฌ์คํธ๋ง๋ค ์ฟผ๋ฆฌ ๋ฐ์!
end
# โ
์ข์ ์
@posts = Post.includes(:user).all
@posts.each do |post|
puts post.user.name # ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ๋ชจ๋ ์ฌ์ฉ์ ๋ก๋
end
# 5. Strong Parameters ์ฌ์ฉ
def post_params
params.require(:post).permit(:title, :content, :published)
end
# 6. ํ๊ฒฝ๋ณ ์ค์ ๋ถ๋ฆฌ
# config/environments/production.rb
config.log_level = :info
config.cache_store = :redis_cache_store
# config/environments/development.rb
config.log_level = :debug
config.cache_store = :memory_store
๐ฌ ๋ง๋ฌด๋ฆฌ: ๋ ์ผ์ฆ๋ก ์์ํ๋ ๊ฐ๋ฐ์์ ์ฌ์
์, ์ฌ๊ธฐ๊น์ง ๋ฃจ๋น ์จ ๋ ์ผ์ฆ์ ํ์คํ ๊ฐ๋ฐ๊ณผ Convention over Configuration ์ฒ ํ์ ๋ํด ๊น์ด ์๊ฒ ์์๋ดค์ด! ์ ๋ง ๊ธด ์ฌ์ ์ด์์ง? ๐
๋ ์ผ์ฆ๋ ๋จ์ํ ์น ํ๋ ์์ํฌ๊ฐ ์๋์ผ. ์ด๊ฑด ๊ฐ๋ฐ์์ ํ๋ณต๊ณผ ์์ฐ์ฑ์ ์ต์ฐ์ ์ผ๋ก ํ๋ ์ฒ ํ์ด๊ณ , 20๋
๊ฐ๊น์ด ๊ฒ์ฆ๋ ๊ฐ๋ฐ ๋ฐฉ๋ฒ๋ก ์ด์ผ. Convention over Configuration ๋๋ถ์ ์ค์ ์ ์๊ฐ์ ๋ญ๋นํ์ง ์๊ณ ์ค์ ๋ฌธ์ ํด๊ฒฐ์ ์ง์คํ ์ ์์ง. ๐ฏ
๋ฌผ๋ก ๋ ์ผ์ฆ๊ฐ ๋ชจ๋ ํ๋ก์ ํธ์ ์๋ฒฝํ ์ ํ์ ์๋์ผ. ํ์ง๋ง ์น ์ ํ๋ฆฌ์ผ์ด์
, ํนํ ๋น ๋ฅด๊ฒ MVP๋ฅผ ๋ง๋ค์ด์ผ ํ๋ ์คํํธ์
์ด๋ ์ค์๊ท๋ชจ ํ๋ก์ ํธ์์๋ ์ ๋ง ๊ฐ๋ ฅํ ๋๊ตฌ์ผ. GitHub, Shopify, Airbnb ๊ฐ์ ๋๊ท๋ชจ ์๋น์ค๋ค์ด ๋ ์ผ์ฆ๋ก ์์ํ๋ค๋ ์ฌ์ค์ด ์ด๋ฅผ ์ฆ๋ช
ํ์ง! ๐
์ค์ ๋ณด๋ค ๊ด๋ก๋ฅผ ๋ฐ๋ผ ๊ฐ๋ฐ ์๋๋ฅผ ๊ทน๋ํ
2. ํ์คํ ํ๋ ์์ํฌ
ํ๋ก ํธ์๋๋ถํฐ ๋ฐฑ์๋, ๋ฐ์ดํฐ๋ฒ ์ด์ค๊น์ง ๋ชจ๋ ๊ฒ์ ์ฒ๋ฆฌ
3. ๊ฐ๋ ฅํ ์ํ๊ณ
์๋ง์ Gem๊ณผ ํ๋ฐํ ์ปค๋ฎค๋ํฐ
4. ๊ฐ๋ฐ์ ๊ฒฝํ
์ฝ๊ธฐ ์ฝ๊ณ ์ง๊ด์ ์ธ ์ฝ๋, ์ฆ๊ฑฐ์ด ๊ฐ๋ฐ ๊ฒฝํ
5. ์ง์์ ์ธ ์งํ
Hotwire, Import Maps ๋ฑ ์ต์ ํธ๋ ๋ ์ ๊ทน ์์ฉ
6. ๊ฒ์ฆ๋ ์์ ์ฑ
20๋ ๊ฐ๊น์ด ์ญ์ฌ์ ์๋ง์ ์ฑ๊ณต ์ฌ๋ก
๋ ์ผ์ฆ๋ฅผ ๋ฐฐ์ฐ๋ ๊ฑด ๋จ์ํ ํ๋์ ํ๋ ์์ํฌ๋ฅผ ์ตํ๋ ๊ฒ ์ด์์ด์ผ. ์ข์ ์ํํธ์จ์ด ์ค๊ณ ์์น, ํ
์คํธ ์ฃผ๋ ๊ฐ๋ฐ, RESTful ์ํคํ
์ฒ ๊ฐ์ ๋ณดํธ์ ์ธ ๊ฐ๋ฐ ๊ฐ๋
๋ค์ ๋ฐฐ์ฐ๊ฒ ๋ผ. ์ด๋ฐ ์ง์์ ๋ค๋ฅธ ์ธ์ด๋ ํ๋ ์์ํฌ๋ฅผ ๋ฐฐ์ธ ๋๋ ํฐ ๋์์ด ๋ ๊ฑฐ์ผ! ๐
์ง๊ธ ๋ฐ๋ก ์์ํด๋ณด๋ ๊ฑด ์ด๋? ํฐ๋ฏธ๋์ ์ด๊ณ rails new my_awesome_app์ ์
๋ ฅํด๋ด. ๊ทธ๋ฆฌ๊ณ ์ฒซ ๋ฒ์งธ "Hello, World!"๋ฅผ ํ๋ฉด์ ๋์๋ณด์. ๊ทธ๊ฒ ๋ฐ๋ก ๋ ์ผ์ฆ ๊ฐ๋ฐ์๋ก์์ ์ฒซ ๊ฑธ์์ด ๋ ๊ฑฐ์ผ! ๐จโ๐ป๐ฉโ๐ป
ํ๋ก๊ทธ๋๋ฐ์ ๋ฐฐ์ฐ๋ฉด์ ๋งํ๋ ๋ถ๋ถ์ด ์๋ค๋ฉด ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํ๋ ๊ฒ๋ ์ข๊ณ , ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๋ฉํ ๋ฅผ ์ฐพ์ 1:1๋ก ๋ฐฐ์ฐ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ. ๋ํ ์ด๋ ์ ๋ ์ค๋ ฅ์ด ์์ด๋ฉด ์ง์ ์ฌ๋ฅ๋ท์์ ๋ ์ผ์ฆ ๊ฐ๋ฐ ์๋น์ค๋ฅผ ์ ๊ณตํ๋ฉด์ ๋ค๋ฅธ ์ฌ๋๋ค์ ๋์์ค ์๋ ์์ด! ๐ค
๋ ์ผ์ฆ๋ ์ฌ์ ํ ์ด์์๊ณ , ์งํํ๊ณ ์๊ณ , ๋ง์ ๊ฐ๋ฐ์๋ค์๊ฒ ์ฌ๋๋ฐ๊ณ ์์ด. ์ด ๋ฉ์ง ํ๋ ์์ํฌ์ ํจ๊ป ๋น์ ์ ์์ด๋์ด๋ฅผ ํ์ค๋ก ๋ง๋ค์ด๋ณด์! ํ๋ณตํ ์ฝ๋ฉ ๋๊ธธ ๋ฐ๋ผ! ๐โจ
2. ๊ฐ๋จํ CRUD ์ฑ ๋ง๋ค์ด๋ณด๊ธฐ
3. GitHub์ ์ฝ๋ ์ฌ๋ฆฌ๊ณ ๋ฐฐํฌํด๋ณด๊ธฐ
4. ๋ ์ผ์ฆ ์ปค๋ฎค๋ํฐ ์ฐธ์ฌํ๊ธฐ
5. ๋งค์ผ ์กฐ๊ธ์ฉ ์ฝ๋ฉํ๊ธฐ
Remember: "The best time to start was yesterday. The next best time is now!" ๐ช
Happy Coding with Ruby on Rails! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.