Instagram Clone : Frontend - part 1 [ SETUP ]

gwanuuoo·2021년 9월 19일
0
post-thumbnail

SETUP

Create React App

타입스크립트로 리액트 앱 만들기

npx create-react-app [앱 이름] --template typescript

NPX란?
패키지를 실행시켜주는 npm 명령어. ( 다운로드 받을 때, 저장 / 실행 중 실행을 누르는 것과 동일 )
패키지를 저장시키지 않고 실행만 시켜준다. 즉, 컴퓨터에 create-react-app을 설치하지 않고 호출함.

폴더를 레포지토리와 연결 시키기

git remote add origin [레포 주소] 

폴더 정리

  1. src 안에 index.tsApp.ts를 제외한 모든 파일을 제거
  2. index.tsApp.ts 안에 있는 제거한 파일들을 import 하는 코드를 모두 제거

Installing Packages

설치할 패키지

  1. Styled-Components → CSS 스타일 컴포넌트
  2. React Hook Form → React에 양식을 쉽게 만들어줌
  3. React Router → URL을 통해 컴포넌트 간 이동
  4. Apollo Client → client의 token으로 접근
  5. React-Helmet-Async → 브라우저 타이틀을 동적으로 변경
  6. React-FontAwesome → 아이콘 사용

설치 명령어

// 여러 패키지를 한번에 설치 가능
npm i styled-components react-router-dom react-hook-form @apollo/client graphql react-helmet-async

npm i --save @fortawesome/fontawesome-svg-core
npm install --save @fortawesome/free-solid-svg-icons
npm install --save @fortawesome/react-fontawesome
npm install --save @fortawesome/free-brands-svg-icons
npm install --save @fortawesome/free-regular-svg-icons

Router Setup

React Router는 URL - path가 매칭되는 것을 기반으로 동작한다. URL안에 path가 포함된다면, Router에서 매칭이 되는 것으로 판단하여 여러 컴포넌트를 동시에 렌더시킨다. (Pattern Matching)

하나의 Route만 렌더시키기 위해선, Swtich 를 사용해야한다. 하지만, Swtich만 사용하면 path="/"에서 모든 URL을 가로채기 때문에, exact 를 같이 사용해야한다. exact를 사용하면 URL이 path와 정확히 일치하는지 확인하는 작업을 거쳐, 내가 원하는 컴포넌트를 렌더시킬 수 있다.

import { BrowserRouter as Router, Route } from "react-router-dom";

function App() {
  return (
    <div>
      <Router>
	<Switch>
	  <Route path="/" exact>
	    <h1>home</h1>
	  </Route>
	  <Route path="/potato">
	    <h1>potato</h1>
	  </Route>
	  <Route path="/banana">
	    <h1>banana</h1>
	  </Route>
	<Switch>
      </Router>
    </div>
  );
}

export default App;

로그인 여부에 따라, 다른 컴포넌트를 보여주기 위해서 상태를 만들어 로그인 상태가 바뀔 때마다 삼항연산자를 이용하여 다른 컴포넌트를 렌더시킬 수 있다.

Swich의 가장 마지막에 Routepath 없이 만들면, 유저가 지정해놓은 URL 이외의 주소로 접속할 경우 에러 화면을 띄워줄 수 있다.

... 
function App() {
	const isLoggedIn = false;
  return (
    <div>
      <Router>
		<Switch>
          <Route path="/">{isLoggedIn ? <Home /> : <Login />}</Route>
          <Route>
            <NotFound />
          </Route>
        </Switch>
      </Router>
    </div>
  );
}
...

다른 방법으로는 Swich의 가장 마지막에 Redirect를 이용하여 다시 홈페이지로 이동시키는 것도 가능하다.

...
        <Switch>
          <Route path="/">{isLoggedIn ? <Home /> : <Login />}</Route>
          <Redirect to="/" />
        </Switch>
...

Auth POC

최상위 컴포넌트인 App에 로그인 상태를 만들고 하위 컴포넌트에 props로 로그인 상태를 내려주는 것은 좋지 않다. 왜냐하면, 로그인 상태는 여러 컴포넌트에서 사용하고, 또 depth가 깊이 있는 컴포넌트까지 계속 아래로 전달해야하기 때문에 props-drilling 문제가 생길 수 있다.

