- 스타일 컴포넌트를 사용하지 않으면 스타일링을 하는 방법들
- 컴포넌트 안에 HTML의 속성으로 스타일을 직접 지정하는 방법
☹️ 태그형태로 추가할 경우, 기존의 CSS 스타일링 문법을 100% 활용할수 없다는 점이 있음
- 글로벌 CSS를 임포트 하는 방법
☹️ 글로벌 CSS에 정의한 모든 항목이 의도치 않게 적용될 수 있음
- CSS 모듈링을 이용하여 클래스명을 지정하는 방법 👍
- 😊 React에서 클래스명을 랜덤하게 지정해줘서 충돌을 예방함
☹️ className을 원하는 곳에 계속해서 복사 붙여넣기 해야함
- 컴포넌트 안에 HTML의 속성으로 스타일을 직접 지정하는 방법
# 기본 폴더에서 CRA를 통해서 React 프로젝트를 생성 후에, styled components를 인스톨
npx create-react-app .
npm install styled-components
npm start-
Styled Component를 사용하기 전에 직접 정의하는 방식을 먼저 사용해본 뒤에 비교분석을 시도해보자
function App() { return ( <div style={{ display: 'flex' }}> <div style={{ backgroundColor: 'teal', width: 100, height: 100 }}> <span style={{ color: 'white' }}>Hello, World!</span> </div> <div style={{ backgroundColor: 'tomato', width: 100, height: 100 }}></div> </div> ); } export default App;
- 상기 코드는 React에서 사용가능한 HTML태그에 직접 스타일을 지정하는 방식인데, 모든 엘리먼트가 div로 되어 있기 때문에, 어떤 스타일을 지정하는지 직접 style 코드를 보고 해석을 해야 알수가 있음
- 스타일 컴포넌트에서는 컴포넌트를 먼저 작성한 뒤에 스타일을 지정하므로, 컴포넌트의 이름을 커스터마이징 가능하고 코드를 읽는데 더 도움을 줄 수 있게 됨 (직관성이 증가)
-
Styled Component를 이용해서 리팩토링 (상기 코드와 결과는 동일)
import styled from 'styled-components'; const Container = styled.div` display: flex; `; const BoxOne = styled.div` background-color: teal; width: 100px; height: 100px; `; const BoxTwo = styled.div` background-color: tomato; width: 100px; height: 100px; `; const Text = styled.span` color: white; `; function App() { return ( <Container> <BoxOne> <Text>Hello, World</Text> </BoxOne> <BoxTwo /> </Container> ); } export default App;
- 스타일 컴포넌트는 정의된 엘리먼트와 스타일을 기반으로 컴포넌트를 만들고 할당함
- 각각의 컴포넌트가 사용되는 곳에서는 랜덤한 클래스명을 붙이고 정의된 스타일을 매치시킴
-
저번 섹션에서 BoxOne, BoxTwo의 CSS속성의 중복을 해결하기 위한 방법으로는 Adapting이 존재함
-
Adapting은 Prop을 이용해서 컴포넌트에 원하는 값을 전달하고, 해당 값을 style에서 이용하는 패턴을 이용하는것을 말함
import styled from 'styled-components'; const Container = styled.div` display: flex; `; const Box = styled.div` background-color: ${(props) => props.bgColor}; width: 100px; height: 100px; `; const Text = styled.span` color: white; `; function App() { return ( <Container> <Box bgColor='teal'> <Text>Hello, World</Text> </Box> <Box bgColor='tomato' /> </Container> ); } export default App;
-
다만, 이 방법또한 문제점이 있을 수 있는데, 다음 코드를 확인해보자
import styled from 'styled-components'; const Container = styled.div` display: flex; `; const Box = styled.div` background-color: ${(props) => props.bgColor}; width: 100px; height: 100px; `; const Circle = styled.div` background-color: ${(props) => props.bgColor}; width: 100px; height: 100px; border-radius: 50px; `; const Text = styled.span` color: white; `; function App() { return ( <Container> <Box bgColor='teal'> <Text>Hello, World</Text> </Box> <Circle bgColor='tomato' /> </Container> ); } export default App;
-
이 코드에서는 새로운 Circle을 만들면서 border-radius를 추가하고 있어서, 새로운 컴포넌트에 스타일이 대부분 중복되어 있다는 점이 문제가 됨
-
원인으로는 컴포넌트명이 변경되면 새롭게 스타일 코드를 작성해야 하기 때문인데, 이를 styled의 함수를 이용해서 해결 가능함 (Extending)
import styled from 'styled-components'; const Container = styled.div` display: flex; `; const Box = styled.div` background-color: ${(props) => props.bgColor}; width: 100px; height: 100px; `; const Circle = styled(Box)` border-radius: 50px; `; const Text = styled.span` color: white; `; function App() { return ( <Container> <Box bgColor='teal'> <Text>Hello, World</Text> </Box> <Circle bgColor='tomato' /> </Container> ); } export default App;
- Circle은 Box를 확장해서 border-radius만 추가하고 있음
- bgColor라는 Prop 데이터를 전달해도, 확장전 부모인 Box에서 이용가능했기 때문에, Circle에서도 이용 가능함
-
-
- 다수의 컴포넌트를 다룰 때 도움이 될 만한 트릭 중 하나
-
As
-
스타일 컴포넌트의 스타일 정의는 그대로 유지하면서 태그만 변경하고 싶을 경우 활용
-
As를 이용하면, 스타일은 유지하면서 태그만 변경할수 있음
import styled from 'styled-components'; const Container = styled.div` display: flex; `; const Btn = styled.button` color: white; background-color: tomato; border: 0; border-radius: 15px; `; const Link = styled(Btn)``; function App() { return ( <Container as='header'> <Btn>Login</Btn> <Btn as='a' href='/'> Login </Btn> </Container> ); } export default App;
-
-
attrs
-
스타일 컴포넌트에서 HTML속성값을 일률적으로 추가해줄때 활용
import styled from 'styled-components'; const Container = styled.div` display: flex; `; const Input = styled.input.attrs({ required: true, minLength: 10 })` background-color: tomato; `; function App() { return ( <Container> <Input /> <Input /> <Input /> <Input /> <Input /> </Container> ); } export default App;
- Input태그를 작성하면서 required와 minLength를 일괄적으로 설정해줄 수 있음
-
-
스타일 컴포넌트에서의 애니메이션 활용법은 클래스 안에 있는 keyframes를 그대로 활용하여 정의해주면 됨
import styled, { keyframes } from 'styled-components'; const Container = styled.div` display: flex; `; const animation = keyframes` 0% { transform: rotate(0deg); border-radius: 0px; } 50% { border-radius: 100px; } 100% { transform: rotate(360deg); border-radius: 0px; } `; const Box = styled.div` height: 200px; width: 200px; background-color: tomato; animation: ${animation} 3s linear infinite; `; function App() { return ( <Container> <Box /> </Container> ); } export default App;
-
스타일 컴포넌트를 활용할때 모든 엘리먼트를 컴포넌트화 해야만 스타일을 정의할 수 있는것은 아님
-
컴포넌트의 자식 엘리먼트가 컴포넌트가 아닐 경우, 부모 컴포넌트의 스타일 컴포넌트 정의에서 CSS 셀렉터를 이용해서 자식 엘리먼트를 타겟으로 지정하여 스타일을 정의할 수 있음
const Box = styled.div` height: 200px; width: 200px; background-color: tomato; animation: ${animation} 3s linear infinite; display: flex; justify-content: center; align-items: center; span { font-size: 32px; } `; function App() { return ( <Container> <Box> <span>😘</span> </Box> </Container> ); }
- 상기 코드에서 Box 컴포넌트의 자식 엘리먼트인 span을 Box 컴포넌트의 스타일 컴포넌트에서 바로 셀렉터로 지정해서 폰트사이즈를 변경하는것을 알 수 있음
const Emoji = styled.span` font-size: 32px; &:hover { font-size: 98px; } `; const Box = styled.div` height: 200px; width: 200px; background-color: tomato; animation: ${animation} 3s linear infinite; display: flex; justify-content: center; align-items: center; ${Emoji} { font-size: 32px; &:hover { font-size: 48px; } &:active { opacity: 0; } } `; function App() { return ( <Container> <Box> <Emoji>😘</Emoji> </Box> </Container> ); }
- 상기 코드에서 흥미로운 부분은 CSS 셀렉터를 이용할때 HTML태그가 아닌, 컴포넌트 변수를 이용해도 된다는 점
- 스타일 컴포넌트에서 테마는 다크모드를 구현할 수 있게 해줌
- 추가로 Local Estate Management라는 기능이 있는데 이는 추후 별도 섹션에서 배우자
- Theme은 색상코드를 모아놓은 오브젝트
-
추후에 색상배합을 변경할때 해당 오브젝트만 수정하면 됨
-
사용방법
import React from 'react'; import ReactDOM from 'react-dom'; import { ThemeProvider } from 'styled-components'; import App from './App'; const darkTheme = { textColor: 'whitesmoke', backgroundColor: '#111', }; const lightTheme = { textColor: '#111', backgroundColor: 'whitesmoke', }; ReactDOM.render( <React.StrictMode> <ThemeProvider theme={darkTheme}> <App /> </ThemeProvider> </React.StrictMode>, document.getElementById('root'), );
- 먼저 index.js에서 정의한 App 컴포넌트를 ThemeProvider로 감싸주기
- ThemeProvider는 Prop으로 theme을 받는데, 해당 Prop에 색상 Props를 정의해주기
import styled from 'styled-components'; const Container = styled.div` width: 100vw; height: 100vh; display: flex; justify-content: center; align-items: center; background-color: ${(props) => props.theme.backgroundColor}; `; const Title = styled.h1` color: ${(props) => props.theme.textColor}; `; function App() { return ( <Container> <Title>Hello, World!</Title> </Container> ); } export default App;
- App.js에서 props를 통해 theme에 접근하고, theme 오브젝트의 색상코드를 그대로 활용 가능
-
- 타입스크립트는 자바스크립트를 기반으로 한 프로그래밍 언어
- 기반으로 했기 때문에 문법등이 전혀 다른것은 아니며, 거의 비슷하고 몇몇 부분만 추가되었을 뿐임
- 타입스크립트는 strongly-typed한 언어이며 strongly-typed? 프로그래밍 언어가 동작하기 전에 타입을 확인함 (컴파일)
// JavaScript 방식
const plus = (a, b) => a + b;
plus(2, 2) // 4
plus(2, 'hi') // '2hi'
// 개발자의 의도가 숫자를 더하는 함수를 만들고 싶었다고 하더라도,
// JavaScript는 타입체크를 하지 않기 때문에 두번째 결과가 발생할 수 있음// 두번째 예제
const user = {
firstName: 'Angela',
lastName: 'Davis',
role: 'Professor',
}
console.log(user.name) // undefined
// name이 존재하지 않기 때문에 JavaScript는 undefined를 출력하지만,
// 개발자에게는 정의되지 않은 오브젝트 내의 속성을 호출할경우 에러를 출력해주는것이 도움이 됨-
런타임에서 에러가 발견되는것은 때에 따라서 치명적일 수 있음
-
타입스크립트는 그러한 에러를 프로그램이 가동되기 전에 미리 알려주는 기능을 탑재하고 있음
-
TypeScript Playground
TS Playground - An online editor for exploring TypeScript and JavaScript
💡 매개변수에 타입을 미리 정의함으로써 코드상에서의 잘못을 지적할 수 있음// TypeScript 방식 const plus = (a:number, b:number) => a + b; plus(2, 2) // 4 plus(2, 'hi') // error
- CRA에 TypeScript를 추가하는 방법으로는 앱을 생성하는 시점에서 타입스크립트의 형태로 만들기
- 기존 앱상에서 타입스크립트를 수동으로 설치해서 변환하는 방법
Adding TypeScript | Create React App
# CRA로 앱 만들때 템플릿을 타입스크립트의 형태로 만들기
npx create-react-app my-app --template typescript
# 기존 프로젝트에 타입스크립트 패키지 설치하기
npm install --save typescript @types/node @types/react @types/react-dom @types/jest- 기존 프로젝트를 변경할 경우 타입스크립트를 인스톨한 뒤에 파일명 변경
- App.js → App.tsx
- index.js → index.tsx
- 변경 후에 JavaScript 라이브러리등에서 에러가 발생할 수 있음
-
예를 들어, styled-components 등에서 에러가 발생하면 해결방법으로는 타입스크립트에 정의문을 추가해줄 필요가 있음
npm install -D @types/styled-components
-
스타일 컴포넌트와 같이 JavaScript 전용 라이브러리의 경우에는 오픈소스 형태로 개발자들이 소스를 분석해서 타입스크립트의 정의문을 제공하는데, 이것이 @types 라이브러리에 있는 styled-components임
-
- 개발자들이 모여서 만든 타입정의 커뮤니티를 DefinitelyTyped 라고 부르고 npm의 @types를 찾아보자
- 컴포넌트에 타입을 지정하는 방법
- JavaScript에서 PropTypes를 통해 React의 Props들의 타입을 확인하는 기능을 추가할 수 있지만, PropTypes는 JavaScript 라이브러리이므로, 런타임에서 확인후 브라우저 콘솔에 출력하는 정도에 그쳤음
- TypeScript를 사용하면, 코드를 작성하는 시점에서 확인후 에러를 표시하므로, 오류가 있는 코드를 릴리즈 하지 않도록 도와줌
- Interface
-
인터페이스란 객체의 생김새를 TypeScript에서 설명해주는 개념
-
컴포넌트의 Props를 사용할때 Props Object를 이용하게 되는데, 그때 Prop들마다 타입을 지정해줄수도 있지만, interface를 이용해서 객체 형태를 먼저 정의해주고 해당 interface를 사용할 수 있음
💡 스타일 컴포넌트에서 props를 정의할때 형태로 사용하는데, 이때 Interface를 이용해서 깔끔하게 코딩 가능interface ContainerProps { bgColor: string; } const Container = styled.div<ContainerProps>` width: 200px; height: 200px; background-color: ${(props) => props.bgColor}; border-radius: 100px; `; interface CircleProps { bgColor: string; } const Circle = ({ bgColor }: CircleProps) => { return <Container bgColor={bgColor} />; };
-
- Typing the Props 섹션에서 작성한 샘플코드의 경우에 Interface를 이용하여 Props를 명시했는데, 이렇게 되면 기본적으로 required 상태가 됨
- Props를 선택적으로 사용하고 싶을 경우에는 어떻게 하면 될까?
- TypeScript에서는 선언형 매개변수, 변수의 선택자에 ?를 붙여서 정의하면 됨
interface ContainerProps {
bgColor: string;
borderColor: string;
}
const Container = styled.div<ContainerProps>`
width: 200px;
height: 200px;
background-color: ${(props) => props.bgColor};
border-radius: 100px;
border: 1px solid ${(props) => props.borderColor};
`;
interface CircleProps {
bgColor: string;
borderColor?: string;
}
const Circle = ({ bgColor, borderColor }: CircleProps) => {
return <Container bgColor={bgColor} borderColor={borderColor ?? bgColor} />;
};-
borderColor를 선택적으로 사용하고 싶을 경우 borderColor?: string으로 정의
-
JavaScript에서 A ?? B 연산자는 A가 null 또는 undefined일 경우에 B가 실행되는 연산자
-
상기 코드의 해석으로는 Container의 Props중 bgColor는 필수이며, borderColor는 선택적이며, 값이 Props Data로 입력될 경우에는 해당 값을 취하지만, 그렇지 않을 경우에는 bgColor와 동일한 색상으로 border를 작성하게 됨
- React의 함수형 hook에 의해서 사용할수 있는 State로는 useState()가 있음
- TypeScript와 함께 사용할 때 useState의 경우 기본값을 주게되면 기본값의 논리형에 근거해서 TypeScript가 추론한 논리형이 변수에 부여되고, setter또한 자동으로 형태가 지정됨
-
이는 합리적인 추론으로, state를 작성하는 시점에 부여되는 형태가 변경되는 경우는 대부분 없음
-
하지만 복수형태의 변수또한 존재할 수는 있는데, 이를 작성시점에 부여할 수 있음
const [value, setValue] = useState<number|string>(1); setValue('hello'); // OK setValue(2); // OK setValue(true); // NOT OK
-
- 폼을 이용하는 기본 소스코드를 일단 첨부
import { useState } from 'react';
const App = () => {
const [value, setValue] = useState('');
const onChange = (event) => {};
return (
<div>
<form>
<input type='text' placeholder='username' value={value} onChange={onChange} />
<button>Login</button>
</form>
</div>
);
};
export default App;-
TypeScript에서는 onChange() 함수의 event를 매개변수로 하는 부분에서 에러가 발생함
- JavaScript에서 event는 any타입으로 설정되어 있기 때문
- TypeScript를 위해서 event에서 어떤 타입을 지정해야 할까?
-
React Forms and Events에서 TypeScript를 사용시 참고
-
☕️ SyntheticEvent에 대해서
const onClick = (event: React.FormEvent<HTMLButtonElement>) => {};
- 위와 같이 React 기반으로 폼이벤트를 지정해서 해당 폼 안에 있는 엘리먼트를 직접 지정하거나, 마우스 이벤트등과 같이 별도의 React만의 이벤트 정의를 이용하게 되는데, 타입스크립트는 이벤트의 타입을 React 전용 타입으로 지정할 뿐임
- 따라서 이러한 방식은 React에 의존한 코딩방식이 되며, 다른 라이브러리(예를 들어 Vue나 Angular)에서는 다른 이벤트 정의 방식을 사용할 수 있음
-
이처럼 React와 TypeScript를 동시에 사용하면서, 서로 보완하는 문법은 배워야 하지만 크게 바뀌지 않고 여러번 사용하다보면 패턴처럼 몸에 익을수 있음
-
간단한 유저이름을 입력받고, 로그인 버튼을 누르면 콘솔에 출력되는 어플리케이션
💡 TypeScript를 이용한 React.FormEvent의 활용으로 런타임이 아닌 컴파일타임에 에러를 발견할 수 있음import React, { useState } from 'react'; const App = () => { const [value, setValue] = useState(''); const onChange = (event: React.FormEvent<HTMLInputElement>) => { setValue(event.currentTarget.value); }; const onSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); console.log(`hello ${value}`); }; return ( <div> <form onSubmit={onSubmit}> <input type='text' placeholder='username' value={value} onChange={onChange} /> <button>Login</button> </form> </div> ); }; export default App;
- 스타일 컴포넌트 섹션에서 공부했던 테마 적용의 경우 TypeScript와 연계하기에 적합한데, index파트에 테마를 적용하고 앱 컴포넌트를 ThemeProvider로 감싸주어, App 컴포넌트 이하 자식 컴포넌트들에서 해당 테마 오브젝트에 접근하는 형태였음
- 이러한 형태를 사용하기 때문에, 테마에 정의되지 않은 Prop에 접근한다거나 코딩시에 철자를 실수한다거나 하는 문제로 런타임시에 의도하지 않게 버그나 작동불가 상황이 발생하게 되는데, TypeScript와 vscode를 연계하여 이러한 문제해결을 시도해볼 수 있음
- TypeScript에서는 타입을 위한 정의문서가 존재하는데, [filename].d.ts 가 그것임
- styled-components의 경우 @types/styled-components 패키지에 해당 파일이 정의되어 있는데, 이를 extending하여 프로젝트 고유의 몇가지 정의문서를 추가할 수 있음
-
정의문서 베이직 참고 사이트
-
샘플코드
-
index.tsx
import React from 'react'; import ReactDOM from 'react-dom'; import { ThemeProvider } from 'styled-components'; import App from './App'; import { darkTheme, lightTheme } from './theme'; ReactDOM.render( <React.StrictMode> <ThemeProvider theme={darkTheme}> <App /> </ThemeProvider> </React.StrictMode>, document.getElementById('root'), );
-
App.tsx
import styled from 'styled-components'; const Container = styled.div` background-color: ${(props) => props.theme.bgColor}; `; const H1 = styled.h1` color: ${(props) => props.theme.textColor}; `; const App = () => { return ( <Container> <H1>World, World!</H1> </Container> ); }; export default App;
-
styled.d.ts
// import package to extend import 'styled-components'; // extending declare module 'styled-components' { export interface DefaultTheme { textColor: string; bgColor: string; btnColor: string; } }
-
theme.ts
import { DefaultTheme } from 'styled-components'; export const lightTheme: DefaultTheme = { bgColor: 'white', textColor: 'black', btnColor: 'tomato', }; export const darkTheme: DefaultTheme = { bgColor: 'black', textColor: 'white', btnColor: 'teal', };
-
-
- 만약 특정 라이브러리를 다운받았는데, 타입스크립트 지원이 되지 않는 경우
- [filename].d.ts가 없는 경우
-
DefinitlyTyped에서 먼저 찾아보기 만약 있다면 설치
npm install -D @types/{packageName} # GitHub에서 패키지를 검색하려 들지 말고, # 바로 npm을 이용해서 패키지명을 넣어보고, 인스톨 되는지 확인하는게 빠름
- CoinPaprika의 API를 이용해서 데이터를 추출해오고 표시해주는 어플리케이션
- fetch를 이용해서 데이터를 가져오는 방법 → React Query를 이용해서 데이터를 가져오는 방법
# 기본 패키지 인스톨
npm install styled-components react-router-dom react-query
npm install -D @types/styled-components# Structure
/ # All coins
/:coinId # Coin details
/:coinId/information # Coin information
/:coinId/chart # Coin chart-
브라우저는 기본 스타일링을 가지고 있기 때문에, 원하는 스타일링을 적용하기 위해 한번 리셋하는 과정을 거치는것이 좋음
-
리셋 스타일을 적용하는 방법
-
styled-reset 패키지 설치 후 적용
# npm i styled-reset (use the -S flag if you're on npm 4 or earlier). # If you're on styled-components 3.x, please npm i styled-reset@1.7.1. npm install styled-reset
// Styled Components 4.x || 5.x import * as React from 'react' import { Reset } from 'styled-reset' const App = () => ( <React.Fragment> <Reset /> <div>Hi, I'm an app!</div> </React.Fragment> )
-
스타일링을 styled-components를 이용한 global style 객체를 생성해서 적용
// You can also use the default export // or named export (lowercase) in your own global style import * as React from 'react' import { createGlobalStyle } from 'styled-components' import reset from 'styled-reset' const GlobalStyle = createGlobalStyle` ${reset} /* other styles */ * { box-sizing: border-box; } body { font-family: 'Source Sans Pro', sans-serif; } a { text-decoration: none; } ` const App = () => ( <> <GlobalStyle /> <div>Hi, I'm an app!</div> </> } export default App
/* * Reset CSS */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, menu, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, main, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section { display: block; } /* HTML5 hidden-attribute fix for newer browsers */ *[hidden] { display: none; } body { line-height: 1; } menu, ol, ul { list-style: none; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } table { border-collapse: collapse; border-spacing: 0; }
-
-
폰트를 적용할때는 구글 폰트사이트를 이용함
-
Source Sans Pro를 이용하여 Light 300, Regular 400을 불러온 뒤에 다음을 적용
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&family=Source+Sans+Pro:wght@300;400&display=swap'); ${reset} * { box-sizing: border-box; } body { font-family: 'Source Sans Pro', sans-serif; } a { text-decoration: none; }
-
-
배경색상등을 선택할때는 flat UI Colors를 이용
-
코인파프리카 API를 이용해서 데이터를 수집
💡 useEffect()는 deps에 지정된 state가 변경되는 타이밍에 실행되는 hook형 메소드인데 deps를 []로 아무런 state도 지정하지 않게 되면 컴포넌트가 로딩되는 최초 타이밍에 1회 실행됨useEffect(() => { const fetchCoins = async () => { const response = await fetch('https://api.coinpaprika.com/v1/coins'); const json = await response.json(); setCoins(json.slice(0, 50)); setLoading(false); }; fetchCoins(); }, []);
-
Coin 인터페이스를 작성해서 State를 정의
💡 인터페이스와 제네릭을 이용해서 State선언시 미리 변수타입을 지정하는 코딩방식을 채용interface CoinsInterface { id: string; name: string; symbol: string; rank: number; is_new: boolean; is_active: boolean; type: string; } const [coins, setCoins] = useState<CoinsInterface[]>([]);
-
Home을 만들고 나서의 동작확인에서 코인 상세화면에 이동후 다시 홈으로 돌아올 경우, coins State가 초기화 되기 때문에 API를 재호출하게 되는데, 이를 해결하는 방법을 다음 섹션에서 해설
-
추가내용
-
메인 화면에서 특정 코인을 클릭하면 코인에 대한 상세화면으로 이동
-
상세화면으로 이동할때 전 화면에서 가지고 있는 정보를 백엔드에 다시 호출하는것은 바람직하지 못한 방식
-
화면 이동시에 어떻게 데이터를 전송할까?
- React Route Dom을 이용하면 Link에서 state prop을 이용하면 데이터를 포함해서 화면을 이동시킬 수 있음
- 다만 이동하는 화면을 직접 호출하면, 전 화면에서 받아야 하는 데이터가 존재하지 않아서 에러가 발생할 수 있음
-
useEffect()는 React에서 컴포넌트의 라이프타임 중 어떤 시점에 코드를 실행할지 결정하는 함수인데, dependency를 어떻게 설정하느냐에 따라서 해당 코드의 실행 시점이 결정됨
-
개념적인 의미로는 deps는 array형태의 state 변수로 이루어져 있으며, 의미로는 해당 state들이 변경되는 타이밍에 코드를 실행한다는 점임
-
deps를 공란으로 두면 어떤 state도 watch하지 않지만, 최초 컴포넌트가 실행되는 타이밍에는 모든 useEffect가 실행되므로, 한번 실행되는것은 보장함
-
useEffect에 대한 자세한 설명은 다음 블로그에서 설명하고 있으니 나중에 참고
-
Nested Routes는 Route안에 있는 또 다른 Route를 의미
-
Nested Router는 다음의 경우 유용하게 사용 가능
- 같은 스크린 안에 다양한 섹션이 나누어져 있는 경우
- 탭으로 여러 분기점을 만드는 경우
-
이것을 구현하기 위해서 State를 이용하거나, Nested Router를 이용할 수 있음
-
Crypto Tracker에서 구현해볼 것들
-
코인 상세화면에서 2가지 탭을 구현 (Chart, Price)
-
각각의 분기는 URL을 이용한 Nested Router를 컴포넌트에서 구현하는 것으로 달성
localhost:3000/btc-bitcoin/chart localhost:3000/btc-bitcoin/price # 위의 두 URL은 같은 화면이지만, 다른 탭을 표시하도록 구현
-
-
URL을 이용해서 분기점을 구현하면, 유저들이 직접 접근할수 있기 때문에 유용함
-
구현 순서
-
routes 디렉토리에 Chart.tsx Price.tsx로 각각의 라우터에서 구현할 화면 컴포넌트를 작성
-
react-route-dom에서 Routes와 Route를 import한 뒤, 화면에서 Routes를 이용해서 분기점을 작성하여 각각의 Route에서 화면(Chart, Price)을 불러옴
-
이 과정에서 react-route-dom의 document를 확인해볼것
-
힌트: path, element 등
- path는 특정 URL path에서 실행할 컴포넌트를 지정 가능
- 부모 라우터의 경로에 /* 를 지정해줘야 해당 컴포넌트 안에서 자식 컴포넌트가 라우터를 렌더링 할 수 있음
function App() { return ( <Routes> <Route path="/" element={<Home />} /> <Route path="dashboard/*" element={<Dashboard />} /> </Routes> ); } function Dashboard() { return ( <div> <p>Look, more routes!</p> <Routes> <Route path="/" element={<DashboardGraphs />} /> <Route path="invoices" element={<InvoiceList />} /> </Routes> </div> ); }
-
-
탭 만들기 (CSS 포함)
-
Link 컴포넌트를 이용해서 탭을 만들기
-
anchor와의 차이점은 anchor는 페이지 자체를 새로고침 하지만, Link의 경우에는 필요한 컴포넌트만 새로 렌더링 한다는 점
-
탭에서 현재 URL정보를 보면서 스타일링을 적용해야 할 경우
- useMatch()를 이용해서 현재 URL정보를 가져오기
- useMatch('/:coinId/price')를 이용하면 현재 URL의 정보가 price인지 확인가능
- usematch는 매치하면 match 객체를, 매치 하지 않으면 null을 반환함
-
탭에 isActive라는 프로퍼티를 추가하고 URL검사결과를 이용
// 스타일 컴포넌트의 props 추가 방법 const Tab = styled.div<{ isActive: boolean }>` width: 200px; padding: 10px; border-radius: 10px; background-color: rgba(0, 0, 0, 0.7); color: ${({ isActive, theme: { accentColor, textColor } }) => isActive ? accentColor : textColor}; display: flex; justify-content: center; `;
-
-
-
먼저 React Query 패키지를 인스톨하자
npm install react-query
-
기본적인 React Query 사용방법
- 쿼리 클라이언트를 이용해서 어플리케이션을 감싸주면 됨
import { QueryClient, QueryClientProvider, useQuery } from 'react-query' const queryClient = new QueryClient() export default function App() { return ( <QueryClientProvider client={queryClient}> <Example /> </QueryClientProvider> ) }
-
React 쿼리의 역할
-
특정 API에서 데이터를 가져와서 State 변수에 설정해줌
- Data State 자동관리
- isLoading State 제공
-
fetcher() 함수를 작성할 수 있게 해줌
const response = awiat fetch('api address'); const json = await response.json(); // 상기 코드를 fetch 과정이라고 하면 fetcher 함수는 이런것들을 함축하고 있음
-
한번 요청한 데이터는 캐싱하여 재사용 할 수 있게 해줌
-
-
React 쿼리 만드는 방법
- api.ts파일 작성
- 이 파일에는 fetch 함수들이 나열됨
- useQuery()를 이용해서 기존 코드의 refactoring을 달성
- api.ts파일 작성
-
CoinPaprika API를 이용해서 차트에 필요한 데이터 받아오기
https://api.coinpaprika.com/#tag/Coins/paths/
1coins1%7Bcoin_id%7D1ohlcv1historical/get- 특정 코인의 날짜별 기록을 받아올수 있는 API가 있음
- React Query를 이용해서 2주전 코인 가격부터 시작해서 조회일자까지를 받아오는 fetcher함수를 제작
-
데이터 시각화 라이브러리 (APEXCHART.js)
ApexCharts.js - Open Source JavaScript Charts for your website
- APEXCHART 설치 순서
-
Docs → Intergration → React Chart
npm install --save react-apexcharts apexcharts
-
React apexchart 패키지를 import 한 뒤에 도큐먼트의 컨픽을 확인하면서 옵션 추가
import ApexChart from 'react-apexcharts'; <ApexChart type='candlestick' series={[ { data: historicals?.map((historicalData) => { return [ new Date(historicalData.time_close).getTime(), [ historicalData.open, historicalData.high.toFixed(3), historicalData.low.toFixed(3), historicalData.close.toFixed(3), ], ]; }), }, ]} options={{ chart: { width: 500, height: 500, toolbar: { show: false }, zoom: { enabled: false }, background: 'transparent', }, theme: { mode: currentTheme.themeName, }, grid: { show: false, }, yaxis: { labels: { show: false, }, }, xaxis: { labels: { formatter: function (value) { return new Date(value).toUTCString().slice(5, 11); }, }, }, }} />
-
- APEXCHART 설치 순서
- Recoil을 이용한 상태관리 방식에 대해서
- Recoil은 미니멀하고 다루기 쉬운 라이브러리
- React에서 상태관리가 왜 필요할까?
- 글로벌하게 이용되는 State를 만들려면 App 컴포넌트 (루트 컴포넌트)에 State를 만들고, 해당 State를 State가 필요한 컴포넌트까지 Props를 이용해서 State를 전달해야 함
- 이렇게 상태를 각각의 컴포넌트에서 공유할때 해당 State 관리가 힘들어진다는 불편함이 생김
- 글로벌 State를 특정 장소에 보관하는 기능이 생긴다면? 그리고 해당 장소에 모든 컴포넌트가 접근할 수 있다면?
- 이런 생각에서 등장한것이 상태관리 라이브러리임
-
리코일은 React에서 발생하는 이러한 상태전달 문제를 해결하기 위해 등장한 라이브러리이며, 리코일에서 사용하는 상태저장소를 Atom이라고 부름
-
각각의 atom에는 각기 다른 값을 저장할 수 있음
-
만들어진 atom은 각각의 컴포넌트와 연결하고 바로 사용 가능함
-
이러한 개념을 통해, 글로벌 State의 관리가 가능해짐
-
Recoil의 설치 및 이용 방법
-
npm으로 recoil을 설치
npm install recoil
-
App을 RecoilRoot로 감싸줌
<RecoilRoot> <App /> </RecoilRoot>
-
atoms.ts 작성
import { atom } from 'recoil'; export const themeNameAtom = atom({ key: 'themeName', default: 'light', });
-
useRecoilValue()를 통해서 값을 받아서 사용
const themeName = useRecoilValue(themeNameAtom);
-
useSetRecoilState()를 통해서 값을 설정
- useStateRecoilState()는 해당 atom의 setter function을 리턴
const setThemeName = useSetRecoilState(themeNameAtom); const toggleTheme = () => setThemeName((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light')); <PaletteBtn onClick={toggleTheme}> <FontAwesomeIcon icon={faPalette} /> </PaletteBtn>
-
-
Atom 또한 State의 일부이며, Atom을 사용한다는 것은 해당 State를 구독한다는 뜻이기 때문에, Atom에 어떤 값의 변화가 있을 경우에는 컴포넌트가 다시 렌더링 된다는 것을 의미함
- 좋은점은 자동으로 변경된 값을 반영할수 있음—
-
redux, recoil 내용 정리
-
정리
- 상태관리 라이브러리는 Traveling Props 문제를 해결함
- 상태관리 라이브러리 중 Recoil을 사용, Recoil은 Atom이라는 글로벌 State를 이용해서 상태를 관리함
- Recoil의 사용법
- useRecoilValue()
- useSetRecoilState()
-
React에서 특정 폼을 만들고 해당 폼 안에 인풋을 만들려면 기본적으로 다음과 같은 코딩이 필요함
import { useState } from 'react'; const ToDoList = () => { const [toDo, setToDo] = useState(''); const onChange = (event: React.FormEvent<HTMLInputElement>) => { setToDo(event.currentTarget.value); }; const onSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); console.log(toDo); }; return ( <div> <form onSubmit={onSubmit}> <input type='text' placeholder='Type your to do' value={toDo} onChange={onChange} /> <input type='submit' value='Add ToDo' /> </form> </div> ); }; export default ToDoList;
-
하나의 인풋에 onChange 훅이 필요하기 때문에, 인풋을 추가할때마다 함수를 추가로 배정해서 관리해야 함
-
또한 인풋에 들어가는 값을 일일히 State화 하여 관리해야 함 (덤으로 검증작업에 대한 로직도 별도 작성)
-
이를 편하게 관리할 수 있도록 도와주는 다양한 라이브러리가 존재
- React Hook Form
-
React Hook Form을 설치하고 사용하기
-
설치
npm install react-hook-form
-
useForm() 훅을 이용한 인풋 관리
const { register, watch } = useForm(); <form> <input {...register('ToDo')} type='text' placeholder='Type your to do' /> <input type='submit' value='Add ToDo' /> </form>
-
-
React Hook Form에서 인풋별로 옵션 추가하기
-
항목의 필수화
- 항목에 옵션으로 required를 설정하게 되면, RHF이 자동으로 항목을 체크하고 후처리를 담당해줌
- 빈칸 자동으로 focus 처리하기
<input {...register('email', { required: true })} placeholder='Enter email' />
-
최소길이 설정
minLength: 10
-
기타 설정값에 대해서 알아보기
-
-
React Hook Form으로 폼 검증하고, 에러 출력하기
const ToDoList = () => { const { register, handleSubmit, formState: { errors }, setError, } = useForm<FormProps>({ defaultValues: { email: '@naver.com', }, }); const onValid = ({ password, passwordConfirm }: FormProps) => { if (password !== passwordConfirm) { setError('passwordConfirm', { message: 'Password are not the same!' }, { shouldFocus: true }); } //setError('extraError', { message: 'Server Offline' }); }; return ( <div> <form style={{ display: 'flex', flexDirection: 'column' }} onSubmit={handleSubmit(onValid)}> <input {...register('email', { required: 'Email is required!', pattern: { value: /^[A-Za-z0-9._%+-]+@naver.com/, message: 'Only naver.com email is allowed', }, })} placeholder='Email' /> <Error>{errors?.email?.message}</Error> <input {...register('username', { required: 'Username is required!', minLength: { value: 8, message: 'Username is too short', }, validate: { noAdmin: (value) => !value.includes('admin') || 'No admin include on username', }, })} placeholder='Username' /> <Error>{errors?.username?.message}</Error> <input {...register('password', { required: 'Password is required!', minLength: { value: 5, message: 'Password is too short', }, })} placeholder='Password' /> <Error>{errors?.password?.message}</Error> <input {...register('passwordConfirm', { required: 'Password Confirm is required!', minLength: { value: 5, message: 'Password is too short', }, })} placeholder='Password Confirm' /> <Error>{errors?.passwordConfirm?.message}</Error> <input type='submit' value='Add ToDo' /> <Error>{errors?.extraError?.message}</Error> </form> </div> ); };
-
Recoil State를 이용해서 작성하는 ToDo 어플리케이션
-
입력은 React Hook Form 라이브러리를 이용해서 최대한 줄이고, 로직 구현에만 집중
-
기본 코드
import { useForm } from 'react-hook-form'; import { atom, useRecoilState } from 'recoil'; interface ToDo { id: number; text: string; category: 'TO_DO' | 'DOING' | 'DONE'; } interface FormProps { ToDo: string; } const toDoState = atom<ToDo[]>({ key: 'toDo', default: [], }); const ToDoList = () => { const [toDos, setToDos] = useRecoilState(toDoState); const { register, handleSubmit, setValue } = useForm(); const onSubmit = ({ ToDo }: FormProps) => { setToDos((prevToDos) => [{ id: Date.now(), text: ToDo, category: 'TO_DO' }, ...prevToDos]); setValue('ToDo', ''); }; return ( <div> <h1>ToDos</h1> <hr /> <form onSubmit={handleSubmit(onSubmit)}> <input {...register('ToDo')} type='text' placeholder='Write a to do' /> <input type='submit' value='Add' /> </form> <ul> {toDos.map(({ id, text }) => ( <li key={id}>{text}</li> ))} </ul> </div> ); }; export default ToDoList;
-
React에서는 useState() 훅을 이용해서 페이지의 상태를 관리하게 됨
-
State변수는 기본적으로는 값을 담는 value와 값을 조정하는 modifier 함수를 별도로 지니게 됨
-
값의 경우에는 readonly 타입으로 값을 조정하는 setter를 통해서만 변수의 값이 변하며 이를 직접적으로 수정하고자 한다면 새로운 그릇에 변수를 담아서 변경된 그릇을 리턴해야 함
-
ToDo 어플리케이션에서 활용한 소스코드
import { useRecoilState } from 'recoil'; import { toDoState } from '../atoms'; import ToDo from '../interfaces/ToDo'; const BaseToDo = ({ text, category, id }: ToDo) => { const [toDos, setToDos] = useRecoilState(toDoState); const onClick = (newCategory: ToDo['category']) => { const targetPosition = toDos.findIndex((toDo) => toDo.id === id); const newToDo: ToDo = { text, id, category: newCategory }; // replace with new ToDo setToDos((prevToDos) => { const newToDos = [...prevToDos]; newToDos[targetPosition] = newToDo; return newToDos; }); }; return ( <li> <span>{text}</span> {category !== 'DOING' && <button onClick={() => onClick('DOING')}>Doing</button>} {category !== 'TO_DO' && <button onClick={() => onClick('TO_DO')}>To Do</button>} {category !== 'DONE' && <button onClick={() => onClick('DONE')}>Done</button>} </li> ); }; export default BaseToDo;
const [toDos, setToDos] = useRecoilState(toDoState); // 이 부분이 state를 선언하는 부분 // toDos라는 배열과, setToDos라는 setter함수를 이용해서 state를 선언한 뒤
setToDos((prevToDos) => { const newToDos = [...prevToDos]; newToDos[targetPosition] = newToDo; return newToDos; }); // setToDos를 이용해서 변경할때는 prev값을 받아오고 직접 수정할수가 없음 // 왜냐하면, prevToDo의 경우에는 readonly로 지정되어 있으며, // 이를 직접 수정하는 것이 금지되어 있기 때문이다. // 따라서, 새로운 newToDos 배열을 기존 값을 복사하는 방식으로 만들어 낸 뒤, // 내용을 수정한 newToDos를 반환한다.
-
Recoil에는 Selector라는 개념이 있음
-
간단하게 이야기하면, Recoil은 전역 상태를 atom이라는 개념으로 관리하는데, selector는 atom에 함수적 처리를 통해 state를 쪼개는것을 말함
-
state를 쪼개지 않고 특정 컴포넌트에서 atom 전체를 받아서 사용해버리면, atom에 상태변화가 생겼을때 컴포넌트 내에서 atom의 일부분만 이용하고 있다고 하더라도, 전부 영향을 받아버리게 됨
-
따라서, atom을 selector를 이용해서 쪼개주는것은 중요하며, to do 어플리케이션으로 예를 들면 to do안의 카테고리 (todo, doing, done)등을 selector로 분리시켜주는 것임
-
selector를 사용한 또다른 이점으로는 selector는 기본적으로 캐싱을 지원한다는 것인데, 이에 대한 자세한 설명을 해놓은 블로그를 발견했으니 참고하자
[Recoil] Selector를 이용하여 API 값 캐싱하기 with React & TypeScript 📮
-
toDo 어플리케이션에서 selector를 이용해서 atom을 분리한 샘플코드
// atom.ts import { atom, selector } from 'recoil'; import ToDo from './interfaces/ToDo'; export const toDoState = atom<ToDo[]>({ key: 'toDo', default: [], }); export const toDoSelector = selector({ key: 'toDoSelector', get: ({ get }) => { const toDos = get(toDoState); return [ toDos.filter((toDo) => toDo.category === 'TO_DO'), toDos.filter((toDo) => toDo.category === 'DOING'), toDos.filter((toDo) => toDo.category === 'DONE'), ]; }, });
// ToDoList.tsx import { useRecoilValue } from 'recoil'; import { toDoSelector } from '../atoms'; import BaseToDo from './BaseToDo'; import CreateToDo from './CreateToDo'; const ToDoList = () => { const [toDos, doings, dones] = useRecoilValue(toDoSelector); return ( <div> <CreateToDo /> <hr /> <h1>To Do</h1> <ul> {toDos.map((toDo) => ( <BaseToDo key={toDo.id} {...toDo} /> ))} </ul> <hr /> <h1>Doing</h1> <ul> {doings.map((doing) => ( <BaseToDo key={doing.id} {...doing} /> ))} </ul> <hr /> <h1>Done</h1> <ul> {dones.map((done) => ( <BaseToDo key={done.id} {...done} /> ))} </ul> </div> ); }; export default ToDoList;
- 카테고리 atom 만들어서 State화
- 메인화면에서 select option 추가해서 각각의 카테고리별로 추가 가능한 선택지 제공
- 카테고리에 따라서 각각 다른 State를 출력
- 카테고리 state에 따라 각각의 todo를 반환하는 selector를 만듦