vite react-ts에서 emotion css prop 쓰기

vite.config.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react({
jsxImportSource: '@emotion/react',
babel: {
plugins: ['@emotion/babel-plugin'],
},
}),
],
});
tsconfig.json
1
2
3
4
5
6
{
"compilerOptions": {
// ...
"jsxImportSource": "@emotion/react"
}
}

참고

cra + eslint(airbnb) extensions, no-unresolved 오류 해결

terminal
1
2
3
yarn create react-app . --template typescript
yarn eslint --init
yarn add -D eslint-config-airbnb@latest
  • cra + eslint(airbnb)
  • 이렇게 리액트 프로젝트를 시작했을 때,

import/extensions
import/no-unresolved
react/jsx-filename-extension

  • 이런 오류가 나온다
  • 오류가 뜨지 않도록 eslintrc를 설정해주자

eslintrc

eslintrc.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
// ...
"rules": {
"react/jsx-filename-extension": [
"error",
{ "extensions": [".ts", ".tsx"] }
],
"import/extensions": [
"error",
"ignorePackages",
{
"ts": "never",
"tsx": "never"
}
]
},
"settings": {
"import/resolver": {
"node": {
"extensions": [".ts", ".tsx", ".js", ".jsx"]
}
}
}
}
  • 위처럼 설정해 주었다

참고

emotion css prop

terminal
1
2
yarn add @emotion/css @emotion/react @emotion/babel-preset-css-prop
yarn add -D react-app-rewired customize-cra
tsconfig.json
1
2
3
4
5
6
{
"compilerOptions": {
// ...
"jsxImportSource": "@emotion/react"
}
}
  • jsx 엘리먼트에서 css 속성을 사용할 수 있다

참고

eslint(react-hooks/exhaustive-deps) autofix

  • eslint(react-hooks/exhaustive-deps)
  • useEffect, useCallback 등 종속성을 가지는 훅에서 누락된 종속성을 알려준다

auto fix

  • 이전 버전에서는 autofix를 지원했는데,
  • 최신 버전은 지원하지 않는다
  • 나는 이전 버전을 사용해서 자동으로 종속성 업데이트해주는 것에 익숙하고 좋았었다
  • 그래서 autofix 활성화하는 법을 찾아보았다
  • auto fix를 활성화하려면 eslint config에 설정을 추가해준다
.eslintrc
1
2
3
4
5
6
7
8
9
10
11
12
{
// ...
"rules": {
// ...
"react-hooks/exhaustive-deps": [
"warn",
{
"enableDangerousAutofixThisMayCauseInfiniteLoops": true
}
]
}
}
  • enableDangerousAutofixThisMayCauseInfiniteLoops를 활성화한다

참고

리액트 SPA 쉽게 공유해보기

내가 만든 리액트 앱을 친구들에게 공유하기 위한 간단한 방법을 소개한다
방법은 2가지가 있다

  • nodejs express
  • github pages

방법1. nodejs express

terminal
1
2
3
git clone https://github.com/chinsun9/serve-spa-expressjs.git
cd serve-spa-expressjs
npm i
  • 위 저장소를 클론하고, 종속성을 설치한다
  • index.js 에서 portstaticDir을 내 상황에 맞게 수정한다

port

index.js:5
1
const port = 5000; // 원하는 포트로 수정

staticDir (spa path)

index.js:7
1
const staticDir = path.join(__dirname, '../build'); // 빌드된 spa 경로, index.js를 기준으로 빌드된 리액트앱 상대경로를 path.join 두번째 인자에 넣어준다

소스코드

방법2. github pages

  • 깃허브 페이지를 통해 배포할 수 있다
  • 하지만 깃허브 페이지에서는 SPA를 지원하지 않는다
  • 1페이지짜리 리액트 앱이면 상관없지만,
  • react-router-dom을 사용하면서 url이 변화하는 경우
  • index.html과 404.html에 스크립트를 추가해줘야 한다
  • github pages에서 spa처럼 동작하게 해주는 마법 ; https://github.com/rafgraph/spa-github-pages#usage-instructions
  • demo page

gh-pages 설치

terminal
1
yarn add -D gh-pages

package.json 수정

package.json
1
2
3
4
5
6
7
8
9
{
// ...
"scripts": {
// ...
"deploy": "gh-pages -d build"
},
// ...
"homepage": "https://chinsun9.github.io/hello-react/"
}
  • deploy 스크립트 추가, homepage 필드를 추가한다
  • 이때 homepage필드의 값은
  • https://{username}.github.io/{repo}/로 한다