이에 대한 대안으로, Apollo Client가 있다. Apollo Client는 로컬 상태를 만들 수 있는 기능을 제공한다.

Reactive Variables

로그인 / 로그아웃 구현 시 가장 좋은 방법은 어떤 컴포넌트든 props를 내려줄 필요없이 로그인 / 로그아웃을 시키는 것이다. 이를 구현하기 위해서 Apollo Client에 포함된 Reactive Variables를 사용하면 된다.

Reactive Variable이란, 기본적으로 반응하고 변하는 변수다. 사용법은 다음과 같이 간단하다.

import { makeVar } from '@apollo/client';

const cartItemVar = makeVar([]); // [] <- deafault value

우선, Reactive Variables만 관리할 파일을 만든다. 여기에는 props로 전달하지 않고 계속 리스닝만 할 수 있는 상태들을 넣어준다. 로그인 상태, 다크모드, 유튜브의 볼륨 조절 등이 이런 상태로 관리하면 좋다.

// apollo.tsx
export const isLoggedInVar = makeVar(false);

컴포넌트에서 useReactiveVar을 호출하여 안에 방금 만든 isLoggedInVar 변수를 넣어준다.

이제 어디서든지 (props를 전달해 줄 필요 없이) isLoggedInVar을 변경할 수 있다. 그렇게 되면, 모든 컴포넌트가 다시 렌더된다.

