Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 158 additions & 78 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
![](./cover.png)

## Demo
아래 QR 코드를 카메라로 찍어보세요. 당근마켓 앱 내에서 데모를 볼 수 있어요.
아래 QR 코드를 카메라로 찍어보세요. 당근마켓 앱 내에서 데모를 볼 수 있어요. 아래 데모에서는 각 플랫폼 (Android/iOS) 환경에 맞게 UI가 표현됩니다.

![](./demo-qr.png)

Expand All @@ -15,94 +15,150 @@ $ yarn add @daangn/karrotframe
import * as kf from '@daangn/karrotframe'
```

## 네비게이터
네비게이터는 화면간 전환 효과와 History를 관리합니다. 네비게이터의 핵심 로직은 `react-router-dom`과 `recoil`에 의존하고 있습니다.
## 1. 네비게이터
네비게이터는 아래의 기능을 지원합니다.

- 자연스러운 화면전환
- History 지원
- 네비게이션 바
- 이전, 닫기 버튼
- 각 플랫폼에 맞게 디자인된 자연스러운 화면전환
- 각 플랫폼에 맞게 디자인된 네비게이션 바
- 상황에 맞는 이전, 닫기 버튼

> 네비게이터의 핵심 로직은 `react-router-dom`과 `recoil`에 의존하고 있습니다.

### 1-a. `Navigator`
`Navigator` 컴포넌트는 화면을 표현하는데 반드시 필요한 요소들이 포함됩니다. 컴포넌트 트리 상단에 포함해주세요

```tsx
import { Navigator } from '@daangn/karrotframe'

const App: React.FC = () => {
return (
<Navigator
theme='Cupertino'
onClose={() => {
console.log('닫기버튼이 눌렸습니다')
}}
>
{/*...*/}
</Navigator>
)
}
```

| Props | 타입 | 역할 | 기본값 |
| ------------- | ------------- | ------------- | ------------- |
| `theme` | `Cupertino`, `Android`, `Web` | UI 테마 | `Web` |
| `animationDuration` | number | 애니메이션 지속시간 | 테마별로 다름 |
| `useCustomRecoilRoot` | boolean | `true`인 경우 `Navigator` 내에 포함된 `<RecoilRoot/>` 을 제거합니다 | `false` |
| `useCustomRouter` | boolean | `true`인 경우 `Navigator` 내에 포함된 `<HashRouter/>` 을 제거합니다 | `false` |

### 1-b. `Screen`
`Screen` 컴포넌트는 화면을 선언하는데 사용합니다. `Navigator` 안에 선언합니다.

### 시작하기
네비게이터의 핵심 컴포넌트로는 `Navigator`와 `Screen`이 존재합니다. `Navigator`의 `environment` props로 iOS(`Cupertino`), Android, Web 환경의 UI/애니메이션을 다르게 설정할 수 있습니다.
```tsx
import { Navigator, Screen } from '@daangn/karrotframe'

const App: React.FC = () => {
return (
<Navigator
environment='Cupertino'
theme='Cupertino'
onClose={() => {
window.alert('닫기')
console.log('닫기버튼이 눌렸습니다')
}}
>
<Screen path='/' component={MyComponent} />
{/* 또는 */}
<Screen path='/'>
<Home />
</Screen>
<Screen path='/posts'>
<PostList />
</Screen>
<Screen path='/posts/:post_id'>
<Post />
<MyComponent />
</Screen>
</Navigator>
)
}
```
| Props | 타입 | 역할 | 기본값 |
| ------------- | ------------- | ------------- | ------------- |
| `path` | string | 해당 화면을 표현할 Path | required |
| `component` | `React.ComponentType` | 렌더링 할 컴포넌트 | |
| `children` | `React.ReactNode` | 렌더링 할 요소 | |
> `component` 또는 `children`은 반드시 사용하세요 (만약 두 props가 동시에 선언되는 경우, `component`가 우선권을 갖습니다)

### `ScreenHelmet`
기본적으로 Screen은 상단 네비게이션 바를 포함하고 있지 않습니다. 기본 제공되는 상단 네비게이션 바를 수정하기 위해서는 `ScreenHelmet` 컴포넌트를 사용하세요.
### 1-c. `ScreenHelmet`
기본적으로 Screen은 상단 네비게이션 바를 포함하고 있지 않습니다. 기본 제공되는 상단 네비게이션 바를 추가, 수정하기 위해서는 `ScreenHelmet` 컴포넌트를 사용하세요.

```tsx
import { ScreenHelmet } from '@daangn/karrotframe'

