React, Redux 및 Sanity.io로 웹 앱 구축하기
게시 됨: 2022-03-10디지털 플랫폼의 빠른 발전은 Wordpress와 같은 기존 CMS에 심각한 제한을 가했습니다. 이러한 플랫폼은 결합되어 있으며 유연하지 않으며 제품보다는 프로젝트에 중점을 둡니다. 고맙게도 이러한 문제와 그 이상을 해결하기 위해 여러 헤드리스 CMS가 개발되었습니다.
기존 CMS와 달리 SaaS(Software as a Service)로 설명할 수 있는 헤드리스 CMS는 웹사이트, 모바일 앱, 디지털 디스플레이 등을 개발하는 데 사용할 수 있습니다. 그들은 무제한 플랫폼에서 사용할 수 있습니다. 플랫폼 독립적이고 개발자 우선이며 교차 플랫폼 지원을 제공하는 CMS를 찾고 있다면 헤드리스 CMS에서 더 멀리 볼 필요가 없습니다.
헤드리스 CMS는 헤드가 없는 CMS입니다. 여기서 head
는 프론트엔드 또는 프리젠테이션 레이어를 나타내고 body
은 백엔드 또는 콘텐츠 저장소를 나타냅니다. 이것은 많은 흥미로운 이점을 제공합니다. 예를 들어 개발자가 원하는 프런트엔드를 선택할 수 있으며 프레젠테이션 레이어를 원하는 대로 디자인할 수도 있습니다.
헤드리스 CMS가 많이 있으며 가장 인기 있는 CMS에는 Strapi, Contentful, Contentstack, Sanity, Butter CMS, Prismic, Storyblok, Directus 등이 있습니다. 이러한 헤드리스 CMS는 API 기반이며 개별적인 장점이 있습니다. 예를 들어 Sanity, Strapi, Contentful 및 Storyblok과 같은 CMS는 소규모 프로젝트에 무료입니다.
이러한 헤드리스 CMS도 다양한 기술 스택을 기반으로 합니다. Sanity.io가 React.js를 기반으로 한다면 Storyblok은 Vue.js를 기반으로 합니다. React 개발자로서 이것이 내가 Sanity에 빠르게 관심을 갖게 된 주요 이유입니다. 그러나 헤드리스 CMS이기 때문에 이러한 각 플랫폼은 Angular, Vue 또는 React와 같은 모든 프론트엔드에 연결할 수 있습니다.
이러한 헤드리스 CMS 각각에는 상당한 가격 상승을 나타내는 무료 및 유료 계획이 있습니다. 이러한 유료 플랜은 더 많은 기능을 제공하지만 중소 규모 프로젝트에 많은 비용을 지불하고 싶지는 않을 것입니다. Sanity는 종량제 옵션을 도입하여 이 문제를 해결하려고 합니다. 이러한 옵션을 사용하면 사용한 만큼 비용을 지불하고 가격 상승을 피할 수 있습니다.
Sanity.io를 선택하는 또 다른 이유는 GROQ 언어입니다. 저에게 Sanity는 이 도구를 제공함으로써 군중들 사이에서 두드러집니다. GROQ(Graphical-Relational Object Queries)는 개발 시간을 단축하고 필요한 형식으로 필요한 콘텐츠를 얻는 데 도움이 되며 개발자가 코드 변경 없이 새 콘텐츠 모델로 문서를 만들 수 있도록 도와줍니다.
또한 개발자는 GROQ 언어에 제약을 받지 않습니다. 또한 GraphQL 또는 기존의 axios
를 사용하고 React 앱에서 fetch
백엔드를 쿼리할 수도 있습니다. 대부분의 다른 헤드리스 CMS와 마찬가지로 Sanity에는 플랫폼에서 구축하는 데 유용한 팁이 포함된 포괄적인 문서가 있습니다.
참고: 이 기사는 React, Redux 및 CSS에 대한 기본적인 이해가 필요합니다.
Sanity.io 시작하기
컴퓨터에서 Sanity를 사용하려면 Sanity CLI 도구를 설치해야 합니다. 이것은 프로젝트에 로컬로 설치할 수 있지만 향후 모든 응용 프로그램에서 액세스할 수 있도록 전역적으로 설치하는 것이 좋습니다.
이렇게 하려면 터미널에 다음 명령을 입력합니다.
npm install -g @sanity/cli
위 명령의 -g
플래그는 전역 설치를 활성화합니다.
다음으로 애플리케이션에서 Sanity를 초기화해야 합니다. 이것은 별도의 프로젝트로 설치할 수 있지만 일반적으로 프론트엔드 앱(이 경우 React) 내에 설치하는 것이 좋습니다.
Kapehe는 블로그에서 Sanity를 React와 통합하는 방법을 자세히 설명했습니다. 이 튜토리얼을 계속하기 전에 기사를 살펴보는 것이 도움이 될 것입니다.
다음 명령을 입력하여 React 앱에서 Sanity를 초기화합니다.
sanity init
Sanity CLI 도구를 설치하면 sanity
명령을 사용할 수 있습니다. 터미널에 sanity
또는 sanity help
를 입력하여 사용 가능한 Sanity 명령 목록을 볼 수 있습니다.
프로젝트를 설정하거나 초기화할 때 프롬프트에 따라 사용자 지정해야 합니다. 또한 데이터 세트를 생성해야 하며 데이터로 채워진 사용자 지정 데이터 세트를 선택할 수도 있습니다. 이 목록 앱의 경우 Sanity의 사용자 지정 공상 과학 영화 데이터 세트를 사용합니다. 이렇게 하면 데이터를 직접 입력하지 않아도 됩니다.
데이터세트를 보고 편집하려면 터미널의 Sanity 하위 cd
로 이동하고 sanity start
를 입력합니다. 이것은 일반적으로 https://localhost:3333/
에서 실행됩니다. 인터페이스에 액세스하려면 로그인해야 할 수 있습니다(프로젝트 초기화 시 사용한 것과 동일한 계정으로 로그인해야 함). 아래는 환경 스크린샷입니다.
Sanity-React 양방향 통신
Sanity와 React는 완전한 기능의 애플리케이션을 위해 서로 통신해야 합니다.
Sanity Manager의 CORS Origins 설정
먼저 React 앱을 Sanity에 연결합니다. 이렇게 하려면 https://manage.sanity.io/
에 로그인하고 Settings
탭의 API Settings
에서 CORS origins
를 찾습니다. 여기에서 프론트엔드 오리진을 Sanity 백엔드에 연결해야 합니다. React 앱은 기본적으로 https://localhost:3000/
에서 실행되므로 CORS에 추가해야 합니다.
이것은 아래 그림에 나와 있습니다.
반응에 온전함 연결하기
Sanity는 생성하는 모든 프로젝트에 project ID
를 연결합니다. 이 ID는 프런트엔드 애플리케이션에 연결할 때 필요합니다. Sanity Manager에서 프로젝트 ID를 찾을 수 있습니다.
백엔드는 sanity client
로 알려진 라이브러리를 사용하여 React와 통신합니다. 다음 명령을 입력하여 Sanity 프로젝트에 이 라이브러리를 설치해야 합니다.
npm install @sanity/client
프로젝트 src
폴더에 sanitySetup.js
파일(파일 이름은 중요하지 않음)을 만들고 다음 React 코드를 입력하여 Sanity와 React 간의 연결을 설정합니다.
import sanityClient from "@sanity/client" export default sanityClient({ projectId: PROJECT_ID, dataset: DATASET_NAME, useCdn: true });
우리는 projectId
, dataset name
및 부울 useCdn
을 @sanity/client
에서 가져온 온전한 클라이언트의 인스턴스에 전달했습니다. 이것은 마술처럼 작동하고 우리 앱을 백엔드에 연결합니다.
양방향 연결을 완료했으므로 이제 바로 프로젝트를 빌드해 보겠습니다.
Redux를 앱에 설정하고 연결하기
React 앱에서 Redux를 사용하려면 몇 가지 종속성이 필요합니다. React 환경에서 터미널을 열고 다음 bash 명령을 입력합니다.
npm install redux react-redux redux-thunk
Redux는 React와 같은 대부분의 프론트엔드 프레임워크 및 라이브러리와 함께 사용할 수 있는 글로벌 상태 관리 라이브러리입니다. 그러나 Redux 저장소 와 React 애플리케이션 간의 통신을 가능하게 하는 중개 도구 react-redux
가 필요합니다. Redux 썽크는 Redux 에서 작업 개체 대신 함수를 반환하는 데 도움이 됩니다.
전체 Redux 워크플로를 하나의 파일에 작성할 수 있지만 종종 문제를 분리하는 것이 더 깔끔하고 더 좋습니다. 이를 위해 우리는 우리의 작업 흐름을 세 개의 파일, 즉 actions
, reducers
, 그리고 store
로 나눌 것입니다. 그러나, 우리는 또한 action types
을 저장하기 위해 별도의 파일이 필요합니다. constants
라고도 합니다.
스토어 설정
저장소는 Redux에서 가장 중요한 파일입니다. 상태를 구성하고 패키징하여 React 애플리케이션에 전달합니다.
다음은 Redux 워크플로를 연결하는 데 필요한 Redux 저장소의 초기 설정입니다.
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducers from "./reducers/"; export default createStore( reducers, applyMiddleware(thunk) );
이 파일의 createStore
함수는 reducer
(필수), 초기 상태 및 향상기(보통 미들웨어, 이 경우 applyMiddleware
를 통해 제공되는 thunk
)의 세 가지 매개변수를 사용합니다. 감속기는 reducers
폴더에 저장되며 reducers
폴더의 index.js
파일로 결합하여 내보냅니다. 이것은 위의 코드에서 가져온 파일입니다. 나중에 이 파일을 다시 살펴보겠습니다.
Sanity의 GROQ 언어 소개
Sanity는 GROQ를 도입하여 JSON 데이터 쿼리를 한 단계 더 발전시켰습니다. GROQ는 Graph-Relational Object Queries의 약자입니다. Sanity.io에 따르면 GROQ는 스키마가 거의 없는 JSON 문서 컬렉션을 쿼리하도록 설계된 선언적 쿼리 언어입니다.
Sanity는 개발자가 언어에 익숙해질 수 있도록 GROQ Playground 도 제공합니다. 그러나 놀이터에 액세스하려면 새너티 비전 을 설치해야 합니다. 터미널에서 sanity install @sanity/vision
을 실행하여 설치합니다.
GROQ는 GraphQL과 유사한 구문을 가지고 있지만 더 간결하고 읽기 쉽습니다. 또한 GraphQL과 달리 GROQ를 사용하여 JSON 데이터를 쿼리할 수 있습니다.
예를 들어, 영화 문서의 모든 항목을 검색하기 위해 다음 GROQ 구문을 사용합니다.
*[_type == "movie"]
그러나 영화 문서에서 _ids
와 crewMembers
만 검색하려는 경우. 다음과 같이 해당 필드를 지정해야 합니다.
`*[_type == 'movie']{ _id, crewMembers }
여기에서 *
를 사용하여 GROQ에 _type
영화의 모든 문서를 원한다고 알렸습니다. _type
은 영화 컬렉션 아래의 속성입니다. 다음과 같이 _id
및 crewMembers
처럼 유형을 반환할 수도 있습니다.
*[_type == 'movie']{ _id, _type, crewMembers }
Redux 작업에서 GROQ를 구현하여 더 많은 작업을 할 것이지만 GROQ에 대한 자세한 내용은 Sanity.io의 설명서를 참조하세요. GROQ 쿼리 치트 시트는 쿼리 언어를 마스터하는 데 도움이 되는 많은 예제를 제공합니다.
상수 설정
Redux 워크플로의 모든 단계에서 작업 유형을 추적하려면 상수가 필요합니다. 상수는 각 시점에 전달된 작업 유형을 결정하는 데 도움이 됩니다. 예를 들어, API가 언제 로드되고, 완전히 로드되고, 오류가 발생하는지 추적할 수 있습니다.
별도의 파일에 상수를 정의할 필요는 없지만 단순성과 명확성을 위해 이것은 일반적으로 Redux에서 가장 좋은 방법입니다.
관습적으로 자바스크립트의 상수는 대문자로 정의됩니다. 여기에서 모범 사례를 따라 상수를 정의합니다. 다음은 이동 영화 가져오기 요청을 나타내는 상수의 예입니다.
export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST";
여기에서 MOVIE_FETCH_REQUEST
의 작업 유형을 나타내는 상수 MOVIE_FETCH_REQUEST
를 만들었습니다. 이렇게 하면 strings
을 사용하지 않고 이 작업 유형을 쉽게 호출하고 버그를 피할 수 있습니다. 또한 프로젝트의 어느 곳에서나 사용할 수 있도록 상수를 내보냈습니다.
마찬가지로 요청이 성공하거나 실패할 때를 나타내는 작업 유형을 가져오기 위해 다른 상수를 만들 수 있습니다. movieConstants.js
의 전체 코드는 아래 코드에 나와 있습니다.
export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST"; export const MOVIE_FETCH_SUCCESS = "MOVIE_FETCH_SUCCESS"; export const MOVIE_FETCH_FAIL = "MOVIE_FETCH_FAIL"; export const MOVIES_FETCH_REQUEST = "MOVIES_FETCH_REQUEST"; export const MOVIES_FETCH_SUCCESS = "MOVIES_FETCH_SUCCESS"; export const MOVIES_FETCH_FAIL = "MOVIES_FETCH_FAIL"; export const MOVIES_FETCH_RESET = "MOVIES_FETCH_RESET"; export const MOVIES_REF_FETCH_REQUEST = "MOVIES_REF_FETCH_REQUEST"; export const MOVIES_REF_FETCH_SUCCESS = "MOVIES_REF_FETCH_SUCCESS"; export const MOVIES_REF_FETCH_FAIL = "MOVIES_REF_FETCH_FAIL"; export const MOVIES_SORT_REQUEST = "MOVIES_SORT_REQUEST"; export const MOVIES_SORT_SUCCESS = "MOVIES_SORT_SUCCESS"; export const MOVIES_SORT_FAIL = "MOVIES_SORT_FAIL"; export const MOVIES_MOST_POPULAR_REQUEST = "MOVIES_MOST_POPULAR_REQUEST"; export const MOVIES_MOST_POPULAR_SUCCESS = "MOVIES_MOST_POPULAR_SUCCESS"; export const MOVIES_MOST_POPULAR_FAIL = "MOVIES_MOST_POPULAR_FAIL";
여기에서 영화나 영화 목록을 가져오고 가장 인기 있는 영화를 정렬하고 가져오기 위한 몇 가지 상수를 정의했습니다. 요청이 loading
될 때, successful
및 failed
할 때를 결정하기 위해 상수를 설정했음을 주목하십시오.
마찬가지로 personConstants.js
파일은 아래와 같습니다.
export const PERSONS_FETCH_REQUEST = "PERSONS_FETCH_REQUEST"; export const PERSONS_FETCH_SUCCESS = "PERSONS_FETCH_SUCCESS"; export const PERSONS_FETCH_FAIL = "PERSONS_FETCH_FAIL"; export const PERSON_FETCH_REQUEST = "PERSON_FETCH_REQUEST"; export const PERSON_FETCH_SUCCESS = "PERSON_FETCH_SUCCESS"; export const PERSON_FETCH_FAIL = "PERSON_FETCH_FAIL"; export const PERSONS_COUNT = "PERSONS_COUNT";
movieConstants.js
와 마찬가지로 사람을 가져오기 위한 상수 목록을 설정합니다. 우리는 또한 사람을 세는 상수를 설정했습니다. 상수는 movieConstants.js
에 대해 설명된 규칙을 따르며 애플리케이션의 다른 부분에서 액세스할 수 있도록 내보냈습니다.
마지막으로 앱에서 밝은 모드와 어두운 모드를 구현하므로 또 다른 상수 파일 globalConstants.js
가 있습니다. 살펴보겠습니다.
export const SET_LIGHT_THEME = "SET_LIGHT_THEME"; export const SET_DARK_THEME = "SET_DARK_THEME";
여기에서 상수를 설정하여 라이트 모드 또는 다크 모드가 디스패치되는 시기를 결정합니다. SET_LIGHT_THEME
는 사용자가 밝은 테마로 전환하는 시기를 결정하고 SET_DARK_THEME
는 어두운 테마가 선택되는 시기를 결정합니다. 또한 표시된 대로 상수를 내보냈습니다.
작업 설정
규칙에 따라 작업은 별도의 폴더에 저장됩니다. 작업은 유형에 따라 그룹화됩니다. 예를 들어 영화 작업은 movieActions.js
에 저장되고 사람 작업은 personActions.js
파일에 저장됩니다.
또한 테마를 밝은 모드에서 어두운 모드로 전환하는 작업을 처리하는 globalActions.js
가 있습니다.
moviesActions.js
의 모든 영화를 가져오자.
import sanityAPI from "../../sanitySetup"; import { MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS } from "../constants/movieConstants"; const fetchAllMovies = () => async (dispatch) => { try { dispatch({ type: MOVIES_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster": poster.asset->url, } ` ); dispatch({ type: MOVIES_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_FETCH_FAIL, payload: error.message }); } };
React를 Sanity 백엔드에 연결하기 위해 sanitySetup.js
파일을 생성했을 때를 기억하십니까? 여기에서 GROQ를 사용하여 온전한 백엔드를 쿼리할 수 있도록 설정을 가져왔습니다. 또한 constants
폴더의 movieConstants.js
파일에서 내보낸 몇 가지 상수를 가져왔습니다.
다음으로 컬렉션의 모든 영화를 가져오기 위한 fetchAllMovies
액션 함수를 만들었습니다. 대부분의 전통적인 React 애플리케이션은 axios
또는 fetch
를 사용하여 백엔드에서 데이터를 가져옵니다. 그러나 여기에서 이것들을 사용할 수 있지만 Sanity의 GROQ
를 사용하고 있습니다. GROQ
모드로 들어가려면 위의 코드와 같이 sanityAPI.fetch()
함수를 호출해야 합니다. 여기서 sanityAPI
는 이전에 설정한 React-Sanity 연결입니다. 이것은 Promise
를 반환하므로 비동기식으로 호출해야 합니다. 여기서는 async-await
구문을 사용했지만 .then
구문을 사용할 수도 있습니다.
응용 프로그램에서 thunk
를 사용하고 있으므로 작업 개체 대신 함수를 반환할 수 있습니다. 그러나 우리는 return 문을 한 줄로 전달하기로 결정했습니다.
const fetchAllMovies = () => async (dispatch) => { ... }
다음과 같이 함수를 작성할 수도 있습니다.
const fetchAllMovies = () => { return async (dispatch)=>{ ... } }
일반적으로 모든 영화를 가져오기 위해 먼저 요청이 아직 로드 중일 때 추적하는 작업 유형을 전달했습니다. 그런 다음 Sanity의 GROQ 구문을 사용하여 영화 문서를 비동기적으로 쿼리했습니다. 영화 데이터의 _id
및 포스터 url을 검색했습니다. 그런 다음 API에서 가져온 데이터가 포함된 페이로드를 반환했습니다.
마찬가지로 _id
로 영화를 검색하고 영화를 정렬하고 가장 인기 있는 영화를 가져올 수 있습니다.
특정 인물의 참조와 일치하는 영화를 가져올 수도 있습니다. fetchMoviesByRef
함수에서 이 작업을 수행했습니다.
const fetchMoviesByRef = (ref) => async (dispatch) => { try { dispatch({ type: MOVIES_REF_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && (castMembers[person._ref match '${ref}'] || crewMembers[person._ref match '${ref}']) ]{ _id, "poster" : poster.asset->url, title } ` ); dispatch({ type: MOVIES_REF_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_REF_FETCH_FAIL, payload: error.message }); } };
이 함수는 인수를 취하여 person._ref
또는 crewMembers
의 castMembers
가 전달된 인수와 일치하는지 확인합니다. 우리는 영화 _id
, poster url
및 title
함께 반환합니다. 또한 MOVIES_REF_FETCH_SUCCESS
유형의 작업을 전달하여 반환된 데이터의 페이로드를 첨부하고 오류가 발생하면 try-catch
래퍼 덕분에 오류 메시지의 페이로드를 첨부하는 MOVIE_REF_FETCH_FAIL
유형의 작업을 전달합니다.
fetchMovieById
함수에서 GROQ
를 사용하여 함수에 전달된 특정 id
와 일치하는 영화를 검색했습니다.
함수에 대한 GROQ
구문은 아래와 같습니다.
const data = await sanityAPI.fetch( `*[_type == 'movie' && _id == '${id}']{ _id, "cast" : castMembers[]{ "ref": person._ref, characterName, "name": person->name, "image": person->image.asset->url } , "crew" : crewMembers[]{ "ref": person._ref, department, job, "name": person->name, "image": person->image.asset->url } , "overview": { "text": overview[0].children[0].text }, popularity, "poster" : poster.asset->url, releaseDate, title }[0]` );
fetchAllMovies
작업과 마찬가지로 movie
유형의 모든 문서를 선택하는 것으로 시작했지만 더 나아가 함수에 제공된 ID를 가진 문서만 선택했습니다. 영화에 대한 많은 세부 정보를 표시할 계획이므로 검색할 속성을 지정했습니다.
우리는 영화 id
와 castMembers
배열에서 ref
, characterName
, 사람 이름 및 사람 이미지와 같은 몇 가지 속성을 검색했습니다. 또한 별칭을 castMembers
에서 cast
로 변경했습니다.
castMembers
처럼 우리는 ref
, department
, job
, 사람의 이름 및 사람의 이미지와 같은 crewMembers
배열에서 몇 가지 속성을 선택했습니다. 또한 별칭을 crewMembers
에서 crew
로 변경했습니다.
같은 방법으로 개요 텍스트, 인기도, 영화 포스터 URL, 영화 개봉일 및 제목을 선택했습니다.
Sanity의 GROQ 언어를 사용하면 문서를 정렬할 수도 있습니다. 항목을 정렬하기 위해 파이프 연산자 옆에 order 를 전달합니다.
예를 들어, releaseDate
를 기준으로 영화를 오름차순으로 정렬하려면 다음을 수행할 수 있습니다.
const data = await sanityAPI.fetch( `*[_type == 'movie']{ ... } | order(releaseDate, asc)` );
sortMoviesBy
함수에서 이 개념을 사용하여 오름차순 또는 내림차순으로 정렬했습니다.
아래에서 이 기능을 살펴보자.
const sortMoviesBy = (item, type) => async (dispatch) => { try { dispatch({ type: MOVIES_SORT_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, title } | order( ${item} ${type})` ); dispatch({ type: MOVIES_SORT_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_SORT_FAIL, payload: error.message }); } };
요청이 로드되는 시점을 결정하기 위해 MOVIES_SORT_REQUEST
유형의 작업을 발송하는 것으로 시작했습니다. 그런 다음 GROQ
구문을 사용하여 movie
컬렉션에서 데이터를 정렬하고 가져왔습니다. 정렬 기준이 되는 항목은 item
변수에 제공되고 정렬 모드(오름차순 또는 내림차순)는 변수 type
에 제공됩니다. 결과적으로 id
, 포스터 url 및 제목을 반환했습니다. 데이터가 반환되면 MOVIES_SORT_FAIL
유형의 작업을 전달하고 실패하면 MOVIES_SORT_SUCCESS
유형의 작업을 전달합니다.
유사한 GROQ
개념이 getMostPopular
함수에 적용됩니다. GROQ
구문은 아래와 같습니다.
const data = await sanityAPI.fetch( ` *[_type == 'movie']{ _id, "overview": { "text": overview[0].children[0].text }, "poster" : poster.asset->url, title }| order(popularity desc) [0..2]` );
여기서 유일한 차이점은 영화를 인기도에 따라 내림차순으로 정렬한 다음 처음 세 개만 선택했다는 것입니다. 항목은 0부터 시작하는 인덱스로 반환되므로 처음 세 항목은 항목 0, 1, 2입니다. 처음 10개 항목을 검색하려면 [0..9]
를 함수에 전달할 수 있습니다.
다음은 movieActions.js
파일에 있는 영화 작업에 대한 전체 코드입니다.
import sanityAPI from "../../sanitySetup"; import { MOVIE_FETCH_FAIL, MOVIE_FETCH_REQUEST, MOVIE_FETCH_SUCCESS, MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, MOVIES_SORT_REQUEST, MOVIES_SORT_SUCCESS, MOVIES_SORT_FAIL, MOVIES_MOST_POPULAR_REQUEST, MOVIES_MOST_POPULAR_SUCCESS, MOVIES_MOST_POPULAR_FAIL, MOVIES_REF_FETCH_SUCCESS, MOVIES_REF_FETCH_FAIL, MOVIES_REF_FETCH_REQUEST } from "../constants/movieConstants"; const fetchAllMovies = () => async (dispatch) => { try { dispatch({ type: MOVIES_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, } ` ); dispatch({ type: MOVIES_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_FETCH_FAIL, payload: error.message }); } }; const fetchMoviesByRef = (ref) => async (dispatch) => { try { dispatch({ type: MOVIES_REF_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && (castMembers[person._ref match '${ref}'] || crewMembers[person._ref match '${ref}']) ]{ _id, "poster" : poster.asset->url, title }` ); dispatch({ type: MOVIES_REF_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_REF_FETCH_FAIL, payload: error.message }); } }; const fetchMovieById = (id) => async (dispatch) => { try { dispatch({ type: MOVIE_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && _id == '${id}']{ _id, "cast" : castMembers[]{ "ref": person._ref, characterName, "name": person->name, "image": person->image.asset->url } , "crew" : crewMembers[]{ "ref": person._ref, department, job, "name": person->name, "image": person->image.asset->url } , "overview": { "text": overview[0].children[0].text }, popularity, "poster" : poster.asset->url, releaseDate, title }[0]` ); dispatch({ type: MOVIE_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIE_FETCH_FAIL, payload: error.message }); } }; const sortMoviesBy = (item, type) => async (dispatch) => { try { dispatch({ type: MOVIES_MOST_POPULAR_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, title } | order( ${item} ${type})` ); dispatch({ type: MOVIES_SORT_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_SORT_FAIL, payload: error.message }); } }; const getMostPopular = () => async (dispatch) => { try { dispatch({ type: MOVIES_SORT_REQUEST }); const data = await sanityAPI.fetch( ` *[_type == 'movie']{ _id, "overview": { "text": overview[0].children[0].text }, "poster" : poster.asset->url, title }| order(popularity desc) [0..2]` ); dispatch({ type: MOVIES_MOST_POPULAR_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_MOST_POPULAR_FAIL, payload: error.message }); } }; export { fetchAllMovies, fetchMovieById, sortMoviesBy, getMostPopular, fetchMoviesByRef };
감속기 설정
리듀서는 Redux에서 가장 중요한 개념 중 하나입니다. 그들은 이전 상태를 취하고 상태 변경을 결정합니다.
일반적으로 switch 문을 사용하여 각 작업 유형에 대한 조건을 실행합니다. 예를 들어 작업 유형이 로딩을 나타낼 때 loading
을 반환하고 성공 또는 오류를 나타낼 때 페이로드를 반환할 수 있습니다. initial state
와 action
를 인수로 취할 것으로 예상됩니다.
우리의 movieReducers.js
파일에는 movieActions.js
파일에 정의된 작업과 일치하는 다양한 감속기가 포함되어 있습니다. 그러나 각 리듀서는 유사한 구문과 구조를 가지고 있습니다. 유일한 차이점은 호출하는 constants
와 반환하는 값입니다.
먼저 fetchAllMoviesReducer
파일에서 movieReducers.js
를 살펴보겠습니다.
import { MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, } from "../constants/movieConstants"; const fetchAllMoviesReducer = (state = {}, action) => { switch (action.type) { case MOVIES_FETCH_REQUEST: return { loading: true }; case MOVIES_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_FETCH_FAIL: return { loading: false, error: action.payload }; case MOVIES_FETCH_RESET: return {}; default: return state; } };
모든 리듀서와 마찬가지로 fetchAllMoviesReducer
는 초기 상태 객체( state
)와 action
객체를 인수로 취합니다. switch 문을 사용하여 각 시점의 작업 유형을 확인했습니다. MOVIES_FETCH_REQUEST
에 해당하는 경우 loading을 true로 반환하여 사용자에게 로딩 표시기를 표시할 수 있습니다.
MOVIES_FETCH_SUCCESS
에 해당하면 로딩 표시기를 끈 다음 가변 movies
에 액션 페이로드를 반환합니다. 그러나 MOVIES_FETCH_FAIL
이면 로딩도 끄고 오류를 반환합니다. 우리는 또한 영화를 재설정하는 옵션을 원합니다. 이렇게 하면 필요할 때 상태를 지울 수 있습니다.
우리는 다른 감속기에 대해 동일한 구조를 가지고 있습니다. 전체 movieReducers.js
가 아래에 나와 있습니다.
import { MOVIE_FETCH_FAIL, MOVIE_FETCH_REQUEST, MOVIE_FETCH_SUCCESS, MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, MOVIES_SORT_REQUEST, MOVIES_SORT_SUCCESS, MOVIES_SORT_FAIL, MOVIES_MOST_POPULAR_REQUEST, MOVIES_MOST_POPULAR_SUCCESS, MOVIES_MOST_POPULAR_FAIL, MOVIES_FETCH_RESET, MOVIES_REF_FETCH_REQUEST, MOVIES_REF_FETCH_SUCCESS, MOVIES_REF_FETCH_FAIL } from "../constants/movieConstants"; const fetchAllMoviesReducer = (state = {}, action) => { switch (action.type) { case MOVIES_FETCH_REQUEST: return { loading: true }; case MOVIES_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_FETCH_FAIL: return { loading: false, error: action.payload }; case MOVIES_FETCH_RESET: return {}; default: return state; } }; const fetchMoviesByRefReducer = (state = {}, action) => { switch (action.type) { case MOVIES_REF_FETCH_REQUEST: return { loading: true }; case MOVIES_REF_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_REF_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const fetchMovieByIdReducer = (state = {}, action) => { switch (action.type) { case MOVIE_FETCH_REQUEST: return { loading: true }; case MOVIE_FETCH_SUCCESS: return { loading: false, movie: action.payload }; case MOVIE_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const sortMoviesByReducer = (state = {}, action) => { switch (action.type) { case MOVIES_SORT_REQUEST: return { loading: true }; case MOVIES_SORT_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_SORT_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const getMostPopularReducer = (state = {}, action) => { switch (action.type) { case MOVIES_MOST_POPULAR_REQUEST: return { loading: true }; case MOVIES_MOST_POPULAR_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_MOST_POPULAR_FAIL: return { loading: false, error: action.payload }; default: return state; } }; export { fetchAllMoviesReducer, fetchMovieByIdReducer, sortMoviesByReducer, getMostPopularReducer, fetchMoviesByRefReducer };
우리는 또한 personReducers.js
에 대해 똑같은 구조를 따랐습니다. 예를 들어, fetchAllPersonsReducer
함수는 데이터베이스의 모든 사람을 가져오기 위한 상태를 정의합니다.
이것은 아래 코드에 나와 있습니다.
import { PERSONS_FETCH_FAIL, PERSONS_FETCH_REQUEST, PERSONS_FETCH_SUCCESS, } from "../constants/personConstants"; const fetchAllPersonsReducer = (state = {}, action) => { switch (action.type) { case PERSONS_FETCH_REQUEST: return { loading: true }; case PERSONS_FETCH_SUCCESS: return { loading: false, persons: action.payload }; case PERSONS_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } };
fetchAllPersonsReducer
와 마찬가지로 state
와 action
을 인수로 사용하여 fetchAllMoviesReducer
를 정의했습니다. 이것은 Redux 감속기의 표준 설정입니다. 그런 다음 switch 문을 사용하여 작업 유형을 확인하고 유형이 PERSONS_FETCH_REQUEST
인 경우 로드를 true로 반환합니다. PERSONS_FETCH_SUCCESS
이면 로드를 끄고 페이로드를 반환하고 PERSONS_FETCH_FAIL
이면 오류를 반환합니다.
감속기 결합
Redux의 combineReducers
기능을 사용하면 둘 이상의 감속기를 결합하여 저장소에 전달할 수 있습니다. reducers
폴더 내의 index.js
파일에 영화와 사람 리듀서를 결합할 것입니다.
살펴보겠습니다.
import { combineReducers } from "redux"; import { fetchAllMoviesReducer, fetchMovieByIdReducer, sortMoviesByReducer, getMostPopularReducer, fetchMoviesByRefReducer } from "./movieReducers"; import { fetchAllPersonsReducer, fetchPersonByIdReducer, countPersonsReducer } from "./personReducers"; import { toggleTheme } from "./globalReducers"; export default combineReducers({ fetchAllMoviesReducer, fetchMovieByIdReducer, fetchAllPersonsReducer, fetchPersonByIdReducer, sortMoviesByReducer, getMostPopularReducer, countPersonsReducer, fetchMoviesByRefReducer, toggleTheme });
여기에서 우리는 영화, 사람 및 전역 감속기 파일에서 모든 감속기를 가져와서 combineReducers
함수에 전달했습니다. combineReducers
함수는 모든 리듀서를 전달할 수 있는 객체를 취합니다. 프로세스의 인수에 별칭을 추가할 수도 있습니다.
우리는 나중에 globalReducers
에 대해 작업할 것입니다.
이제 Redux store.js
파일에서 감속기를 전달할 수 있습니다. 이것은 아래에 나와 있습니다.
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducers from "./reducers/index"; export default createStore(reducers, initialState, applyMiddleware(thunk));
Redux 워크플로를 설정했으면 React 애플리케이션을 설정해 보겠습니다.
React 애플리케이션 설정하기
우리의 반응 응용 프로그램은 영화와 해당 출연진 및 승무원을 나열합니다. 라우팅에는 react-router-dom
을 사용하고 앱 스타일에는 styled-components
를 사용합니다. 또한 아이콘 및 일부 UI 구성 요소에 Material UI를 사용할 것입니다.
종속성을 설치하려면 다음 bash
명령을 입력하십시오.
npm install react-router-dom @material-ui/core @material-ui/icons query-string
구축할 내용은 다음과 같습니다.
Redux를 React 앱에 연결하기
React-redux
에는 애플리케이션을 Redux 저장소에 연결할 수 있는 Provider 기능이 포함되어 있습니다. 이렇게 하려면 저장소의 인스턴스를 공급자에게 전달해야 합니다. index.js
또는 App.js
파일에서 이 작업을 수행할 수 있습니다.
다음은 index.js 파일입니다.
import React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; import { Provider } from "react-redux"; import store from "./redux/store"; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") );
여기에서는 react-redux
에서 Provider
를 가져오고 Redux 스토어에서 store
를 가져왔습니다. 그런 다음 저장소를 공급자로 전달하여 전체 구성 요소 트리를 래핑했습니다.
다음으로 React 애플리케이션에서 라우팅을 위한 react-router-dom
이 필요합니다. react-router-dom
은 경로와 경로를 정의하는 데 사용할 수 있는 BrowserRouter
, Switch
및 Route
와 함께 제공됩니다.
App.js
파일에서 이 작업을 수행합니다. 이것은 아래에 나와 있습니다.
import React from "react"; import Header from "./components/Header"; import Footer from "./components/Footer"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import MoviesList from "./pages/MoviesListPage"; import PersonsList from "./pages/PersonsListPage"; function App() { return ( <Router> <main className="contentwrap"> <Header /> <Switch> <Route path="/persons/"> <PersonsList /> </Route> <Route path="/" exact> <MoviesList /> </Route> </Switch> </main> <Footer /> </Router> ); } export default App;
이것은 react-router-dom으로 라우팅하기 위한 표준 설정입니다. 그들의 문서에서 확인할 수 있습니다. Header
, Footer
, PersonsList
및 MovieList
구성 요소를 가져왔습니다. 그런 다음 Router
와 Switch
에 모든 것을 래핑하여 react-router-dom
을 설정합니다.
페이지가 동일한 머리글과 바닥글을 공유하기를 원하기 때문에 Switch
로 구조를 래핑하기 전에 <Header />
및 <Footer />
구성 요소를 전달해야 했습니다. 우리는 또한 전체 애플리케이션을 래핑하기를 원하기 때문에 main
요소에 대해서도 유사한 작업을 수행했습니다.
react-router-dom
에서 Route
를 사용하여 각 구성 요소를 경로에 전달했습니다.
페이지 및 구성 요소 정의
우리의 응용 프로그램은 구조화된 방식으로 구성되어 있습니다. 재사용 가능한 구성 요소는 components
폴더에 저장되고 페이지는 pages
폴더에 저장됩니다.
우리 pages
는 movieListPage.js
, moviePage.js
, PersonListPage.js
및 PersonPage.js
로 구성되어 있습니다. MovieListPage.js
는 Sanity.io 백엔드의 모든 영화와 가장 인기 있는 영화를 나열합니다.
모든 영화를 나열하려면 우리의 movieAction.js
파일에 정의된 fetchAllMovies
작업을 dispatch
하기만 하면 됩니다. 페이지가 로드되는 즉시 목록을 가져와야 하므로 useEffect
에서 정의해야 합니다. 이것은 아래에 나와 있습니다.
import React, { useEffect } from "react"; import { fetchAllMovies } from "../redux/actions/movieActions"; import { useDispatch, useSelector } from "react-redux"; const MoviesListPage = () => { const dispatch = useDispatch(); useEffect(() => { dispatch(fetchAllMovies()); }, [dispatch]); const { loading, error, movies } = useSelector( (state) => state.fetchAllMoviesReducer ); return ( ... ) }; export default MoviesListPage;
useDispatch
및 useSelector
Hooks 덕분에 Redux 작업을 전달하고 Redux 저장소에서 적절한 상태를 선택할 수 있습니다. 상태 loading
, error
및 movies
는 Reducer 함수에 정의되었으며 여기에서는 React Redux의 useSelector
Hook을 사용하여 선택했습니다. 이러한 상태 즉, loading
, error
및 movies
는 fetchAllMovies()
작업을 전달한 즉시 사용할 수 있게 됩니다.
영화 목록을 가져오면 map
기능을 사용하거나 원하는 대로 애플리케이션에 표시할 수 있습니다.
다음은 moviesListPage.js
파일의 전체 코드입니다.
import React, {useState, useEffect} from 'react' import {fetchAllMovies, getMostPopular, sortMoviesBy} from "../redux/actions/movieActions" import {useDispatch, useSelector} from "react-redux" import Loader from "../components/BackdropLoader" import {MovieListContainer} from "../styles/MovieStyles.js" import SortIcon from '@material-ui/icons/Sort'; import SortModal from "../components/Modal" import {useLocation, Link} from "react-router-dom" import queryString from "query-string" import {MOVIES_FETCH_RESET} from "../redux/constants/movieConstants" const MoviesListPage = () => { const location = useLocation() const dispatch = useDispatch() const [openSort, setOpenSort] = useState(false) useEffect(()=>{ dispatch(getMostPopular()) const {order, type} = queryString.parse(location.search) if(order && type){ dispatch({ type: MOVIES_FETCH_RESET }) dispatch(sortMoviesBy(order, type)) }else{ dispatch(fetchAllMovies()) } }, [dispatch, location.search]) const {loading: popularLoading, error: popularError, movies: popularMovies } = useSelector(state => state.getMostPopularReducer) const { loading: moviesLoading, error: moviesError, movies } = useSelector(state => state.fetchAllMoviesReducer) const { loading: sortLoading, error: sortError, movies: sortMovies } = useSelector(state => state.sortMoviesByReducer) return ( <MovieListContainer> <div className="mostpopular"> { popularLoading ? <Loader /> : popularError ? popularError : popularMovies && popularMovies.map(movie => ( <Link to={`/movie?id=${movie._id}`} className="popular" key={movie._id} style={{backgroundImage: `url(${movie.poster})`}}> <div className="content"> <h2>{movie.title}</h2> <p>{movie.overview.text.substring(0, 50)}…</p> </div> </Link> )) } </div> <div className="moviespanel"> <div className="top"> <h2>All Movies</h2> <SortIcon onClick={()=> setOpenSort(true)} /> </div> <div className="movieslist"> { moviesLoading ? <Loader /> : moviesError ? moviesError : movies && movies.map(movie =>( <Link to={`/movie?id=${movie._id}`} key={movie._id}> <img className="movie" src={movie.poster} alt={movie.title} /> </Link> )) } { ( sortLoading ? !movies && <Loader /> : sortError ? sortError : sortMovies && sortMovies.map(movie =>( <Link to={`/movie?id=${movie._id}`} key={movie._id}> <img className="movie" src={movie.poster} alt={movie.title} /> </Link> )) ) } </div> </div> <SortModal open={openSort} setOpen={setOpenSort} /> </MovieListContainer> ) } export default MoviesListPage
useEffect
Hook에서 getMostPopular
영화 작업(이 작업은 가장 인기 있는 영화를 선택함)을 전달하는 것으로 시작했습니다. 이렇게 하면 페이지가 로드되는 즉시 가장 인기 있는 영화를 검색할 수 있습니다. 또한 사용자가 releaseDate
과 popularity
를 기준으로 영화를 정렬할 수 있습니다. 이것은 위의 코드에서 전달된 sortMoviesBy
액션에 의해 처리됩니다. 또한 쿼리 매개변수에 따라 fetchAllMovies
를 전달했습니다.
또한 useSelector
Hook을 사용하여 이러한 각 작업에 해당하는 감속기를 선택했습니다. 각 감속기에 대해 loading
, error
및 movies
상태를 선택했습니다.
리듀서에서 movies
을 가져온 후 이제 사용자에게 표시할 수 있습니다. 여기서는 ES6 map
기능을 사용하여 이를 수행했습니다. 각 영화 상태가 로드될 때마다 먼저 로더를 표시하고 오류가 있으면 오류 메시지를 표시합니다. 마지막으로 영화를 가져오면 map
기능을 사용하여 영화 이미지를 사용자에게 표시합니다. MovieListContainer
구성 요소에 전체 구성 요소를 래핑했습니다.
<MovieListContainer> … </MovieListContainer>
태그는 스타일이 지정된 구성 요소를 사용하여 정의된 div
입니다. 곧 간단히 살펴보겠습니다.
스타일이 지정된 구성 요소로 앱 스타일 지정
스타일이 지정된 구성 요소를 사용하면 페이지와 구성 요소의 스타일을 개별적으로 지정할 수 있습니다. 또한 inheritance
, Theming
, passing of props
등과 같은 몇 가지 흥미로운 기능을 제공합니다.
우리는 항상 개별적으로 페이지의 스타일을 지정하고 싶지만 때로는 전역 스타일이 바람직할 수 있습니다. 흥미롭게도 styled-components는 createGlobalStyle
함수 덕분에 그렇게 하는 방법을 제공합니다.
응용 프로그램에서 styled-components를 사용하려면 설치해야 합니다. 반응 프로젝트에서 터미널을 열고 다음 bash
명령을 입력합니다.
npm install styled-components
styled-components를 설치했으면 글로벌 스타일을 시작하겠습니다.
src
디렉토리에 styles
라는 별도의 폴더를 만들어 보겠습니다. 이것은 우리의 모든 스타일을 저장할 것입니다. 또한 스타일 폴더 내에 globalStyles.js
파일을 생성해 보겠습니다. styled-components에서 전역 스타일을 생성하려면 createGlobalStyle
을 가져와야 합니다.
import { createGlobalStyle } from "styled-components";
그런 다음 다음과 같이 스타일을 정의할 수 있습니다.
export const GlobalStyle = createGlobalStyle` ... `
스타일이 지정된 구성 요소는 템플릿 리터럴을 사용하여 소품을 정의합니다. 이 리터럴 내에서 전통적인 CSS
코드를 작성할 수 있습니다.
우리는 또한 definition.js
라는 파일에 정의된 deviceWidth
를 가져왔습니다. deviceWidth
는 미디어 쿼리를 설정하기 위한 중단점 정의를 보유합니다.
import { deviceWidth } from "./definition";
애플리케이션의 흐름을 제어하기 위해 오버플로를 숨김으로 설정했습니다.
html, body{ overflow-x: hidden; }
또한 .header
스타일 선택기를 사용하여 헤더 스타일을 정의했습니다.
.header{ z-index: 5; background-color: ${(props)=>props.theme.midDarkBlue}; display:flex; align-items:center; padding: 0 20px; height:50px; justify-content:space-between; position:fixed; top:0; width:100%; @media ${deviceWidth.laptop_lg} { width:97%; } ... }
여기에 배경색, z-index, padding 및 기타 많은 기존 CSS 속성과 같은 다양한 스타일이 정의됩니다.
styled-components props
을 사용하여 배경색을 설정했습니다. 이를 통해 구성 요소에서 전달할 수 있는 동적 변수를 설정할 수 있습니다. 또한 테마 토글링을 최대한 활용할 수 있도록 테마 변수도 전달했습니다.
스타일 구성 요소의 ThemeProvider
로 전체 애플리케이션을 래핑했기 때문에 여기에서 테마 지정이 가능합니다. 잠시 후에 이에 대해 이야기하겠습니다. 또한 CSS flexbox
를 사용하여 헤더의 스타일을 적절하게 지정하고 위치를 fixed
로 설정하여 브라우저에 대해 고정된 상태를 유지하도록 했습니다. 또한 헤더를 모바일 친화적으로 만들기 위해 중단점을 정의했습니다.
다음은 globalStyles.js
파일의 전체 코드입니다.
import { createGlobalStyle } from "styled-components"; import { deviceWidth } from "./definition"; export const GlobalStyle = createGlobalStyle` html{ overflow-x: hidden; } body{ background-color: ${(props) => props.theme.lighter}; overflow-x: hidden; min-height: 100vh; display: grid; grid-template-rows: auto 1fr auto; } #root{ display: grid; flex-direction: column; } h1,h2,h3, label{ font-family: 'Aclonica', sans-serif; } h1, h2, h3, p, span:not(.MuiIconButton-label), div:not(.PrivateRadioButtonIcon-root-8), div:not(.tryingthis){ color: ${(props) => props.theme.bodyText} } p, span, div, input{ font-family: 'Jost', sans-serif; } .paginate button{ color: ${(props) => props.theme.bodyText} } .header{ z-index: 5; background-color: ${(props) => props.theme.midDarkBlue}; display: flex; align-items: center; padding: 0 20px; height: 50px; justify-content: space-between; position: fixed; top: 0; width: 100%; @media ${deviceWidth.laptop_lg}{ width: 97%; } @media ${deviceWidth.tablet}{ width: 100%; justify-content: space-around; } a{ text-decoration: none; } label{ cursor: pointer; color: ${(props) => props.theme.goldish}; font-size: 1.5rem; } .hamburger{ cursor: pointer; color: ${(props) => props.theme.white}; @media ${deviceWidth.desktop}{ display: none; } @media ${deviceWidth.tablet}{ display: block; } } } .mobileHeader{ z-index: 5; background-color: ${(props) => props.theme.darkBlue}; color: ${(props) => props.theme.white}; display: grid; place-items: center; width: 100%; @media ${deviceWidth.tablet}{ width: 100%; } height: calc(100% - 50px); transition: all 0.5s ease-in-out; position: fixed; right: 0; top: 50px; .menuitems{ display: flex; box-shadow: 0 0 5px ${(props) => props.theme.lightshadowtheme}; flex-direction: column; align-items: center; justify-content: space-around; height: 60%; width: 40%; a{ display: flex; flex-direction: column; align-items:center; cursor: pointer; color: ${(props) => props.theme.white}; text-decoration: none; &:hover{ border-bottom: 2px solid ${(props) => props.theme.goldish}; .MuiSvgIcon-root{ color: ${(props) => props.theme.lightred} } } } } } footer{ min-height: 30px; margin-top: auto; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 0.875rem; background-color: ${(props) => props.theme.midDarkBlue}; color: ${(props) => props.theme.white}; } `;
리터럴 내에서 순수 CSS 코드를 작성했지만 몇 가지 예외가 있습니다. Styled-components를 사용하면 소품을 전달할 수 있습니다. 문서에서 이에 대해 자세히 알아볼 수 있습니다.
전역 스타일을 정의하는 것 외에도 개별 페이지에 대한 스타일을 정의할 수 있습니다.
예를 들어 다음은 styles
폴더의 PersonStyle.js
에 정의된 PersonListPage.js
의 스타일입니다.
import styled from "styled-components"; import { deviceWidth, colors } from "./definition"; export const PersonsListContainer = styled.div` margin: 50px 80px; @media ${deviceWidth.tablet} { margin: 50px 10px; } a { text-decoration: none; } .top { display: flex; justify-content: flex-end; padding: 5px; .MuiSvgIcon-root { cursor: pointer; &:hover { color: ${colors.darkred}; } } } .personslist { margin-top: 20px; display: grid; place-items: center; grid-template-columns: repeat(5, 1fr); @media ${deviceWidth.laptop} { grid-template-columns: repeat(4, 1fr); } @media ${deviceWidth.tablet} { grid-template-columns: repeat(3, 1fr); } @media ${deviceWidth.tablet_md} { grid-template-columns: repeat(2, 1fr); } @media ${deviceWidth.mobile_lg} { grid-template-columns: repeat(1, 1fr); } grid-gap: 30px; .person { width: 200px; position: relative; img { width: 100%; } .content { position: absolute; bottom: 0; left: 8px; border-right: 2px solid ${colors.goldish}; border-left: 2px solid ${colors.goldish}; border-radius: 10px; width: 80%; margin: 20px auto; padding: 8px 10px; background-color: ${colors.transparentWhite}; color: ${colors.darkBlue}; h2 { font-size: 1.2rem; } } } } `;
먼저 styled
styled-components
에서 style을 가져오고 definition
파일에서 deviceWidth
를 가져왔습니다. 그런 다음 PersonsListContainer
를 스타일을 유지하기 위한 div
로 정의했습니다. 미디어 쿼리와 설정된 중단점을 활용하여 다양한 중단점을 설정하여 페이지를 모바일 친화적으로 만들었습니다.
여기에서는 크고 작은 화면과 매우 큰 화면에 대해 표준 브라우저 중단점만 사용했습니다. 또한 CSS flexbox와 그리드를 최대한 활용하여 페이지에 콘텐츠를 적절하게 스타일 지정하고 표시합니다.
PersonListPage.js
파일에서 이 스타일을 사용하기 위해 우리는 단순히 그것을 가져와 다음과 같이 페이지에 추가했습니다.
import React from "react"; const PersonsListPage = () => { return ( <PersonsListContainer> ... </PersonsListContainer> ); }; export default PersonsListPage;
래퍼는 스타일에서 div로 정의했기 때문에 div
를 출력합니다.
테마 추가 및 마무리
애플리케이션에 테마를 추가하는 것은 항상 멋진 기능입니다. 이를 위해서는 다음이 필요합니다.
- 별도의 파일(이 경우
definition.js
파일)에 정의된 사용자 정의 테마. - Redux 액션과 리듀서에 정의된 로직.
- 애플리케이션에서 테마를 호출하고 구성 요소 트리를 통해 전달합니다.
이것을 확인합시다.
다음은 definition.js
파일의 theme
객체입니다.
export const theme = { light: { dark: "#0B0C10", darkBlue: "#253858", midDarkBlue: "#42526e", lightBlue: "#0065ff", normal: "#dcdcdd", lighter: "#F4F5F7", white: "#FFFFFF", darkred: "#E85A4F", lightred: "#E98074", goldish: "#FFC400", bodyText: "#0B0C10", lightshadowtheme: "rgba(0, 0, 0, 0.1)" }, dark: { dark: "white", darkBlue: "#06090F", midDarkBlue: "#161B22", normal: "#dcdcdd", lighter: "#06090F", white: "white", darkred: "#E85A4F", lightred: "#E98074", goldish: "#FFC400", bodyText: "white", lightshadowtheme: "rgba(255, 255, 255, 0.9)" } };
밝고 어두운 테마에 다양한 색상 속성을 추가했습니다. 밝은 모드와 어두운 모드 모두에서 가시성을 확보할 수 있도록 색상이 신중하게 선택되었습니다. 원하는 대로 테마를 정의할 수 있습니다. 이것은 어렵고 빠른 규칙이 아닙니다.
다음으로 Redux에 기능을 추가해 보겠습니다.
Redux 작업 폴더에 globalActions.js
를 만들고 다음 코드를 추가했습니다.
import { SET_DARK_THEME, SET_LIGHT_THEME } from "../constants/globalConstants"; import { theme } from "../../styles/definition"; export const switchToLightTheme = () => (dispatch) => { dispatch({ type: SET_LIGHT_THEME, payload: theme.light }); localStorage.setItem("theme", JSON.stringify(theme.light)); localStorage.setItem("light", JSON.stringify(true)); }; export const switchToDarkTheme = () => (dispatch) => { dispatch({ type: SET_DARK_THEME, payload: theme.dark }); localStorage.setItem("theme", JSON.stringify(theme.dark)); localStorage.setItem("light", JSON.stringify(false)); };
여기서는 단순히 정의된 테마를 가져왔습니다. 필요한 테마의 페이로드를 전달하여 해당 작업을 전달했습니다. 페이로드 결과는 밝은 테마와 어두운 테마 모두에 대해 동일한 키를 사용하여 로컬 저장소에 저장됩니다. 이를 통해 브라우저에서 상태를 유지할 수 있습니다.
또한 테마에 대한 감속기를 정의해야 합니다.
import { SET_DARK_THEME, SET_LIGHT_THEME } from "../constants/globalConstants"; export const toggleTheme = (state = {}, action) => { switch (action.type) { case SET_LIGHT_THEME: return { theme: action.payload, light: true }; case SET_DARK_THEME: return { theme: action.payload, light: false }; default: return state; } };
이것은 우리가 하고 있는 것과 매우 유사합니다. switch
문을 사용하여 작업 유형을 확인한 다음 적절한 payload
를 반환했습니다. 또한 사용자가 밝은 테마 또는 어두운 테마를 선택했는지 여부를 결정하는 상태 light
을 반환했습니다. 우리는 이것을 컴포넌트에서 사용할 것입니다.
또한 루트 리듀서 및 저장소에 추가해야 합니다. 다음은 store.js
의 전체 코드입니다.
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import { theme as initialTheme } from "../styles/definition"; import reducers from "./reducers/index"; const theme = localStorage.getItem("theme") ? JSON.parse(localStorage.getItem("theme")) : initialTheme.light; const light = localStorage.getItem("light") ? JSON.parse(localStorage.getItem("light")) : true; const initialState = { toggleTheme: { light, theme } }; export default createStore(reducers, initialState, applyMiddleware(thunk));
사용자가 새로 고칠 때 테마를 유지해야 했기 때문에 localStorage.getItem()
을 사용하여 로컬 저장소에서 테마를 가져와 초기 상태로 전달해야 했습니다.
React 애플리케이션에 기능 추가하기
스타일이 지정된 구성 요소는 응용 프로그램을 통해 테마를 전달할 수 있는 ThemeProvider
를 제공합니다. 이 기능을 추가하기 위해 App.js 파일을 수정할 수 있습니다.
살펴보겠습니다.
import React from "react"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import { useSelector } from "react-redux"; import { ThemeProvider } from "styled-components"; function App() { const { theme } = useSelector((state) => state.toggleTheme); let Theme = theme ? theme : {}; return ( <ThemeProvider theme={Theme}> <Router> ... </Router> </ThemeProvider> ); } export default App;
ThemeProvider
를 통해 테마를 전달하면 스타일에서 테마 소품을 쉽게 사용할 수 있습니다.
예를 들어 다음과 같이 bodyText
사용자 정의 색상으로 색상을 설정할 수 있습니다.
color: ${(props) => props.theme.bodyText};
애플리케이션에서 색상이 필요한 모든 곳에서 사용자 정의 테마를 사용할 수 있습니다.
예를 들어 border-bottom
을 정의하려면 다음을 수행합니다.
border-bottom: 2px solid ${(props) => props.theme.goldish};
결론
우리는 Sanity.io를 조사하여 설정하고 React 애플리케이션에 연결하는 것으로 시작했습니다. 그런 다음 Redux를 설정하고 GROQ 언어를 사용하여 API를 쿼리했습니다. react-redux
를 사용하여 Redux를 React 앱에 연결하고 사용하는 방법과 styled-components 및 테마를 사용하는 방법을 보았습니다.
그러나 우리는 이러한 기술로 가능한 것의 표면만을 긁었습니다. 내 GitHub 리포지토리에 있는 코드 샘플을 살펴보고 이러한 기술을 사용하여 완전히 다른 프로젝트에서 직접 배우고 마스터하는 것이 좋습니다.
자원
- 온전한 문서
- Kapehe의 Sanity.io로 블로그를 구축하는 방법
- 리덕스 문서
- 스타일이 지정된 구성 요소 문서
- GROQ 치트 시트
- 머티리얼 UI 문서
- Redux 미들웨어 및 부작용
- Redux 썽크 문서