Router에 basename 추가

index.js
1
2
3
4
5
6
7
8
ReactDOM.render(
<React.StrictMode>
<Router basename={process.env.PUBLIC_URL}>
<App />
</Router>
</React.StrictMode>,
document.getElementById('root')
);
  • 최상위 Router에 basename={process.env.PUBLIC_URL}을 추가한다

404.html 추가

index.html 수정

build && deploy

terminal
1
2
yarn build
yarn deploy
  • gh-pages 브랜치가 생성되어 있고,
  • 깃허브 페이지 기능이 활성화되어있는 것을 확인할 수 있을 것이다

소스코드

참고

slash converter

  • 파일 경로 문자열 변환기를 만들었다
  • 파일 경로를 적어야 할 때가 생각보다 많은데
  • 어떤 때는 /를 사용하고
  • 어떤 때는 \를 사용하고
  • 또 어떤 때는 \\를 사용한다
  • 일일이 수정해주기 귀찮을 때가 분명히 있었을 것이다

변환 코드

Main.tsx
1
2
3
const convertSlash = (type: string, origin: string) => {
return origin.replace(/[/\\]+/g, type);
};
  • 슬래시와 백슬래시를 찾아내서 정해진 타입으로 변환한다

그 외

  • emotion theme provider를 사용해 테마를 적용해보았다

참고

react ts context

  • react에서 상위에서 하위 컴포넌트로 어떤 값을 보내 줄 때 prop으로 넘겨주게 된다
  • 이게 1, 2단계 정도면 괜찮을 텐데, 더 깊어지면 엄청 불편해진다
  • 넘기고 넘기고 넘기고 반복되는 것을 prop drilling이라고 한다
  • prop drilling으로 만들 때 긍정적인 점은 그만큼 각 컴포넌트가 느슨하게 연결되도록 만들었다는 게 아닐까..?
  • 컴포넌트가 독립성을 가지게 해주는 것도 좋지만 재사용될지 모르는 것에 너무 힘을 쓰진 말자…
  • 전역적인 state를 사용하고 싶을 때 사용할 수 있겠다
  • 리액트를 처음 배울 때 무작정 튜토리얼을 따라 하다가 redux를 배웠고,
  • 나중에 context api 만으로 충분하다는 것을 알게 되었다
  • 일단 다른 라이브러리를 설치하지 않고 충분히 가능하다는 것에 주로 사용하게 되었다
  • 사용 흐름은 redux나 context api 유사하다
  • 전역 저장소(스토어)를 생성하고, 각 컴포넌트에서는 dispatch를 통해서 상태를 업데이트할 수 있다
  • 생성, 삭제가 있다고 하면 생성 액션, 삭제 액션이 있고 dispatch 할 때 어떤 액션을 할지 정해서 업데이트를 수행한다
  • 실제 업데이트는 리듀서에서 된다. 리듀서에는 액션의 구현부?가 있다

참고 소스 코드

GlobalContext.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/* eslint-disable no-case-declarations */
import React, { createContext, Dispatch, useContext, useReducer } from 'react';
import { Comment } from '../types';

type State = Map<number, Comment>;

const initialState: State = new Map<number, Comment>();

initialState.set(0, {
id: 0,
avatar: 'https://i.pravatar.cc/48',
username: 'string',
publishedTime: '1주 전',
content: 'hello world!',
likeCount: 999,
dislikeCount: 0,
});

type CommentAction = { type: 'ADD_COMMENT'; comment: Comment };

const CommentContext = createContext<State>(initialState);
const CommentDispatch = createContext<Dispatch<CommentAction>>(() => null);

let nextId = 0;

function reducer(state: State, action: CommentAction): State {
switch (action.type) {
case 'ADD_COMMENT':
const { comment } = action;
nextId += 1;
return new Map(state).set(nextId, { ...comment, id: nextId });

default:
throw new Error('Unhandled action');
}
}

export default function GlobalContextProvider({
children,
}: {
children: React.ReactNode;
}) {
const [state, dispatch] = useReducer(reducer, initialState);

return (
<CommentContext.Provider value={state}>
<CommentDispatch.Provider value={dispatch}>
{children}
</CommentDispatch.Provider>
</CommentContext.Provider>
);
}

export function useCommentState() {
return useContext(CommentContext);
}

export function useCommentDispatch() {
return useContext(CommentDispatch);
}
  • 컨텍스트를 만들고
index.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import GlobalContextProvider from './context/GlobalContext';