// App.tsx
...
function App(){
	const isLoggedIn = useReactiveVar(isLoggedInVar);
	return (
...

이벤트를 발생시켜 상태를 바꿔보자. 정말 간단하게, 이벤트 안에 변경할 값이 들어간 함수를 넣어주면 끝이다.

이 이벤트가 발생하면, Reactive Variables를 만든 App.tsx와 하위 모든 컴포넌트들이 다시 렌더된다.

// Login.tsx
...
<button onClick={() => isLoggedInVar(true)}>Log in now!</button>
...

Styled Components

Basics

Styled Components는 말 그대로 컴포넌트를 이용하여 CSS 작성할 수 있도록 해준다. 보통 CSS는 className과 id를 사용하여 다른 CSS 파일에 속성 값을 주는 방식으로 작업이 진행된다. 그래서 만약, React에서 상태에 따라 다른 CSS를 렌더시키고 싶다면 다음과 같은 방법을 사용할 것이다.

// Login.tsx
...
<div className=`{selected ? "selected" "unselected"}`>
	<h1>Login</h1>
</div>
... 
.selected {
	color : red,
	...	
}

.unselected {
	color : gray
	...
}

Styled-Components는 완전 다른 방식으로 접근한다. className이나 id가 아닌 컴포넌트를 만드는데, 컴포넌트에 CSS를 직접 삽입하는 방식이다. 마치, 나만의 커스텀 태그를 만드는 것과 같다. 내가 원하는 이름과 CSS로 컴포넌트를 만들어서 React에서 사용하면 된다. 다음과 같은 방법으로 Styled-Components를 사용할 수 있다.

// Login.tsx
...
const Title = styled.h1`
	color: bisque;
`

const Container = styled.div`
	background-color: tomato;
`

function Login(){
	return (
		<Container>
			<Title>Login</Title>
		</Container>
	)
}

위와 같이, 컴포넌트를 만드는 방법은 다음과 같다.

const [컴포넌트 이름] = styled.[HTML 태그]`
	[CSS 속성] : [값]
` 

모든 HTML 태그와 CSS 속성이 자동완성 기능으로 제공되기 때문에 작성하기 굉장히 편하다.

Styled-Components는 React 컴포넌트이기 때문에, props를 받는다. props를 이용해서 state가 변화할 때마다 스타일을 동적으로 변화시키는 것도 가능하다. 방법은 다음과 같다.

  1. 단일 CSS 속성에 props를 내려주는 방법
const Button = styled.button`
  background: ${props => props.primary ? "palevioletred" : "white"};
  color: ${props => props.primary ? "white" : "palevioletred"};
  ...
`;

render(
  <div>
    <Button>Normal</Button>
    <Button primary>Primary</Button>
  </div>
);
  1. CSS 속성 전체를 새로 적어서 내려주는 방법
// Login.tsx
...
const Title = styled.h1`
	color: bisque;
	...
	${props => props.potato ? css`
		font-size: 50px;
	`
	: css`
		text-decoration: underline;	
	`}
`
...
	const togglePotato = () => setPotato((current) => !current);
	return (
		<Container>
			<Title potato={potato}>Login</Title>
			<TogglePotato onClick={togglePotato}>Toggle Potato</TogglePotato>
		</Container>
	)
...

Themes

Theme 생성

타입스크립트로 작성하고 있기 때문에, 새로운 Theme을 선언하기 위해선 Theme의 타입이 필요하다.

(아래 TS SETUP 참고) styles.ts란 파일에 따로 Theme만 만들어주었다.

// styles.ts
...
export const darkTheme: DefaultTheme = {
  fontColor: "lightgray",
  bgColor: "#2c2c2c",
};

export const lightTheme: DefaultTheme = {
  bgColor: "lightgray",
  fontColor: "#2c2c2c",
};

Theme 적용

Light와 Dark Theme을 구현하기 위해, ThemeProvider를 추가해야한다.

ThemeProvider는 기본적인 테마를 제공해준다.

적용 방법은 Theme을 적용시키고 싶은 컴포넌트에 ThemeProvider라는 컴포넌트를 감싸주면 된다.

Router가 시작되기 전에 이미 Theme이 적용되는 것을 확인할 수 있다.

다크모드 로컬 상태를 만들어준다. 그리고 다크모드 상태를 관리하기 위해 Reactive Variables를 사용한다.

// apollo.tsx
...
export const darkModeVar = makeVar(false);
// App.tsx
...
  const darkMode = useReactiveVar(darkModeVar);
  const isLoggedIn = useReactiveVar(isLoggedInVar);

  return (
    <ThemeProvider theme={darkMode ? darkTheme : lightTheme}>
      <Router>
        <Switch>
          <Route path="/">{isLoggedIn ? <Home /> : <Login />}</Route>
          <Route>
            <NotFound />
          </Route>
        </Switch>
      </Router>
    </ThemeProvider>
  );
...

다크모드를 변경하기 위해서, Theme의 상태에 따라 동적으로 변하는 스타일 컴포넌트와 버튼을 만든다.

// Login.tsx
...
const Title = styled.h1`
  color: ${(props) => props.theme.fontColor};
`;

const Container = styled.div`
  background-color: ${(props) => props.theme.bgColor};
`;

function Login() {
  return (
    <Container>
      <Title>Login</Title>
      <button onClick={() => darkModeVar(true)}>To dark</button>
      <button onClick={() => darkModeVar(false)}>To light</button>
    </Container>
  );
}
...

이로써, 버튼 클릭 시 다크모드는 적용되지만 새로고침 시 스타일 상태가 유지되지 않는 문제가 발생한다.

Global Styles

Style-Reset 설치

style-reset 설치 → 모든 스타일 속성을 0으로 만들어줌

npm i styled-reset

Global Styles 선언

지금까지는 HTML 태그 단위의 스코프에서 CSS 속성을 주었다면, Styled-Components의 Global Styles를 이용하여 전역 스코프에 CSS를 적용시킬 수 있다. 선언하는 방법은 createGlobalStyle이라는 메서드를 호출한 후, 백틱 안에 바로 CSS 속성을 넣는 것이 아닌 속성을 적용시킬 태그를 먼저 명시해준 후에 그 CSS를 넣어주면 된다.

GlobalStyles 컴포넌트가 ProvideThemes 안에 있기 때문에, props.themes에서 Theme으로 저장한 속성을 가져오는 것도 가능하다.

추가적으로, 전역의 모든 CSS 속성을 기본적으로 리셋해주기 위해 styled-reset을 적용시켜준다.

// styles.ts
...
import reset from "styled-reset";

export const GlobalStyles = createGlobalStyle`
${reset}
    body {
        background-color: ${(props) => props.theme.bgColor};
    }
`;
	//* body 말고도 form이나 div 등의 태그도 가능

GlobalStyles 컴포넌트는 ThemeProvider 내 최상단에 넣어주면 전역에 CSS를 적용시킬 수 있다.

// App.tsx
...
<ThemeProvider theme={darkMode ? darkTheme : lightTheme}>
      **<GlobalStyles />**
      <Router>
        <Switch>
          <Route path="/">{isLoggedIn ? <Home /> : <Login />}</Route>
          <Route>
            <NotFound />
          </Route>
        </Switch>
      </Router>
</ThemeProvider>
...

TS SETUP

Styled-Components

styled-components 공식 문서

types 설치

npm i @types/styled-components

타입 선언

선언 파일을 만든다. 보통 파일명 뒤에 'd'를 붙여 만든다. 파일 안에서, "styled-component"의 타입을 확장한다. DefaultTheme (styled-components에서 지원하는 인터페이스) 라는 인터페이스를 export한다. 인터페이스 안에 배경색이나 폰트색 같은 속성의 타입을 모두 지정해준다.

//styled.d.ts
import "styled-components";

declare module "styled-components" {
  export interface DefaultTheme {
    bgColor: string;
    fontColor: string;
  }
}

내가 만들 Theme을 다른 파일에 만들어준다. 이 때, 선언 파일에서 조작한 DefaultTheme을 import 해준다. 이제, DefaultTheme의 지정된 타입에 맞게 Theme을 만들어주면 된다.

//styles.ts
import { DefaultTheme } from "styled-components";

export const ourTheme: DefaultTheme = {
  bgColor: "black",
  fontColor: "blue",
};

Theme 적용

만들어진 Theme을 ThemeProvider라는 컴포넌트를 통해 적용시켜준다.

//App.tsx
...
<ThemeProvider theme={ourTheme}>
      <Container>App</Container>
</ThemeProvider>
...

ThemeProvider 안에 감싸진 컴포넌트는 props를 내려주지 않아도 props.theme을 통해 미리 만들어놓은 Theme의 CSS 속성을 사용할 수있다.

만약 타입에 맞지 않는 속성을 추가한다면, 타입스크립트에서 미리 에러를 발생시킨다. 이런 경우, 다시 타입을 선언한 파일로 돌아가서 타입을 고쳐주면 된다.

props 타입 선언

인터페이스를 선언한 다음 props로 내려주는 속성들의 타입을 명시해주면 된다. 그리고, 스타일 컴포넌트를 선언할 때, HTML 태그와 함께 타입을 적어주면 된다.

//App.tsx
...
interface IContainerProps {
	floating : boolean;
}

const Container = styled.div<IContainerProps>`
...
`;

function App(){
return (
	<ThemeProvider theme={ourTheme}>
		<Container floating={true}>App</Container>
...

이제, floating이란 props의 상태에 따라, box-shadow의 값을 다르게 줄 수 있다.

React Hook Form

React Hook Form 공식 문서

Form 생성

기본적인 Form을 만들어준다.

// App.tsx
...
function App() {
  const { register, handleSubmit, getValues } = useForm();

  const onValid = (data) => {
    const { name, lastName } = getValues();
  };

  return (
    <form onSubmit={handleSubmit(onValid)}>
      <input {...register("name", { required: true })} type="text" />
      <input {...register("lastName")} />
    </form>
  );
}
...

여기서 문제는, data의 타입이 any이다. 이로 인해 두 가지 문제가 있는데,

  1. 입력 값마다 모두 인터페이스를 만들긴 번거롭다.
  2. 타입이 정해지지 않았기 때문에, 자동 완성이 되지 않는다.

useForm 타입화

간단하게 Form을 인터페이스로 보내주면 된다.

interface IForm {
	name : string;
	lastName? : string; // optional
}

handleSubmit 함수 인자 타입화

위의 코드에서 onValid의 data의 타입을 지정해주어야 한다. 두 가지 방법이 있다.

  1. onValid를 submitHandler로 만들어 data가 타입을 가지게 하기

    import { SubmitHandler, useForm } from "react-hook-form";
    
    ...
    const onValid : SubmitHandler<IForm> = (data) => {
    const { name, lastName } = getValues();
    ...
  2. getValues를 이용하고 getValues를 useForm 훅에서 가져오기

    getValues는 이미 타입화가 되어있기 때문에 자동 완성이 제공된다.

    // getValues - useForm<IForm> → lastName / name 타입 제공
    const onValid = () => {
        const { name, lastName } = getValues();
      };

완성된 코드는 다음과 같다.

import { SubmitHandler, useForm } from "react-hook-form";

interface IForm {
  name: string;
  lastName?: string;
}

function App() {
  const { register, handleSubmit, getValues } = useForm<IForm>();

  // op1 : onValid를 submitHandler로 만들어 data가 타입을 가지게 하기
  /*
      const onValid : SubmitHandler<IForm> = (data) => {
      Do something...
    };
  */

  // op2 : getValues를 이용하고 getValues를 useForm 훅에서 가져오기
  const onValid = () => {
    const { name, lastName } = getValues();
  };

  return (
    <form onSubmit={handleSubmit(onValid)}>
      <input {...register("name", { required: true })} type="text" />
      <input {...register("lastName")} />
    </form>
  );
}

GraphQL

Apollo 공식 문서

Apollo Tooling 설치

npm install -g apollo

서버 API 타입 만들기

Apollo Client의 codegen 명령어로 GraphQL 요청의 타입을 생성한다. 서버에 있는 GraphQL 스키마를 다운받은 다음 스키마에 있는 모든 것들에 대해 타입을 자동으로 만들어준다. 그 전에 설정 파일을 먼저 만들어주어야한다.

Configuration

백엔드가 아닌 React 프론트엔드에 apollo.config.ts 파일을 만들어준다. 그리고 다음과 같이 module.exports 코드를 입력한다. 이 설정 파일을 기반으로, Apollo가 React 컴포넌트로를 탐색하여 query와 mutation을 찾고 무엇을 썼든 Apollo가 인터페이스를 자동적으로 생성해준다.

// apollo.config.ts
module.exports = {
    client: {
	/* gql 코드의 위치에 대한 패턴
	(src 내 tsx나 ts로 끝나는 모든 파일의 코드에서 찾아라) */
        includes: ["./src/**/*.{tsx,ts}"],
	// tsx에 있는 모든 gql 태그를 찾음 -> 이를 위한 타입스크립트를 생성
        tagName: "gql",
        service: {	
	    // 서비스 이름
            name: "instaclone-backend",
	    // 백엔드의 URL
            url: "http://localhost:4000/graphql",
        },
    }
};

Codegen

다음 명령어로 타입 인터페이스를 생성해준다.

apollo client:codegen src/__generated__ --target=typescript --outputFlat

그러면, generated 폴더에 프론트엔드의 모든 GraphQL 요청 (query, mutation)에 대한 인터페이스가 만들어진 것을 볼 수 있다. 잘못된 요청 코드를 작성하는 것을 사전에 방지할 수 있다.

// App.tsx
const LOGIN_MUTATION = gql`
  mutation login($username: String!, $password: String!) {
    login(username: $username, password: $password) {
      ok
      token
      error
    }
  }
`;

const FEED_QUERY = gql`
  query seeFeed {
    seeFeed {
      id
      user {
        username
        avatar
      }
      file
      caption
      likes
      comments {
        id
      }
      isMine
    }
  }
`;

function App() {
  const [loginMutation] = useMutation<login, loginVariables>(LOGIN_MUTATION, {
    variables: { username: "hello", password: "1234" },
    // 뮤테이션의 리턴 타입
    onCompleted: (data) => data.login.token,
  });
  
  const { data } = useQuery<seeFeed>(FEED_QUERY);
  console.log(data?.seeFeed?.map((photo) => photo?.user.avatar));
  return <h1>Apollo</h1>;
}
// login.ts
export interface login_login {
  __typename: "LoginResult";
  ok: boolean;
  token: string | null;
  error: string | null;
}

export interface login {
  login: login_login;
}

export interface loginVariables {
  username: string;
  password: string;
}

// seeFeed.ts
export interface seeFeed_seeFeed_user {
  __typename: "User";
  username: string;
  avatar: string | null;
}

export interface seeFeed_seeFeed {
  __typename: "Photo";
  id: number;
  user: seeFeed_seeFeed_user;
  file: string;
  caption: string | null;
  likes: number;
  comments: number;
  isMine: boolean;
}

export interface seeFeed {
  seeFeed: (seeFeed_seeFeed | null)[] | null;
}

Codegen 명령어는 여러 번 실행하기 때문에, package.json 파일의 스크립트로 저장해놓는 것이 좋다.

코드를 실행할 때 마다, __generated__ 폴더를 먼저 지우는 것이 좋다.

// package.json
"scripts": {
   ...,
    "codegen": "apollo client:codegen src/__generated__ --target=typescript --outputFlat"
  },
profile
작지만 꾸준하게 성장하는 개발자🌳

0개의 댓글