const Home: React.FC = () => {
const MyComponent: React.FC = () => {
return (
<div>
<ScreenHelmet
title='홈'
title='당근알바'
appendLeft={
<div>왼쪽에추가</div>
}
appendRight={
<div>오른쪽에추가</div>
}
customBackButton={
<div>이전</div>
}
customCloseButton={
<div>닫기</div>
}
/>
</div>
)
}
```
| Props | 타입 | 역할 | 기본값 |
| ------------- | ------------- | ------------- | ------------- |
| `title` | `React.ReactNode` | 타이틀 부분에 출력할 요소 | `undefined` |
| `appendLeft` | `React.ReactNode` | 왼쪽에 요소를 추가 (이전 버튼 오른쪽에 표시됩니다) | `undefined` |
| `appendRight` | `React.ReactNode` | 오른쪽에 요소를 추가 (닫기 버튼 왼쪽에 표시됩니다) | `undefined` |
| `customBackButton` | `React.ReactNode` | 이전 버튼을 사용자화합니다 | `undefined` |
| `customCloseButton` | `React.ReactNode` | 닫기 버튼을 사용자화합니다 | `undefined` |

다음과 같이 좌측, 우측에 Element를 추가하고, 가운데 타이틀을 덮어씌울수 있습니다.
### 1-d. `Link`
특정 path로 이동할 수 있는 링크를 생성하는 컴포넌트입니다.

```tsx
import { ScreenHelmet } from '@daangn/karrotframe'
import { Link } from '@daangn/karrotframe'

const Home: React.FC = () => {
return (
<div>
<ScreenHelmet
title='홈'
left={
<div>left</div>
}
right={
<div>right</div>
}
center={
<div style={{ color: 'red' }}>홈</div>
}
/>
<Link to='/posts'>글 목록</Link>
</div>
)
}
```
| Props | 타입 | 역할 | 기본값 |
| ------------- | ------------- | ------------- | ------------- |
| `to` | string | 이동 할 path | required |
| `replace` | boolean | path 이동을 replace로 처리할 지 여부 | `undefined` |
| `className` | string | className | `undefined` |

### URL 파라미터 받기
`useLocation`, `useParams`, `useRouteMatch`를 활용할 수 있습니다
### 1-e. `useLocation`, `useParams`, `useRouteMatch`
react-router-dom에 존재하는 `useLocation`, `useParams`, `useRouteMatch`를 그대로 사용할 수 있습니다

```tsx
import { useLocation, useParams, useRouteMatch } from '@daangn/karrotframe'

const Post: React.FC = () => {
/**
* 현재 location 정보
*/
const location = useLocation()

/**
* path parameter로 들어온 값
*/
const params = useParams()

/**
* 현재 위치와 특정 path regex를 비교해 파싱된 값을 반환합니다.
* (매치하지 않는다면 null 반환)
*/
const match = useRouteMatch({
path: '/:post_id',
})
Expand All @@ -111,88 +167,112 @@ const Post: React.FC = () => {
}
```

### 화면 전환
화면 전환은 `Link` 또는 `useNavigator` 를 통해 수행할 수 있습니다.
### 1-f. `useNavigator`
화면 전환을 수행합니다.

```tsx
import { Link } from '@daangn/karrotframe'
import { useNavigator } from '@daangn/karrotframe'

const Home: React.FC = () => {
return (
<div>
<Link to='/posts'>글 목록</Link>
</div>
)
}
```
const Posts: React.FC = () => {
const { push, pop, replace } = useNavigator()

또는
const goPost = (postId: string) => () => {
// 특정 path로 이동합니다
push(`/posts/${postId}`)
}

```tsx
import { useNavigator } from '@daangn/karrotframe'
const goBack = () => {
// 한단계 뒤로 갑니다
pop()

// depth argument를 통해 여러단계를 pop 할 수 있습니다
pop(1)
}

useEffect(() => {
if (!user) {
// 특정 path로 이동합니다 (replace)
// 애니메이션 없이 이동하므로, redirect behavior에 적절합니다
replace('/login')
}
})

const Home: React.FC = () => {
const { push } = useNavigator()
return (
<div>
<button
onClick={() => {
push('/posts')
}}
>
글 목록
</Link>
{posts.map((post) => {
return (
<div>
{/* ... */}
<button onClick={goPost(post.id)}>
글 상세보기
</button>
</div>
)
})}
{/* ... */}
<button onClick={goBack}>뒤로가기</button>
</div>
)
}
```

### 화면 간 데이터 전송
`useNavigator`의 `pop()`과 `await push()`를 통해 화면간 데이터 전송을 할 수 있습니다.
추가적으로, `useNavigator`의 `pop().send()`과 `await push()`를 통해 화면간 데이터 전송을 할 수 있습니다.

`pop()` 함수 내 `depth` argument를 2 이상으로 부여할 시 여러 화면을 뛰어넘어서 전송도 가능합니다.
> `pop()` 함수 내 `depth` argument를 2 이상으로 부여할 시 여러 화면을 뛰어넘어서 전송도 가능합니다.

```tsx
import { useNavigator } from '@daangn/karrotframe'

const Home: React.FC = () => {
const Posts: React.FC = () => {
const { push } = useNavigator()

const writePost = () => {
// 다음 화면에서 전송할 데이터를 기다립니다
const data = await push('/posts/write')
console.log(data)
// {
// hello: 'world',
// }
}
return (
<div>
<button
onClick={async () => {
const data = await push('/form')

console.log(data)
// {
// title: '안녕하세요',
// content: '데이터 전송합니다.'
// }
}}
onClick={writePost}
>
글 작성하기
</Link>
</div>
)
}

const Form: React.FC = () => {
const PostWriteForm: React.FC = () => {
const { pop } = useNavigator()

const onSubmit = () => {
pop(1, {
title: '안녕하세요',
content: '데이터 전송합니다.'
// 이전 화면으로 데이터 전송
pop().send({
hello: 'world',
})

// 아래와 같이 여러 단계를 뛰어넘어 데이터 전송도 가능합니다
pop(3).send({
hello: 'world',
})
}

return /* ... */
}
```

## 2. 추가 예정인 기능
- [ ] 하프뷰
- [ ] 모달
- [ ] 토스트
> 추가적으로 Karrotframe에 추가되었으면하는 기능이 있으시다면, 꼭 이슈에 남겨주세요.

## Contributor
- Bucky (bucky@daangn.com)
- Tony (tony@daangn.com)

## Help
슬랙의 #dev-frontend로 언제든지 편하게 문의해주세요!
당근마켓 Slack 내 `#dev-frontend`로 언제든지 편하게 문의해주세요!
3 changes: 1 addition & 2 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ const bridge = new Bridge()
function App() {
return (
<Navigator
environment={'Cupertino'}
animationDuration={350}
theme={bridge.environment}
onClose={() => {
bridge.router.close()
}}>
Expand Down
9 changes: 5 additions & 4 deletions example/src/components/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import styled from '@emotion/styled'
const Home: React.FC = () => {
const navigator = useNavigator()

const onPage2Click = () => {
navigator.push('/page2')
const onPage2Click = async () => {
const data = await navigator.push('/page2')
window.alert(JSON.stringify(data))
}

return (
<Container>
<ScreenHelmet
title='당근알바'
left={
appendLeft={
<HamburgerIcon>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="3.8" y1="5.2" x2="20.2" y2="5.2" stroke="#212529" strokeWidth="1.6" strokeLinecap="round"/>
Expand All @@ -22,7 +23,7 @@ const Home: React.FC = () => {
</svg>
</HamburgerIcon>
}
right={
appendRight={
<SearchIcon>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M10.4992 2.19995C5.91526 2.19995 2.19922 5.91599 2.19922 10.5C2.19922 15.0839 5.91526 18.8 10.4992 18.8C12.5031 18.8 14.3411 18.0898 15.7754 16.9075L20.4335 21.5656C20.746 21.8781 21.2525 21.8781 21.5649 21.5656C21.8773 21.2532 21.8773 20.7467 21.5649 20.4343L16.9067 15.7761C18.0891 14.3419 18.7992 12.5038 18.7992 10.5C18.7992 5.91599 15.0832 2.19995 10.4992 2.19995ZM3.79922 10.5C3.79922 6.79964 6.79891 3.79995 10.4992 3.79995C14.1995 3.79995 17.1992 6.79964 17.1992 10.5C17.1992 14.2003 14.1995 17.2 10.4992 17.2C6.79891 17.2 3.79922 14.2003 3.79922 10.5Z" fill="#212529"/>
Expand Down
4 changes: 3 additions & 1 deletion example/src/components/Page2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ const Page2: React.FC = () => {
const navigator = useNavigator()

const onPopClick = () => {
navigator.pop(1)
navigator.pop().send({
hello: 'world',
})
}
const onPage3Click = () => {
navigator.push('/page3')
Expand Down
Loading