ReactDOM.render(
<React.StrictMode>
<GlobalContextProvider>
<App />
</GlobalContextProvider>
</React.StrictMode>,
document.getElementById('root')
);
  • index.tsx에서 App을 감싸든 어디든 같은 컨텍스트로 묶어줄 상위 컴포넌트를 감싸준다
App.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function App() {
const comment = useCommentState();

return (
<Main>
<Header />

<div className="contents">
{Array.from(comment).map(([id, data]) => {
return (
<div className="commentThread" key={id}>
<Comment data={data} />
</div>
);
})}
</div>
</Main>
);
}
  • 참조하여 사용할 때는 이렇게
Header.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Header() {
const commentDispatch = useCommentDispatch();

// ...

const onSubmitHandler = () => {
commentDispatch({
type: 'ADD_COMMENT',
comment,
});
};

// ...
}
  • 상태를 업데이트하고 싶으면 이런 식으로 할 수 있겠다

참고 소스 코드 저장소

참고

react table 연습

  • 표를 포함한 간단한 웹을 만들 때 부트스트랩 템플릿 안에 들어있는 datatables를 사용하곤 했었다
  • 데이터가 쌓이지 않는 상황에서,
  • 페이지네이션 및 필터링을 이미 제공하고 있기 때문에 내가 구현할 내용이 거의 없다는 게 장점이다

상황

  • 나는 주로 리액트를 사용하는데, 이 부트스트랩 템플릿(html, js, css)을 그대로 리액트로 가져왔다
  • 이 템플릿의 종속성인 vendor 디렉터리에 있는 내용들도 public으로 옮기고,
  • 컴포넌트에서는 동적으로 이 종속성을 스크립트 태그로 로드하고
  • 이 스크립트에서 사용하는 함수를 호출하기 위해서 window 객체에 담아서 실행하고…
  • 하지만 테이블 관련 동작을 커스텀하기도 어렵고,, 좋지 못한 구조로 가는 느낌이 들었다
  • 그래서 리액트에서 사용할 수 있는 테이블 라이브러리를 찾다가 react-table을 공부해보기로 했다

따라하기

  • Codevolution React Table Tutorial을 보고 따라했다

타입스크립트로 따라하기

type

  • yarn add @types/react-table
  • 타입스크립트로 진행하려면 필요하다

useTable()

1
2
3
4
const tableInstance = useTable({
columns, // ts-err
data,
});
1
Type '({ Header: string; Footer: string; accessor: string; columns?: undefined; } | { Header: string; Footer: string; columns: { Header: string; Footer: string; accessor: string; }[]; accessor?: undefined; })[]' is not assignable to type 'Column<{ ... }>[]'.
  • 위 에러 메시지가 나온다
1
2
3
4
5
const tableInstance = useTable({
// @ts-ignore
columns,
data,
});
  • 위처럼 주석을 추가한다
  • 다른 react-table typescript example을 보니까 // @ts-ignore을 사용하는 걸 참고 했다

자동완성 안됨

1
2
3
4
5
6
7
8
9
<thead>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((columns) => (
<th {...columns.getHeaderProps()}>{columns.render('Header')}</th>
))}
</tr>
))}
</thead>

레포

참고

리액트할 때 사용하는 스니펫

  • ES7 React/Redux/GraphQL/React-Native snippets 확장도구에서 제공하는 스니펫을 사용한다

리액트

1
2
3
4
5
6
7
8
// rfce 또는 rfc
import React from 'react';

function App() {
return <div></div>;
}

export default App;

리액트 네이티브

1
2
3
4
5
6
7
8
9
10
11
// rnf
import React from 'react';
import { View, Text } from 'react-native';

export default function App() {
return (
<View>
<Text></Text>
</View>
);
}

참고

반응속도 테스트 클론코딩

기능

  • 반응속도 기록
  • 랜덤 간격
  • 눈 덜 아프게
  • 결과에 따라 다른 메시지
  • 예측 클릭하면 횟수 -1

느낀점

  • 기존 테스트가 색이 확 반전돼서
  • 집중은 해야 하는데 눈이 너무 아팠다
  • 그래서 배경색은 그대로에 초록점이 생기도록 했다
  • 컨텍스트 쓸까 고민하다가 어차피 깊이도 안 깊어서 그냥 prop으로 넘겨줬다

개선방향

공유 버튼 만들기

  • 결과 이미지 생성해서 클립보드 복사해주기 또는 이미지 파일로 저장

구린 css 바꾸기

  • 색을 좀 이쁘게 써보자

레포