반응형

https://uxdesign.cc/58-rules-for-stunning-and-effective-user-interface-design-ea4b93f931f6

 

58 rules for beautiful UI design

The right UI can elevate an application from functional to unforgettable, making the difference between a user who engages once and one…

uxdesign.cc

Crafted to be your ultimate roadmap in the journey of UI design. Whether you are a seasoned designer looking to refresh your approach or a novice eager to learn the ropes, these rules are tailored to help you create interfaces that are not just visually appealing but also intuitively functional. To navigate this complex terrain, I have compiled 58 rules across eight categories, collectively forming the “Elegance Formula” for user interface design.

🫀 Empathy: There is no universal concept of beauty; only when you truly understand your target audience can you create a design that is appealing to them.

🖼️ Layout: The layout is the canvas of your interface; it should guide the user’s eye effortlessly, creating a seamless flow that intuitively connects each element.

🎟 Essentialism: Embrace simplicity; every element in your design should serve a purpose, as clutter can obscure the message and hinder the user experience.

🧭 Guidance: Design should not just please the eye but also lead the user, providing clear pathways and cues for what they should do next.

💎 Aesthetics: Aesthetics go beyond mere appearance; they encapsulate the feel of the design, creating an environment that resonates emotionally with the user.

🛸 Novelty: Innovative designs capture attention, but the true art lies in balancing novelty with familiarity, ensuring users feel intrigued yet comfortable.

🎛 Consistency: Consistency in design breeds familiarity; it ensures the user feels at home across various parts of your interface, building trust and ease of use.

🕹 Engagement: An engaging design is like a good conversation; it keeps the user interested, responds to their actions, and encourages them to come back for more.

Cultural and societal influences play a crucial role in shaping preferences and perceptions

1. Consider Cultural and Societal Influences: Factor in the diverse cultural and societal backgrounds of your audience to ensure your design resonates broadly and respectfully.

2. Understand Industry and Context of Use: Tailor your design to align with the specific industry norms and the practical context in which your interface will be used.

3. Embrace User Demographics: Embrace the diversity in user demographics, incorporating insights about age, gender, profession, and other factors to craft a more tailored and effective interface.

4. Adapt to Your Audience’s Tech-Savviness: Customize your interface to suit the specific tech-savviness level of your target audience

The Nielsen Norman Group’s research across different demographics — highlighting the unique online behaviors and expectations of young adults, the evolving digital literacy and specific usability needs of seniors, and the distinct and varying design requirements for children — emphasizes the critical importance of empathetic and user-centric design in user interface development to cater effectively to each group’s unique characteristics and preferences.

A well-planned layout is not just about placing elements on a screen; it’s about creating a visual symphony that directs, delights, and engages users

5. Embrace Negative Space: Use negative space wisely to create a clean, uncluttered interface that highlights the most important elements and improves readability.

6. Use the Golden Ratio or Rule of Thirds: Incorporate the Golden Ratio or the Rule of Thirds in your design to achieve natural balance and aesthetically pleasing proportions.

7. Establish a Clear Hierarchy with Size, Color, and Spacing: Utilize variations in size, color, and spacing to create a visual hierarchy that guides the user’s eye to the most significant information first.

8. Utilize Grid Systems: Implement grid systems to bring structure and consistency to your layout, ensuring a cohesive and harmonious arrangement of elements.

The welcome screen of the Allset app skillfully utilizes the Z-pattern layout to create rhythm and direct the user’s attention to the ‘Sign Up’ or ‘Log In’ button. By employing grid systems and ample negative space, the design presents multiple options in a manner that is clear and not overwhelming, effectively balancing information display with visual ease.

9. Create a Clear Focal Point: Designate a clear focal point in your layout to capture immediate attention and orient the user’s interaction with your content.

10. Create Rhythm to Direct Attention: Employ rhythmic design elements, such as repeated patterns or structured layouts, to create a visual flow that intuitively directs the user’s attention through the interface.

In addition, consider utilizing F and Z-pattern layouts to match users’ natural scanning habits. Employ the F-pattern in text-dense interfaces, strategically placing crucial information at the top and left.

Simplicity is ultimate sophistication

It’s about stripping away the non-essential elements and focusing on what truly matters to the user.

11. Achieve Simplicity Through Thoughtful Reduction: Prioritize content and features, removing anything non-essential. Focus on the core functionalities to create a streamlined and more user-friendly interface.

12. Organization Helps the System of Many Look Fewer: Use clear categorization and grouping of elements. Implement drop-down menus or tabs to organize content, making the interface less cluttered and more navigable.

13. Don’t Make Users Think: Ensure that navigation and task flows are logical and predictable. Use common UI elements and place them where users expect them to be, reducing cognitive load.

14. Good Design is as Little Design as Possible: Adopt a minimalist approach, using only elements that are necessary for functionality. Avoid excessive use of colors, fonts, and graphics to maintain a clean and focused interface.

The Tesla App is evidently designed with a focus on minimalism and enduring design aesthetics. This is primarily achieved through the reduction of components and labels. The interface avoids the use of intrusive styles and instead, it employs a digital representation of the car itself as the main visual element.

15. Break Up Huge Tasks into Smaller Steps: Design complex processes, like forms or multi-step tasks, into smaller segments. Use progress bars or breadcrumbs to visually indicate the user’s progress and what remains.

16. Savings in Time Feel Like Simplicity: Optimize load times and streamline processes to make interactions quicker. Use smart defaults, autocomplete features, and predictive text to speed up user input and decision-making.

You can find more recommendations in How to simplify your design.

It’s not just about leading the user from point A to point B; it’s about creating a journey that feels natural, effortless, and engaging

The art of designing a user interface involves guiding the user through a digital landscape with intuition and ease.

17. Craft Engaging User Onboarding: Start by designing an engaging onboarding process that educates users about your product from the first interaction. Effective onboarding lays the foundation for the user’s entire experience with your interface.

18. Ensure an Intuitive Flow: Develop your interface with a logical, step-by-step flow that feels natural and requires minimal effort for users to navigate, enhancing their overall experience.

19. Offer Contextual Hints and Tips: Implement contextual assistance such as tooltips, pop-ups, or inline instructions that appear when users need them, aiding in their understanding and use of the interface.

The engaging onboarding process of the ‘How We Feel’ app allows users to immediately grasp the value of the product. Helpful tips and guided recommendations are tailored based on the user’s current feelings, fostering a sense of control and intuitiveness in the user experience.

20. Implement Progressive Disclosure: Strategically reveal information to users, showing only what’s necessary at each step. This approach helps maintain a clean interface and focuses the user’s attention on immediate tasks.

21. Design to Encourage User Actions: Use clear design elements like buttons, icons, and calls to action to guide users towards desired interactions, ensuring these elements are prominent and easily accessible.

22. Provide Feedback for User Actions: Create a system that offers immediate visual or auditory feedback for user actions, acknowledging their interactions and guiding them to the next step in the interface.

Masterfully applied typography helps you stand out, enhance readability and aesthetic appeal

23. Establish Typography Hierarchy: Create a clear hierarchy using different font sizes, weights, and styles to guide the user’s attention to the most important content first.

24. Prioritize Readability: Choose fonts that are easy to read on various devices and screen sizes. Legibility should be a top priority, especially for body text.

25. Reflect Brand Mood: Select fonts that align with your brand’s personality. Whether it’s professional, playful, or elegant, typography should reinforce the brand’s tone.

The Nike Run Club App skillfully employs bold, italic typography as its main focus, evoking a sense of movement and uniqueness without overwhelming, thanks to its sparing use alongside a neutral body font

26. Pair Fonts Wisely: When combining multiple fonts, ensure they complement each other.

27. Limit Font and Style Variations: Too many font types or styles can create a cluttered and confusing interface. Stick to a limited set to maintain a clean and cohesive look.

28. Adjust Line Spacing, Kerning, and Line Height: Proper spacing between letters (kerning), words, and lines improves readability and text flow. Experiment with different settings to find the most visually appealing and readable format.

The right color choices can make a significant difference in how users perceive and interact with a product

29. Contrast is Key: Ensure sufficient contrast between text and background to enhance readability and accessibility.

30. Create and Use a Consistent Color Palette: Develop a consistent color palette that reflects your brand identity and use it consistently across your interface to maintain visual coherence.

31. Use the 60–30–10 rule for balancing colors: — 60% dominant color, 30% secondary color, and 10% accent color, to create a visually harmonious interface.

The MasterClass app serves as an exemplary model for the application of the 60–30–10 rule in design, showcasing how this principle can be effectively utilized to enhance user interface aesthetics and functionality.

32. Understand Color Psychology and Cultural Significance: Consider how different colors evoke different emotions and meanings in various cultures. Tailoring your color choices to your audience can enhance the user experience and avoid cultural missteps.

33. Communicate Status with Semantic Colors: Use colors to communicate status intuitively, like red for errors or green for success, to help users understand system feedback quickly.

34. Use Color to Guide Actions: Utilize color strategically to highlight key actions, like buttons or links, guiding the user’s attention to important interactions.

Effective visual content in UI design enhances user engagement and emotional connection

35. Prioritize Content Over Excessive UI Styling: Focus on delivering content through visuals without overwhelming the user with excessive UI decorations. Let the visuals speak for themselves.

36. Purposeful Imagery and Illustrations: Use imagery and illustrations that add meaning to your content. Avoid generic stock photos; opt for custom or carefully selected images that reflect the brand’s identity and message.

37. Keep Text Concise and Straightforward: Complement visuals with clear and concise text. Avoid long paragraphs and opt for bullet points or short descriptions that enhance the visual message.

The Hims app distinguishes itself with a content-first approach, minimizing the reliance on complex UI styling. It employs high-quality visuals, including well-curated photos and short videos, that are consistent with the app’s mood and style, contributing to a cohesive and user-friendly interface.

38. Micro-Interactions & Delightful Animations: Incorporate subtle animations and micro-interactions that enhance user engagement without detracting from the main content.

39. Use Video for Dynamic Storytelling: Implement video content to tell stories or explain complex concepts dynamically. Videos can be particularly effective in conveying messages that are difficult to express through static images.

40. Incorporate High-Quality Product Shots or Renders: For e-commerce and product-based interfaces, use high-quality photographs or 3D renders of products. Detailed and attractive product visuals can significantly boost user interest and sales.

Innovative or unique interfaces will create memorable experiences, leading to higher user satisfaction.

41. Strive for Originality and Uniqueness: Create UI designs that stand out with original concepts and unique elements, differentiating your product in a crowded market.

42. Leverage the Latest Technology: Stay abreast of emerging technologies and consider how they can be incorporated into your design to offer cutting-edge experiences.

43. Be the Most Advanced, Yet Acceptable: Push the boundaries of innovation, but ensure your designs remain user-friendly and accessible to your target audience.

Citizen’s personal safety network empowers users to protect themselves and their communities. Its integration of a personal agent concept is both innovative and user-friendly, offering a novel yet logical enhancement to the experience.

44. Take Inspiration from Other Industries: Look beyond the field of UI design for inspiration, drawing creative ideas from art, architecture, nature, and more.

45. Be Conscious of Latest Trends, But Do Not Follow Them Blindly: Stay informed about current design trends, but use them judiciously to ensure your design maintains its unique identity.

46. Ensure that Novelty Enhances the User Experience Rather Than Complicating It: Novelty should always serve a purpose, enhancing the overall user experience without adding unnecessary complexity.

Consistency creates a sense of familiarity and helps build trust and confidence

47. Develop a Comprehensive Design System: A design system acts as a single source of truth for all design elements, ensuring uniformity across all aspects of the UI.

48. Limit Design Patterns: Using a consistent set of design patterns simplifies the user’s interaction model, making the interface more predictable and user-friendly.

49. Ensure Predictability in Element Behavior: Interface elements should behave consistently throughout the application, so users know what to expect from their interactions.

The Apple Health app serves as an exemplary model of consistent user experience across various devices. Its extensive library of components and templates ensures that new features and updates can be seamlessly integrated, maintaining ease of use and uniformity.

50. Use Standardized Templates: For common page types, standardized templates provide a consistent structure, aiding in user navigation and content comprehension.

51. Maintain Cross-Device Consistency: A consistent UI across different devices and platforms enhances the user experience, making the interface more approachable and accessible.

52. Standardize Content Guidelines: Consistent tone, style, and formatting in content presentation help maintain a coherent narrative across the interface.

Create a more immersive user experience that entertains

53. Introduce Gamification Elements: Incorporate game mechanics like points, badges, and leaderboards to motivate users and encourage interaction.

54. Personalization and Customization: Offer users the ability to customize their experience. Personalization can increase the relevance of the interface to the individual user, enhancing engagement.

55. Utilize Storytelling Techniques: Embed narrative elements in the UI to create a more compelling and memorable user experience. Storytelling can guide users through the interface in an engaging way.

The Bloom App effectively incorporates gamification and educational components to assist investors in staying engaged and making well-informed investment decisions. An example of this is the offering of random gift stocks, a type of variable reward, which serves to create a sense of delight and surprise among users.

56. Visually Display Progress: Use visual indicators like progress bars to show users their achievements and progression. This can increase motivation and sense of accomplishment.

57. Incorporate Variable Reward Mechanisms: Implement elements of surprise and delight, such as unexpected rewards or bonuses, to keep users engaged and curious.

58. Integrate Social Features: Include social integration features like sharing achievements or competing with friends to foster a sense of community and encourage continued engagement.

 

Thank you for reading! If you found these insights helpful, don’t miss out on my poster of “58 Rules for Stunning and Effective User Interface Design.” It’s a practical checklist that you can use to elevate your design skills. You can find it here.
https://www.figma.com/community/file/1326467076529354215/poster-58-rules-for-stunning-and-effective-user-interface-design

반응형
반응형

https://netflixtechblog.com/reverse-searching-netflixs-federated-graph-222ac5d23576

 

Reverse Searching Netflix’s Federated Graph

By Ricky Gardiner, Alex Hutter, and Katie Lefevre

netflixtechblog.com

 

Since our previous posts regarding Content Engineering’s role in enabling search functionality within Netflix’s federated graph (the first post, where we identify the issue and elaborate on the indexing architecture, and the second post, where we detail how we facilitate querying) there have been significant developments. We’ve opened up Studio Search beyond Content Engineering to the entirety of the Engineering organization at Netflix and renamed it Graph Search. There are over 100 applications integrated with Graph Search and nearly 50 indices we support. We continue to add functionality to the service. As promised in the previous post, we’ll share how we partnered with one of our Studio Engineering teams to build reverse search. Reverse search inverts the standard querying pattern: rather than finding documents that match a query, it finds queries that match a document.

Intro
Tiffany is a Netflix Post Production Coordinator who oversees a slate of nearly a dozen movies in various states of pre-production, production, and post-production. Tiffany and her team work with various cross-functional partners, including Legal, Creative, and Title Launch Management, tracking the progression and health of her movies.

So Tiffany subscribes to notifications and calendar updates specific to certain areas of concern, like “movies shooting in Mexico City which don’t have a key role assigned”, or “movies that are at risk of not being ready by their launch date”.

Tiffany is not subscribing to updates of particular movies, but subscribing to queries that return a dynamic subset of movies. This poses an issue for those of us responsible for sending her those notifications. When a movie changes, we don’t know who to notify, since there’s no association between employees and the movies they’re interested in.

We could save these searches, and then repeatedly query for the results of every search, but because we’re part of a large federated graph, this would have heavy traffic implications for every service we’re connected to. We’d have to decide if we wanted timely notifications or less load on our graph.

If we could answer the question “would this movie be returned by this query”, we could re-query based on change events with laser precision and not impact the broader ecosystem.

The Solution
Graph Search is built on top of Elasticsearch, which has the exact capabilities we require:

percolator fields that can be used to index Elasticsearch queries
percolate queries that can be used to determine which indexed queries match an input document.

Instead of taking a search (like “spanish-language movies shot in Mexico City”) and returning the documents that match (One for Roma, one for Familia), a percolate query takes a document (one for Roma) and returns the searches that match that document, like “spanish-language movies” and “scripted dramas”.

We’ve communicated this functionality as the ability to save a search, called SavedSearches, which is a persisted filter on an existing index.

type SavedSearch {
  id: ID!
  filter: String
  index: SearchIndex!
}
That filter, written in Graph Search DSL, is converted to an Elasticsearch query and indexed in a percolator field. To learn more about Graph Search DSL and why we created it rather than using Elasticsearch query language directly, see the Query Language section of “How Netflix Content Engineering makes a federated graph searchable (Part 2)”.

We’ve called the process of finding matching saved searches ReverseSearch. This is the most straightforward part of this offering. We added a new resolver to the Domain Graph Service (DGS) for Graph Search. It takes the index of interest and a document, and returns all the saved searches that match the document by issuing a percolate query.

"""
Query for retrieving all the registered saved searches, in a given index,
based on a provided document. The document in this case is an ElasticSearch
document that is generated based on the configuration of the index.
"""
reverseSearch(
  after: String,
  document: JSON!,
  first: Int!,
  index: SearchIndex!): SavedSearchConnection
Persisting a SavedSearch is implemented as a new mutation on the Graph Search DGS. This ultimately triggers the indexing of an Elasticsearch query in a percolator field.

"""
Mutation for registering and updating a saved search. They need to be updated
any time a user adjusts their search criteria.
"""
upsertSavedSearch(input: UpsertSavedSearchInput!): UpsertSavedSearchPayload
Supporting percolator fields fundamentally changed how we provision the indexing pipelines for Graph Search (see Architecture section of How Netflix Content Engineering makes a federated graph searchable). Rather than having a single indexing pipeline per Graph Search index we now have two: one to index documents and one to index saved searches to a percolate index. We chose to add percolator fields to a separate index in order to tune performance for the two types of queries separately.

Elasticsearch requires the percolate index to have a mapping that matches the structure of the queries it stores and therefore must match the mapping of the document index. Index templates define mappings that are applied when creating new indices. By using the index_patterns functionality of index templates, we’re able to share the mapping for the document index between the two. index_patterns also gives us an easy way to add a percolator field to every percolate index we create.

Example of document index mapping

Index pattern — application_*

{
  "order": 1,
  "index_patterns": ["application_*"],
  "mappings": {
  "properties": {
    "movieTitle": {
      "type": "keyword"
    },
    "isArchived": {
      "type": "boolean"
    }
  }
}
Example of percolate index mappings

Index pattern — *_percolate

{
  "order": 2,
  "index_patterns": ["*_percolate*"],
  "mappings": {
    "properties": {
      "percolate_query": {
        "type": "percolator"
      }
    }
  }
}
Example of generated mapping

Percolate index name is application_v1_percolate

{
  "application_v1_percolate": {
    "mappings": {
      "_doc": {
        "properties": {
          "movieTitle": {
            "type": "keyword"
          },
          "isArchived": {
            "type": "boolean"
          },
          "percolate_query": {
            "type": "percolator"
          }
        }
      }
    }
  }
}
Percolate Indexing Pipeline
The percolate index isn’t as simple as taking the input from the GraphQL mutation, translating it to an Elasticsearch query, and indexing it. Versioning, which we’ll talk more about shortly, reared its ugly head and made things a bit more complicated. Here is the way the percolate indexing pipeline is set up.


See Data Mesh — A Data Movement and Processing Platform @ Netflix to learn more about Data Mesh.
When SavedSearches are modified, we store them in our CockroachDB, and the source connector for the Cockroach database emits CDC events.
A single table is shared for the storage of all SavedSearches, so the next step is filtering down to just those that are for *this* index using a filter processor.
As previously mentioned, what is stored in the database is our custom Graph Search filter DSL, which is not the same as the Elasticsearch DSL, so we cannot directly index the event to the percolate index. Instead, we issue a mutation to the Graph Search DGS. The Graph Search DGS translates the DSL to an Elasticsearch query.
Then we index the Elasticsearch query as a percolate field in the appropriate percolate index.
The success or failure of the indexing of the SavedSearch is returned. On failure, the SavedSearch events are sent to a Dead Letter Queue (DLQ) that can be used to address any failures, such as fields referenced in the search query being removed from the index.
Now a bit on versioning to explain why the above is necessary. Imagine we’ve started tagging movies that have animals. If we want users to be able to create views of “movies with animals”, we need to add this new field to the existing search index to flag movies as such. However, the mapping in the current index doesn’t include it, so we can’t filter on it. To solve for this we have index versions.


Dalia & Forrest from the series Baby Animal Cam
When a change is made to an index definition that necessitates a new mapping, like when we add the animal tag, Graph Search creates a new version of the Elasticsearch index and a new pipeline to populate it. This new pipeline reads from a log-compacted Kafka topic in Data Mesh — this is how we can reindex the entire corpus without asking the data sources to resend all the old events. The new pipeline and the old pipeline run side by side, until the new pipeline has processed the backlog, at which point Graph Search cuts over to the version using Elasticsearch index aliases.

Creating a new index for our documents means we also need to create a new percolate index for our queries so they can have consistent index mappings. This new percolate index also needs to be backfilled when we change versions. This is why the pipeline works the way it does — we can again utilize the log compacted topics in Data Mesh to reindex the corpus of SavedSearches when we spin up a new percolate indexing pipeline.


We persist the user provided filter DSL to the database rather than immediately translating it to Elasticsearch query language. This enables us to make changes or fixes when we translate the saved search DSL to an Elasticsearch query . We can deploy those changes by creating a new version of the index as the bootstrapping process will re-translate every saved search.
Another Use Case
We hoped reverse search functionality would eventually be useful for other engineering teams. We were approached almost immediately with a problem that reverse searching could solve.

The way you make a movie can be very different based on the type of movie it is. One movie might go through a set of phases that are not applicable to another, or might need to schedule certain events that another movie doesn’t require. Instead of manually configuring the workflow for a movie based on its classifications, we should be able to define the means of classifying movies and use that to automatically assign them to workflows. But determining the classification of a movie is challenging: you could define these movie classifications based on genre alone, like “Action” or “Comedy”, but you likely require more complex definitions. Maybe it’s defined by the genre, region, format, language, or some nuanced combination thereof. The Movie Matching service provides a way to classify a movie based on any combination of matching criteria. Under the hood, the matching criteria are stored as reverse searches, and to determine which criteria a movie matches against, the movie’s document is submitted to the reverse search endpoint.

In short, reverse search is powering an externalized criteria matcher. It’s being used for movie criteria now, but since every Graph Search index is now reverse-search capable, any index could use this pattern.

A Possible Future: Subscriptions
Reverse searches also look like a promising foundation for creating more responsive UIs. Rather than fetching results once as a query, the search results could be provided via a GraphQL subscription. These subscriptions could be associated with a SavedSearch and, as index changes come in, reverse search can be used to determine when to update the set of keys returned by the subscription.

 

 

 

반응형
반응형

https://blog.stackademic.com/frontend-masters-feature-sliced-design-fsd-pattern-81416088b006

 

Feature-Sliced Design (FSD) Pattern

Imagine a delicious pizza as your complex project. To understand how feature slicing works, let’s break it down slice by slice: easy to manage and delicious (to maintain)! 🍕

I will repeat what I said in my previous medium stories. This will be a long (because why not 😇) and maybe a little difficult to understand article. But once you read it and understand it perfectly, you will now be a 10x frontend developer. 😎 Let’s take your coffee. ☕️ If you don’t drink enough coffee, it will take you longer to become a 10x developer. 😅

 

I like to simplify web and mobile applications by dividing them into smaller parts called feature sets. Each feature set has its own user interface, business logic, and data layer, making it easier to handle. This method, called Feature-Sliced Design (FSD), shares advantages with component-based approaches. What stands out to me about FSD is its ability to break down web and mobile applications into more manageable pieces, especially focusing on user-centric features.

 

Structure

FSD methodology is built on three abstraction levels: layers, slices, and segments.

Layers and Slices

Imagine your app as a delicious pizza. 🍕 (As a developer working at Domino’s, it is my natural right to use the pizza analogy. 😎)

1. Shared Layer (The Pantry):

  • Ingredients for everyone: Holds reusable components, utilities, hooks, and services that multiple slices can access. (Think of it as the shared kitchen where everyone can grab common ingredients and tools.)
  • Examples:
    - 
    Common UI elements like buttons, forms, modals and navigation bars (think of them as pre-cut veggies and cheese)
    - Utility functions for data formatting or validation (like a sharp pizza cutter)
    - Global state management solutions like Redux, Zustand, Tanstack Query (the recipe book for consistency)

2. Processes Layer (The Kitchen Staff):

  • The hardworking chefs: Handles background tasks and data fetching, keeping the pizza kitchen running smoothly. (Think of them as the pizza chefs who prepare the dough, sauce, and toppings, and coordinate the baking process.)
  • Examples:
    - 
    Fetching pizza orders from the online system
    - Sending notifications when pizzas are ready
    - Syncing data with the delivery drivers

3. Features Layer (The Pizza Slices):

  • Independent and self-contained: Each slice encapsulates a specific feature, with its own UI, logic, and data, like individual pizza slices with their toppings.
  • Examples:
    - “Order Pizza” slice:
     Handles pizza selection, customization, and checkout (pepperoni, mushrooms, extra cheese, sucuk -sausage- you name it!)
    - “Track Order” slice: Displays order status and estimated delivery time (like a pizza tracker)
    - “Review Pizza” slice: Allows customers to rate and comment on their experience (a feedback form for the chef)

4. App Layer (The Pizza Chef):

  • The head chef: Oversees the entire pizza-making operation, deciding which slices to bake and how to present them to customers. (Think of it as the master chef who designs the menu, creates new recipes, and ensures each pizza is cooked to perfection.)

5. Pages Layer (The Pizza Display):

  • Arranges the slices: Composes slices into meaningful page layouts, like arranging pizza slices on a platter or delivery carton box.
  • Examples:
    - Homepage:
     Combines “Featured Pizzas” and “Order History” slices
    - My Account: Includes “Personal Information” and “Order Preferences” slices

6. Widgets Layer (The Spices):

  • Optional flavor enhancers: Small, reusable UI components that can be sprinkled across slices or pages/screens, like adding extra seasoning to your pizza.
  • Examples:
    - 
    Search bar (for finding your favorite pizza quickly)
    - User notification panel (alerting you when your pizza is ready)
    - Modal dialogs (for special requests or confirmations)

5. Entities Layer (The Raw Ingredients):

  • Building blocks of data: Represents core business entities, like the flour, yeast, and toppings in a pizza.
  • Examples:
    - 
    User entity (storing customer details)
    - Pizza entity (defining pizza types and ingredients)
    - Order entity (tracking order information)

Key points to remember:

  • Each layer has a clear responsibility and dependency direction.
  • Slices can communicate with each other using well-defined contracts, like pizza slices sharing a common crust.
  • The goal is to create modular, independent, and easily testable slices, making your “pizza” codebase more manageable and delicious!

Additional pizza analogy notes:

  • The kitchen staff (processes) work behind the scenes, preparing ingredients and ensuring a smooth pizza-making process.
  • The pizza chef (app) is the mastermind, orchestrating the creation of different pizzas (features) and deciding how to serve them up (pages).
  • The raw ingredients (entities) are essential for any pizza, but they’re not always visible to the customer — they’re the foundation that makes everything else possible.

Segments (The Toppings):

  • The ingredients within a slice: While a slice is a complete feature, it’s often made up of smaller parts, called segments. These are like the individual toppings that make up a pizza slice.
  • Focused on specific tasks: Each segment has a clear responsibility within its slice, like handling a particular UI element, data operation, or piece of logic.
  • Examples:
  • Within the “Order Pizza” slice:
    - “Pizza Menu” segment:
     Displays available pizza options and prices.
    - “Topping Selector” segment: Allows customers to choose their desired toppings.
    - “Checkout Form” segment: Collects payment and delivery information.

With a more React way:

Each slice is split into one or more of the following segments:

  • ui/: User Interface components and UI related logic
  • model/: Business logic (store, actions, effects, reducers, etc.)
  • lib/: Infrastructure logic (utils/helpers)
  • config/: Local configuration (constants, enums, meta information)
  • api/: Logic of API requests (api instances, requests, etc.)

Key points to remember:

  • Slices are larger, self-contained features, while segments are smaller, focused parts within a slice.
  • Each slice can have multiple segments, just like a pizza slice can have various toppings.
  • The goal is to create well-organized, modular code that’s easy to understand, develop, and maintain — like making a pizza with beautifully arranged toppings, each adding its unique flavor to the whole pie!
 

How about exemplifying what we have read so far in code?

1. Folder Structure (The Pizza Kitchen Organization):

2. Order Pizza Slice (The Extravaganzza):

// features/order-pizza/slice.tsx
import React from 'react';

interface OrderPizzaSliceProps {
  // ... slice props
}

const OrderPizzaSlice: React.FC<OrderPizzaSliceProps> = ({ /* ...props */ }) => {
  // ... slice logic and state management

  return (
    <div>
      <PizzaMenu />
      <ToppingSelector />
      <CheckoutForm />
    </div>
  );
};

export default OrderPizzaSlice;

3. Pizza Menu Segment (The Dough Base):

// features/order-pizza/components/PizzaMenu.tsx
import React from 'react';

interface PizzaMenuProps {
  // ... pizza options
}

const PizzaMenu: React.FC<PizzaMenuProps> = ({ /* ...props */ }) => {
  // ... fetch pizza options and display them

  return (
    <ul>
      {/* List of pizza options */}
    </ul>
  );
};

export default PizzaMenu;

4. Homepage (The Pizza Display Counter):

Feature-Sliced Design (FSD) is like slicing that pizza into neat, individual pieces, each with its own toppings and flavor. Here’s the good and not-so-good of this approach:

// pages/HomePage.tsx
import React from 'react';
import OrderPizzaSlice from '../features/order-pizza/slice';

const HomePage: React.FC = () => {
  return (
    <div>
      <h1>Welcome to Domino's Pizza!</h1>
      <OrderPizzaSlice />
    </div>
  );
};

export default HomePage;

Key takeaways from the code:

  • Clear folder structure: Each slice has its own folder, keeping its components and logic organized.
  • Independent slices: Each slice can be developed and tested independently, like a self-contained pizza.
  • Reusable components: Shared components (buttons, inputs, etc.) can be used across slices for consistency and efficiency.
  • Composition within pages: Pages combine slices to create meaningful layouts, like arranging pizza slices on a platter.

Remember: This is a simplified example. Real-world FSD involves more complex state management, data fetching, and communication between slices. However, this example demonstrates the core principles of organizing React apps using FSD.

The good stuff (slices):🍕

  • Easy to manage: Like separate pizza slices, each feature is self-contained, making code easier to understand, fix, and update. No more domino effect when changing one part!
  • Scales like crazy: Need more features? Just add more slices! FSD lets your app grow gracefully, adapting to new needs like adding pepperoni to your veggie delight.
  • Faster cooking (development): Different teams can work on separate slices at the same time, speeding up development like having multiple chefs making pizzas.
  • Clear ownership: Each slice has a designated “pizzaiolo,” making developers responsible for its quality and performance, similar to how each chef takes pride in their creation.
  • Testing made simple: Testing becomes like checking each slice for doneness, making it more focused and efficient.

The not-so-good stuff (crusts):👎🏻

  • Trickier planning: Slices need to work together seamlessly, like ensuring the cheese doesn’t spill over when joining them. Careful planning and communication are key to avoiding a pizza mess.
  • Learning curve: Newcomers might be initially confused by the distributed nature of the “pizza,” like figuring out where to find the pineapple chunks. Good documentation is essential to help them navigate.
  • Extra effort for teamwork: Ensuring communication and smooth connections between slices takes time and attention, like coordinating the chefs to build the perfect pizza together.
  • Potential redundancy: Sometimes, two slices might have similar ingredients, like having both mozzarella and ricotta. Careful planning and shared resources can help avoid unnecessary duplication.
  • Limited tools: FSD is still relatively new, so finding tools specifically designed for it might be like searching for a pizza cutter shaped like a unicorn. It might require some extra effort at first.

The decision:

FSD is like a great strategy for large and complex apps, but it’s not a one-size-fits-all recipe. Consider your project’s size, team, and development environment before diving in. Remember, even the most delicious pizza can be tricky to make if you don’t have the right skills and ingredients!

반응형
반응형

When working with UI in Flutter, there are often cases where you need to measure the size of widgets that have variable sizes depending on their child widgets. Let’s take the example of an OTT streaming app.


In the movie detail page, the plot section shows both text and images. If this area exceeds a certain height, a ‘More’ button appears, partially hiding the content. Pressing ‘More’ reveals the rest of the content.

This plot section does not have a fixed size. Rather, its height varies dynamically based on the amount of text data, device width, and image height. Therefore, to implement such a UI structure, it’s necessary to measure the rendered height of the widget and conditionally set up UI elements like the ‘more’ button and the hidden content based on whether it exceeds a certain height.

So, how can we measure the dynamic size of widgets? In this post, we’ll explore step-by-step how to measure the variable size of widgets through a simple example. Additionally, we’ll delve into the following concepts, addressing Flutter’s rendering process.

WidgetTree, ElementTree, RenderTree
BuildContext
RenderObject
addPostFrameCallback
NotificationListener
Implementation Goals
Let’s briefly examine the example we’ll cover in the post.


The above screenshot depicts a simple page displaying information about movie cast members. It consists of a Text widget for the title section and a ListView widget with ExpansionTile displaying information about the cast members, all wrapped inside a Column. The Text widget for the title section shows the current height of the Column.

class CastInfoPage extends StatelessWidget {
  const CastInfoPage({super.key});@override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF000000),
      appBar: AppBar(
        leading: const Icon(
          Icons.arrow_back_ios,
          color: Colors.white,
        ),
        titleSpacing: 0,
        backgroundColor: Colors.black,
        centerTitle: false,
        title: Text(
          'Dune: Part Two',
          style: AppTextStyle.headline1,
        ),
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.symmetric(horizontal: 16) +
              const EdgeInsets.only(top: 20),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                'Height : ${0}',
                style: PretendardTextStyle.bold(
                  size: 24,
                  height: 37,
                  letterSpacing: -0.2,
                ),
              ),
              const SizedBox(height: 10),
              ListView.separated(
                physics: const NeverScrollableScrollPhysics(),
                padding: EdgeInsets.zero,
                shrinkWrap: true,
                itemCount: CastModel.castList.length,
                separatorBuilder: (_, __) => const SizedBox(height: 8),
                itemBuilder: (context, index) {
                  final item = CastModel.castList[index];
                  return ExpansionTile(
                    tilePadding: EdgeInsets.zero,
                    title: Row(
                      children: [
                        ClipRRect(
                          borderRadius: BorderRadius.circular(56 / 2),
                          child: CachedNetworkImage(
                            height: 56,
                            width: 56,
                            imageUrl: item.imgUrl,
                            fit: BoxFit.cover,
                          ),
                        ),
                        const SizedBox(width: 10),
                        Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              item.name,
                              style: AppTextStyle.title1,
                            ),
                            Text(
                              item.role,
                              style: AppTextStyle.body3.copyWith(
                                color: AppColor.gray02,
                              ),
                            )
                          ],
                        ),
                      ],
                    ),
                    children: <Widget>[
                      Text(
                        item.description,
                        style: AppTextStyle.body3,
                      ),
                    ],
                  );
                },
              )
            ],
          ),
        ),
      ),
    );
  }
}
When you click on the ExpansionTile, the widget expands to show detailed information about the cast members. Therefore, the size of the widget changes, requiring adjustment of the height value accordingly. We can summarize the following requirements.

- Must be able to obtain the precise size of the currently rendered widget.
- Should be able to access the value in the measuring widget to handle UI conditionally.
- Must detect changes in widget size dynamically and obtain the changed size (not expandable size).
- Should be easy and convenient to use.
You can check out the implemented example through the following site.

measure_size_implementation
A new Flutter project.
measure-size-builder-example.netlify.app

How are Widgets Drawn?
First, let’s take a look at how widgets are drawn on the screen in Flutter.


You’ve probably heard about the concept that Flutter creates widgets based on a 3 Tree Architecture consisting of Widget, Element, and Render. You may not be very familiar with encountering objects of elements or render trees during Flutter development, as it's not very common. However, understanding the basic concepts of Flutter's widget tree structure can be very helpful when direct manipulation or access to element or render objects is required, as in this example. So, let's try to explain it in a way that's easier to understand.

Widget Tree — Blueprint of a Car
class Lamborghini extends StatelessWidget {
  const Lamborigini({super.key});  
  @override
  Widget build(BuildContext context) {
    return Car(
        paint: RedPaint(),
        engine: 4LV8Engine(),
        wheel: RimsAltaneroShinyBlack(),
        carbon : UpperExteriorCarbon(),
        ...
    );
  }
}
To help understand the three tree structures of widgets, let’s use the analogy of building a Lamborghini. To build a car, various components such as color, engine, etc., need to be determined. In the above code, we’re passing necessary options to the Car class within the build method.


This process is similar to creating a blueprint for the car, defining how the car’s components are structured and shaped. StatelessWidget or StatefulWidget always override the build method and return a widget inside. These codes are returned as a widget tree and internally create the necessary 'element' through createElement().

Key Point!
The code inside the build method is returned as a 'widget tree' and creates an 'element tree'.

Element Tree — Car Parts and Engineers
The element tree generated from the widget tree is comprised of elements that are part of widgets and are responsible for managing widget lifecycle and state changes. While the widget tree contains structural information about the code written by developers, the element tree consists of pieces of widgets created based on the widget tree.


If the widget tree is likened to a blueprint of a car, the element in the element tree can be compared to both the car parts and the engineer managing those parts. Just as a car engineer arranges and manages necessary parts according to the blueprint, the element tree creates elements that are parts of the UI needed for the final rendering of widgets and communicates any changes to the render tree as necessary. Now, let's take a closer look at the characteristics of elements.

Widgets are Immutable, Elements are Mutable
All Flutter widgets are immutable, meaning their content cannot be modified during runtime. This is similar to a car not being able to suddenly transform into a motorcycle. However, elements are mutable, allowing widgets to be changed as needed. In other words, elements can be removed and replaced with new elements.

Role of BuildContext
The BuildContext, which we always pass as an argument when executing the build method in StatelessWidget or StatefulWidget, is used when direct manipulation or access to the Element object is required. It also indicates where the Element created from the widget tree is positioned in the tree. It's like an engineer (BuildContext) examining the blueprint (Widget Tree) to determine where the necessary parts (Element) are and arranging them accordingly.

showDialog<void>(  
  context: context,  
  builder: (BuildContext context) {  
    return AlertDialog(...);  
  },  
);
Similarly, when displaying a popup using methods like showDialog, we always need to pass the BuildContext because we need to know which widget (screen) in the composed tree the dialog should appear on.

Key Point!
- The element tree manages the widget’s lifecycle and communicates changes to the render tree as needed.
- BuildContext is used to determine the position of widgets currently displayed on the screen and plays an important role in manipulating or accessing elements.
-BuildContext is also considered as an Element.

Rendering Tree — Car Manufacturing, Manufactured Car

Once the necessary elements are created, the widget finally creates a Render Tree. It's used to handle the actual rendering via the widget's createRenderObject method, which creates a RenderObject, an object that manages the widget's size and layout information. During this process, the RenderObjectElement, created from the Element Tree, becomes directly involved.

The rendering tree can be likened to the manufacturing of a car using car parts. It completes the car using the components manufactured from the Element Tree.

In the rendering tree, two main methods, layout and paint, are used to actually render the widgets we see. During the layout phase, parent nodes pass constraints to child nodes, and at the lowest level, the final size information is passed back up to determine where and how widgets should be drawn. Then, the paint operation is performed, passing the work to the GPU thread to finally complete the widgets.

Key Point!


What If it Wasn’t Composed of Three Widget Trees?

Now that you have some understanding of the architecture of the widget tree, you can clearly understand the reason why widgets are composed of three widget trees. If you were to replace a wheel on a car, you wouldn’t need to rebuild the entire car from scratch. You’d simply replace the existing wheel with a new one. Flutter operates on a similar principle. When parts of a rendered widget need to change based on state, the corresponding element detects this and communicates the changes to the render tree, allowing only the necessary parts to be re-rendered.

However, if Flutter’s widget tree were composed of only one tree, even widgets that didn’t need to change based on state would be redrawn, leading to inefficiencies. It would be like building a new car every time you change a wheel.

In summary, the fundamental reason Flutter’s widgets are composed of three tree structures is to efficiently re-render only the necessary parts of the screen when changes are needed based on state.

1. Separating Widget Trees and Accessing Render Objects via BuildContext
Now, let’s go through step by step how to derive the size of widgets with variable sizes depending on their child widgets.

As mentioned earlier, the rendered size of a widget exists in the render tree within a RenderObject, and to access this object, we need a BuildContext. Therefore, the formula is established that with just a BuildContext, we can access the rendered size of a widget.

Size size = context.size!;
With this code, we can check the rendered size of a widget accessible via the BuildContext.

However, there is one problem in the current example code’s widget tree.


We want to obtain the rendered size of the Column widget through its associated RenderObject. However, the BuildContext that can access the RenderObject is located higher up at the CaseInfoScreen level. This means that if we proceed like this, we’ll end up measuring not only the size of the Column but also the size of the AppBar, which is a child widget of the Scaffold.


To solve this issue, there are various methods, but the simplest approach is to separate the widget whose size you want to measure, like a StatelessWidget or StatefulWidget. By doing this, a new sub-widget tree is created where the BuildContext is directly accessible through the build(BuildContext context) method of the separated widget. This is commonly referred to as "separating the BuildContext."

class ContentView extends StatefulWidget {
  const ContentView({Key? key});

  @override
  State<ContentView> createState() => _ContentViewState();
}

class _ContentViewState extends State<ContentView> {
  double renderedHeight; // <-- Rendered height of the [ContentView] widget

  @override
  void initState() {
    super.initState();
    /// Accessing the rendered size of the widget via [BuildContext]
    /// and assigning it to the renderedHeight variable
    renderedHeight = context.size?.height ?? 0; 
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          'Height : ${renderedHeight}', // <- Displaying the rendered height in a Text widget
        ),
        ...
      ],
    );
  }
}
Now, as shown in the above code, by separating the widget we want to measure into a separate StatefulWidget, we can obtain the rendered height of the desired widget by accessing the size value through the BuildContext within the new build method.

NOTE
If you don’t separate it into a separate widget, using GlobalKey to find the RenderObject can also be a good approach.

2. Obtaining Size When Widget Rendering is Complete
However, running the above code will result in the following runtime error:

======== Exception caught by widgets library =======================================================
The following assertion was thrown building Builder(dirty):
Cannot get size during build.
Why does this error occur?


As explained earlier, in the render tree, the layout method is responsible for passing constraints from parent nodes to child nodes and determining the final size information from the bottom node to the top node to decide where and how the widget should be drawn. The problem arises because an attempt is made to access the size value before the layout method is executed.

To address this issue, Flutter provides the addPostFrameCallback method. This method is used to register a callback that is invoked after the widget has been painted on the screen. In other words, it's a callback method that is executed after the rendering tree operations are completed.

WidgetsBinding.instance!.addPostFrameCallback((_) {
   setState(() {
         renderedHeight = context.size!.height; 
    }); 
});
Inside the addPostFrameCallback callback method, as shown in the code above, you can assign the rendering height of the widget accessed through the BuildContext to the renderedHeight variable. This way, by accessing the rendering size value through the context only after the widget has been rendered, you can assign a value to the renderedHeight variable without encountering any errors.

3. Detecting and Obtaining Size When Widget Size Dynamically Changes
While most of the requirements have been met, there’s one remaining functionality: detecting when the widget’s size changes due to user interaction and displaying the updated size on the screen.


When the ExpansionListTile widget on the screen is clicked, the widget expands, changing its size. However, the current code does not detect the size change and update the value.

To address this, we can use a widget called NotificationListener, which is useful for detecting and handling notifications (such as size changes, scrolling, gestures, etc.) that occur within the widget tree.

NotificationListener(  
  onNotification: (_) { 
    if(renderedHeight != context.size!.height) {
  setState(() {  
renderedHeight = context.size!.height;  
log('height : $renderedHeight');  
   });  
    }
    return true;  
  },  
  child: Column(...)
  )
Wrap the existing Column widget with a NotificationListener widget. Inside the onNotification callback, add logic to detect the size change of the widget and update its size accordingly. In the provided code, the setState method is used to update the changed size.

NOTE
Since NotificationListener can receive events like scrolling or touch gestures, which could lead to unnecessary updates if the size remains the same, the update logic is placed within the condition if(renderedHeight != context.size!.height).

By adding this code, the widget’s height changes are detected, and the size is updated accordingly. However, a new issue arises: continuous execution of the onNotification callback whenever the widget's size changes.

[log] height : 367.0
[log] height : 367.0
[log] height : 369.3854225873947
[log] height : 375.8518112897873
[log] height : 385.81551444530487
[log] height : 435.25
[log] height : 413.13516367971897
[log] height : 430.28125
[log] height : 448.9247215986252
[log] height : 469.3435592651367
[log] height : 491.38857555389404
[log] height : 551.9744523763657
[log] height : 540.0271100997925
[log] height : 567.0
The onNotification callback is repeatedly executed whenever the widget's size changes, leading to multiple unnecessary calls to the setState method. This could potentially impact performance and needs to be addressed.

To solve this issue, we can implement a debouncer logic. A debouncer delays consecutive calls for a certain period and executes the action only after the last call.

/// Deboucner Module
class Debouncer {  
  final Duration delay;  
  Timer? _timer;  
  
  Debouncer(this.delay);  
  
  void run(VoidCallback action) {  
    _timer?.cancel();  
    _timer = Timer(delay, action);  
  }  
}
/// ContentView Widget
double? renderedHeight;  
final Debouncer debouncer = Debouncer(const Duration(milliseconds: 50));

Widget build(BuildContext context) {  
 return NotificationListener(  
 onNotification: (_) {  
     debouncer.run(() {  // <-- Apply Debouncer Callback 
      if(renderedHeight != context.size!.height) {
     setState(() {  
    renderedHeight = context.size!.height;  
    log('height : $renderedHeight');  
      });  
     }
       });  
       return true;  
     },  
     child: Column(...)
      ... 
   }
In the provided code, a Debouncer class is declared, and within the onNotification callback, the debouncer is used to delay consecutive calls to the setState method. This ensures that the setState method is only called after the widget's size has stopped changing, optimizing performance.

4. Modularizing for Ease of Use
Since the functionality of obtaining the rendering size of a variable widget can be applied to different screens or multiple projects, modularizing it for easy use is a good idea.


Therefore, I created a custom widget called MeasureSizeBuilder. This widget encompasses all the logic discussed earlier, and it is designed to allow access to the rendering size of the specified widget through the builder property, which returns the widget whose size needs to be measured. The size can be accessed through the size property within the builder.

Now, let’s take a look at the completed example code.

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:measure_size_builder/measure_size_builder.dart';
import 'package:measure_size_implementation/src/cast_model.dart';
import 'package:measure_size_implementation/src/style/app_color.dart';
import 'package:measure_size_implementation/src/style/app_text_style.dart';

class CastInfoPage extends StatelessWidget {
  const CastInfoPage({Key? key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF000000),
      appBar: AppBar(
        leading: const Icon(
          Icons.arrow_back_ios,
          color: Colors.white,
        ),
        titleSpacing: 0,
        backgroundColor: Colors.black,
        centerTitle: false,
        title: Text(
          'Dune: Part Two',
          style: AppTextStyle.headline1,
        ),
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.symmetric(horizontal: 16) +
              const EdgeInsets.only(top: 20),
          child: MeasureSizeBuilder(
            builder: (context, size) {
              return Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Height : ${size.height}',
                    style: PretendardTextStyle.bold(
                      size: 24,
                      height: 37,
                      letterSpacing: -0.2,
                    ),
                  ),
                  const SizedBox(height: 10),
                  ListView.separated(
                    physics: const NeverScrollableScrollPhysics(),
                    padding: EdgeInsets.zero,
                    shrinkWrap: true,
                    itemCount: CastModel.castList.length,
                    separatorBuilder: (_, __) => const SizedBox(height: 8),
                    itemBuilder: (context, index) {
                      final item = CastModel.castList[index];
                      return ExpansionTile(
                        tilePadding: EdgeInsets.zero,
                        title: Row(
                          children: [
                            ClipRRect(
                              borderRadius: BorderRadius.circular(56 / 2),
                              child: CachedNetworkImage(
                                height: 56,
                                width: 56,
                                imageUrl: item.imgUrl,
                                fit: BoxFit.cover,
                              ),
                            ),
                            const SizedBox(width: 10),
                            Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(
                                  item.name,
                                  style: AppTextStyle.title1,
                                ),
                                Text(
                                  item.role,
                                  style: AppTextStyle.body3.copyWith(
                                    color: AppColor.gray02,
                                  ),
                                )
                              ],
                            ),
                          ],
                        ),
                        children: <Widget>[
                          Text(
                            item.description,
                            style: AppTextStyle.body3,
                          ),
                        ],
                      );
                    },
                  )
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}

Finally, we have fulfilled all the requirements we set out to achieve 🎉

I have published the measure_size_builder package used in the example. For those interested in this feature or modularized code, please refer to the following link.

measure_size_builder | Flutter package
Simplest way to get dynamic size of widget
pub.dev

 


https://medium.com/@ximya/get-dynamic-widget-size-in-flutter-f3e12c52ce1f

반응형
반응형

https://game.naver.com/lounge/chzzk/board/detail/4033868

 

⚡치지직 정식오픈 - 본겜시작 : 치지직

⚡치지직 정식오픈 - 본겜시작 : 치지직

game.naver.com

 

네이버가 게임 특화 스트리밍 플랫폼 ‘치지직’을 9일 정식으로 출시했다. 네이버는 정식 서비스를 통해 서비스 안정성을 더욱 높이고 편의 기능을 강화할 방침이다.

 

‘치지직’은 지난 12월 베타서비스로 공개된 서비스다. 신규 서비스임에도 불구하고 치지직은 경쟁 스트리밍 서비스인 ‘트위치’의 한국 시장 철수 시점에 출시되면서 국내 점유율을 높이고 있다. 실제로 모바일 앱 분석 기관인 모바일인덱스 조사에 따르면, 치지직의 12월 사용자의 77% 이상이 트위치 사용자에서 유입됐다. 2024년 3월 기준 치지직 사용자 수는 227만 명으로 국내 스트리밍 플랫폼 점유율 1위 기업인 아프리카TV(248만명)와의 사용자 수 격차가 21만 명으로 좁혀졌다. 참고로 네이버는 치지직의 공식 사용자 수를 공개하지 않았다.

네이버는 치지직을 정식 출시함에 따라, 안정적인 서비스를 제공하며 스트리밍의 편의 요소가 추가할 예정이다. 대표적으로 ‘미션 후원’ 기능과 ‘치지직 클립’이 이번에 지원된다. 미션 후원은 시청자가 후원금을 주기 위해 특정 조건을 걸 수 있는 기능이다. 가령 시청자가 ‘A 게임을 모두 실행할 경우’라는 조건을 걸고, 스트리머가 실제 게임을 전부 시행했을 때 후원이 최종 완료되는 식이다. ‘치지직 클립’을 통해 스트리밍 영상을 간편히 편집하는 도구다. 단순히 영상 편집하는 것 외에도 시청자가 후원을 진행할 때 보내는 영상 메시지를 치지직 클립으로 간편하게 제작해 전달할 수 있다.

치지직 정식 서비스에선 네이버 타서비스의 연계도 강화된다. 네이버는 치지직에서 생성한 스트리머의 숏폼 영상을 네이버앱의 콘텐츠 추천 영역에서 노출할 예정이다. 또한 스트리머 팬카페에 치지직 라이브 진행 여부 및 VOD 영상 노출을 확인할 수 있도록 카페 연동을 더욱 고도화하고, 네이버의 AI 보이스 기술을 적용한 스트리머 보이스 후원 기능도 3분기 내에 발표할 계획이다.

네이버 치지직 김정미 리더는 “치지직은 베타 서비스 기간에 꼼꼼히 서비스의 사용성 및 안정성을 점검하며 완성도를 높여갔고, 스트리밍 시장의 대표 서비스로 빠르게 안착했다”라며 “앞으로도 치지직은 다양한 타 서비스와의 연계, 다채로운 기능 오픈 등을 통해 서비스 경쟁력을 강화하겠다”라고 설명했다.

 

 

반응형
반응형

Python에서 현재 설치된 라이브러리(패키지) 목록을 확인하는 방법은 여러 가지가 있습니다. 
주로 pip 패키지 관리자를 사용하여 설치된 패키지 목록을 조회합니다. 
아래는 다양한 방법으로 현재 설치된 라이브러리 목록을 확인하는 방법을 설명합니다.

1. pip list 명령어 사용

가장 일반적인 방법은 터미널 또는 명령 프롬프트에서 pip list 명령을 사용하여 설치된 모든 패키지 목록을 조회하는 것입니다.


pip list




위 명령을 실행하면 설치된 모든 패키지의 목록이 출력됩니다.


2. pip freeze 명령어 사용

pip freeze 명령은 pip list와 유사하게 현재 환경에 설치된 패키지와 그 버전을 출력합니다. 이 명령은 보통 requirements.txt 파일을 생성하는 데 사용됩니다.


pip freeze




3. Python 스크립트를 통한 확인

Python 스크립트 내에서 pkg_resources 모듈을 사용하여 현재 설치된 패키지를 조회할 수 있습니다.

import pkg_resources

# 현재 설치된 패키지 목록 조회
installed_packages = pkg_resources.working_set
installed_packages_list = sorted(["%s==%s" % (i.key, i.version) for i in installed_packages])

for package in installed_packages_list:
    print(package)




4. conda list (conda 환경에서)

만약 Anaconda 또는 Miniconda와 같은 conda 패키지 관리자를 사용하고 있다면, conda list 명령을 사용하여 현재 환경에 설치된 패키지 목록을 확인할 수 있습니다.

conda list



위 방법 중 하나를 선택하여 현재 Python 환경에 설치된 모든 패키지 목록을 확인할 수 있습니다. 

주로 pip list 명령이 가장 일반적이고 널리 사용되는 방법입니다.

반응형
반응형

책임감 있는 AI, 일명 RAI(Responsible AI)에 대한 관심이 더욱 높아지고 있다. RAI는 신뢰와 도입부터 LLM 환각 관리, 유해한 생성형 AI 콘텐츠 제거에 이르기까지 모든 것을 추진하는 핵심 요소다. 효과적인 RAI로 기업은 더 빠르게 혁신하고, 비즈니스를 더 개선하고, 미래의 AI 규정을 준수하고, 평판이 손상되거나 규제를 어기는 일 없이 계속 경쟁에 참여할 수 있다.

안타깝게도 RAI가 실제로 무엇인지, 무엇을 제공하는지, 어떻게 달성할 수 있는지에 대한 혼란이 있다. 치명적인 결과를 초래할 수 있는 문제다. 그러나 RAI 이니셔티브가 제대로 수행되지 않으면 혁신이 방해받고 지연과 비용이 추가된다. RAI의 정의와 목적에 대한 좋은 해석도 있지만 잘못된 통념과 오해도 만연해 있다. 기업은 RAI는 비용이 많이 들고 비효율적이며 시간 낭비라는 잘못된 통념을 깨뜨리고 AI에 기반해 가치를 창출하는 원동력으로 전환해야 한다.

 

RAI에 대한 오해 중 가장 문제적인 것은 무엇일까? 그리고 지속 가능한 이니셔티브를 추진하기 위해 RAI를 가장 잘 정의하려면 어떻게 해야 할까?


잘못된 통념 1 : RAI는 원칙론일 뿐

어느 거대 IT 기업에서나 설명 가능성, 공정성, 개인정보 보호, 포용성, 투명성과 같은 RAI 원칙을 찾아볼 수 있 수 있다. 이렇게 널리 퍼진 여러 원칙이 있으니 RAI의 중심에 원칙이 있다고 해도 문제가 없을 것이다. 이런 기본적인 기업 원칙은 책임감 있는 개인에게 기대하는 것과 정확히 같은 종류이므로 책임감 있는 AI 보장에 핵심이 된다고 하면 맞는 말일까?

아니, 틀렸다. 모든 기업에는 이미 원칙이 있다. 일반적으로 기업 원칙은 RAI와 정확히 동일할 것이다. 공정, 투명성, 포용에 반대한다고 밝힐 기업이 있을까? 만약 그런 기업이 있다고 해도, AI에는 투명성을 적용하고 기업의 나머지에는 투명성을 적용하지 않을 수가 있을까?

또한 원칙은 AI에 대한 신뢰를 불러일으키는 것보다 사람과 기업에 대한 신뢰를 끌어내는 데에 훨씬 효과적이다. 항공사의 비행기가 목적지까지 안전하게 데려다 줄 것이라고 믿는 이유가 그 회사가 원칙을 지키기 때문인지 생각해 보자. 아니, 엄격하게 시행되는 프로세스를 따르고, 신중하게 테스트하고 정기적으로 검사한 장비를 사용하는 숙련된 조종사, 기술자, 항공 교통 관제사가 있어서 신뢰하는 것이다.

비행기 여행과 마찬가지로 RAI의 핵심은 원칙을 실현하고 시행하는 사람, 프로세스, 기술이다. 독자 모두 이미 올바른 원칙을 정해두었을 것이다. 문제는 실행이다.
 

잘못된 통념 2 : RAI는 윤리와 관계가 있다

RAI는 AI를 윤리적으로 사용하고, 모델이 공정하게 작동하도록, 새로운 차별이 생겨나지 않도록 하는 지침일 뿐이라고 생각한다면 틀렸다.

신용 평가에 사용되거나 이력서를 심사하거나 실직 여부를 결정하는 모델 등 실제로 윤리나 공정성을 고려하는 AI 사용례는 극히 일부일 뿐이다. 당연히 이러한 사용례를 책임감 있게 처리하는 것도 RAI의 목적이지만, 다른 모든 AI 솔루션의 안전과 안정적 개발 및 사용, 또 기업의 성능 및 재무 요구 사항 충족도 RAI가 필요한 부문이다.

설명 가능성을 제공하고, 편향성을 확인하고, 개인정보 보호를 보장하는 데 사용하는 도구는 정확성, 신뢰성 및 데이터 보호를 보장하는 데 사용하는 도구와 정확히 같다. RAI는 공정성을 고려해야 할 때 AI를 윤리적으로 사용하는 데 도움이 되지만, 다른 모든 AI 사용례에서도 마찬가지로 중요하다.
 

잘못된 통념 3 : RAI를 알려면 설명 가능성부터 알아야 한다

AI를 신뢰하고 책임감 있게 사용하기 위해서는 설명 가능성, 즉 해석 가능성이 필요하다는 것이 일반적인 생각이지만, 사실은 아니다. 비행기 여행에 안심하기 위해서 비행기의 작동 원리를 알아야 할 필요가 없듯, AI를 신뢰하기 위해 설명 가능성을 꼭 이해할 필요는 없다.

인간의 결정이 좋은 예다. 인간은 거의 대부분 자신의 결정 이유를 설명할 수 있지만, 사실 설명은 의사 결정 행동의 실제 동인과는 거의 관련이 없는, 사후에 지어낸 이야기라는 증거가 많다.

그러나 쉽게 이해할 수 있는 '화이트박스' 모델과 LIME 및 ShAP과 같은 방법을 사용하는 AI 설명 가능성은 모델이 올바르게 작동하는지 테스트하는 데 중요하다. 잘못된 상관관계와 잠재적인 불공정한 차별을 식별하는 데 도움이 되기 때문이다. 패턴을 쉽게 감지하고 설명할 수 있는 간단한 사용례에서는 신뢰도를 높이는 지름길이 될 수 있다. 그러나 패턴이 충분히 복잡한 경우에는 어떤 설명도 기껏해야 어떤 결정이 어떻게 내려졌는지에 대한 암시만 제공할 뿐이며, 최악의 경우 완전한 중언부언이나 횡설수설에 지나지 않을 것이다.

즉, 요컨대, 설명 가능성은 있으면 좋지만, 이해관계자와의 신뢰를 의미 있게 이끌어내는 방식으로 제공하는 것은 불가능할 때가 많다. RAI는 모든 AI 사용례에 대한 신뢰를 보장하는 것으로, 이는 개발 및 운영에 사용되는 사람, 프로세스, 기술(특히 플랫폼)을 통해 신뢰를 제공하는 것을 의미한다.
 

책임감 있는 AI는 곧 위험 관리

결국 RAI는 AI 및 머신러닝 모델을 개발하고 사용할 때 위험을 관리하는 관행이다. 여기에는 비즈니스 위험(성능 저하 또는 신뢰할 수 없는 모델 등), 법적 위험(규제 벌금, 고객사나 또는 직원 소송 등), 심지어 사회적 위험(차별이나 환경 피해 등)까지 관리해야 한다.

이러한 위험은 사람, 프로세스, 기술의 형태로 RAI 역량을 구축하는 다층적 전략을 통해 관리된다. 사람 측면에서는 RAI를 책임지는 리더(예: 최고 데이터 분석 책임자, 최고 AI 책임자, 데이터 과학 책임자, ML 부사장)의 역량을 강화하고 실무자와 사용자가 책임감 있게 AI를 개발, 관리, 사용할 수 있도록 교육해야 한다.

프로세스 측면에서는 데이터 액세스 및 모델 학습에서 모델 배포, 모니터링 및 재학습에 이르는 엔드투엔드 수명 주기를 관리하고 통제해야 한다. 기술 측면에서 특히 중요한 것은 측면에서 플랫폼은 대규모로 사람과 프로세스를 지원하고 활성화하는 플랫폼이다. 플랫폼은 설명 가능성, 편향성 감지, 편향성 완화, 공정성 평가, 드리프트 모니터링 등 RAI 방법에 대한 액세스를 민주화하고, AI 아티팩트 거버넌스, 계보 추적, 문서 자동화, 승인 워크플로 조율, 데이터 보안은 물론 RAI 프로세스를 간소화하는 수많은 기능을 제공한다.

제약, 금융 서비스, 보험 등 규제가 심한 산업의 고급 AI 팀이 이미 이러한 기능을 구축하여 가치 창출에 나서고 있다. 이들 선도 업체는 빠른 구현, 더 큰 채택, 더 나은 성능, 향상된 안정성 등의 이점을 통해 모든 AI, 특히 생성형 AI에 대한 신뢰를 대규모로 구축하고 있다. AI 규제 마련에 대비하여 AI 이니셔티브를 미래에 대비할 뿐 아니라 무엇보다도 모든 사용자를 더 안전하게 만드는 데에도 유용하다. 책임감 있는 AI는 대규모의 AI 가치를 실현하는 열쇠지만, 그러려면 먼저 잘못된 통념을 깨야 한다.

 

https://www.itworld.co.kr/news/335878

 

'책임감 있는 AI'에 대한 잘못된 통념 3가지

책임감 있는 AI, 일명 RAI(Responsible AI)에 대한 관심이 더욱 높아지고 있다. RAI는 신뢰와 도입부터 LLM 환각 관리, 유

www.itworld.co.kr

 

 

반응형
반응형

윈도우에서 리눅스로 전환할 때 가장 큰 어려움이 바로 소프트웨어다. 윈도우에서 쓰던 것을 리눅스에서 쓰지 못할지 우려한다. 하지만 리눅스에서도 특수 목적 애플리케이션 외에 다양한 소프트웨어를 사용할 수 있다.
 

ⓒ IDG
기본적으로 리눅스는 윈도우와 다르다. 윈도우와 다르게 작동, 구성되며 윈도우 프로그램을 바로 실행할 수 없다. 다만, 가상화 또는 와인(Wine)을 사용해 리눅스에서도 윈도우 프로그램을 사용할 수 있다. 반면 윈도우에서 이미 오픈소스 소프트웨어를 많이 써왔다면 리눅스 환경에서도 쉽게 적응할 수 있다. 리브레 오피스, 파이어폭스, 썬더버드, VLC와 같은 프로그램은 리눅스와 윈도우에서 모두 똑같이 사용할 수 있으며, 작동 방식도 거의 같다. 이들 외에 다른 애플리케이션도 다양하지만, 일부 기능과 작동 방식은 윈도우용 프로그램과 다소 차이가 있다. 어느 정도 익숙해지는 시간이 필요하다.
 

리눅스에서 소프트웨어 설치하기

윈도우 10 또는 11에서는 마이크로소프트 스토어를 통해 새 소프트웨어를 설치하고 업데이트할 수 있다. 여기서 제공되지 않는 것은 제조업체의 웹사이트나 기타 다른 곳에서 내려받은 설치 파일을 사용한다. 이런 소프트웨어를 표준화된 관리 기능이 없으므로 업데이트가 자동으로 이뤄지지 않을 수 있다.

리눅스 시스템은 윈도우와 달리 각 배포판의 리포지토리에서 소프트웨어를 가져온다. 중앙 패키지 데이터베이스가 있으며, 업데이트는 시스템과 설치된 모든 프로그램에 적용됩니다. 따라서 프로그램 패키지를 확인하고 서명하기 때문에 높은 수준의 보안이 보장된다.

문제는 우분투 22.04 또는 리눅스 민트 21.2와 같은 LTS(long-term support) 리눅스 배포판은 5년의 지원 기간 동안 새로운 소프트웨어가 거의 제공되지 않는다는 점이다. 파이어폭스나 썬더버드 같은 보안에 중요한 소프트웨어는 정기적으로 업데이트되지만, 리브레 오피스와 같은 프로그램은 기본 버전이 그대로 유지된다. 시스템의 안정성을 더 중요하게 생각하기 때문이다.
 

사용할 수 있는 업데이트에 대해 자동으로 알려준다. 시스템과 설치된 모든 애플리케이션에 대한 설치가 중앙 집중식으로 이루어진다. ⓒ IDG
리눅스용 최신 프로그램 : LTS 배포판 사용자가 최신 소프트웨어를 계속 받을 수 있도록 우분투와 리눅스 민트는 표준 패키지 관리 외에도 스냅(Snap)과 플랫팩(Flatpak) 등 2가지 컨테이너 형식을 사용한다. 프로그램과 필요한 모든 시스템 파일을 별도의 영역에 설정해 시스템의 나머지 부분이 최신 파일의 영향을 받지 않는 장점이 있다. 반면 기존 패키지 설치보다 하드 드라이브에 더 많은 공간이 있어야 하는 것은 단점이다.

사용자 관점에서 처음에는 패키지의 정확한 형식이 중요하지 않다. 우분투에서는 "우분투 소프트웨어(Ubuntu Software)" 도구를 통해, 리눅스 민트에서는 "애플리케이션 관리(Application Management)"를 통해 패키지 관리가 여전히 중앙 집중화되어 있기 때문이다. 여기서 프로그램을 검색하거나 프로그램을 추천받을 수 있다.

하지만 새 프로그램을 설치할 때는 사용 가능한 버전이 여러 가지인 경우가 많으므로 버전에 주의해야 한다. 우분투 소프트웨어에서 "스냅 스토어(Snap Store)"와 "ubunt-jammyuniverse(deb) 또는 이와 유사한 소스(Source) 중 선택할 수 있다. 스냅 스토어의 프로그램은 일반적으로 더 최신 버전이며, "프리뷰/엣지(preview/edge)"가 추가된 프리 릴리즈 버전도 있다. 리눅스 민트도 비슷합니다. 일반적으로 오래된 "시스템 패키지(system package)"와 "플랫팩, 플랫텁(Flatpak, Flathub)" 및 "플랫팩, 플랫텁 베타(Flatpak, Flathub Beta)" 중 선택할 수 있다. 클래식 시스템 패키지와 스냅, 플랫팩 컨테이너를 병렬로 설치할 수도 있다.
 

리브레 오피스는 광범위한 기능을 제공한다. 메뉴 대신 리본을 활성화할 수 있어 마이크로소프트 오피스와 비슷하다. ⓒ IDG 

리눅스용 오피스 패키지

마이크로소프트 오피스는 가장 널리 쓰이는 생산성 앱이다. 워드 프로세싱, 스프레드시트의 전 세계 표준이다. 하지만 가끔 메일을 사용하거나 문서를 작성하는 정도라면 우분투와 리눅스 민트에 사전 설치된 리브레 오피스로도 충분하다.

다른 사람과 문서를 자주 주고받거나 마이크로소프트 인프라에 의존하는 환경에서 작업한다면 리눅스를 자신 있게 추천하기 힘들다. 리브레 오피스는 마이크로소프트 서식 문서를 읽을 수 있지만, 복잡한 문서의 경우 서식이 항상 올바르게 표시되는 것은 아니다. 특히 표나 매크로 등이 깨질 가능성이 있다. 가장 큰 어려움은 아마도 스프레드시트다. 리브레 오피스의 스프레드시트 앱인 칼크(Calc)는 엑셀과 비슷한 기능을 제공하지만, 전체적으로 같은 기능이라고 할 수는 없다. 

또한, 리브레 오피스는 기본적으로 클래식 메뉴 모음을 표시한다. 마이크로소프트 오피스에 익숙하다면 보기 > 사용자 인터페이스에서 "탭에서(In tab)"를 선택해 활성화하는 리본을 선호할 수도 있다. "모두(For all)"를 클릭하면 이 설정이 오피스 제품군의 모든 하위 프로그램에 적용된다.

리브레 오피스 대신 리눅스에서 사용할 만한 생산성 앱을 더 찾는다면, 마이크로소프트의 무료 온라인 오피스 제품도 살펴볼 만하다. 오피스닷컴(www.office.com)에 마이크로소프트 계정으로 로그인하기만 하면, 웹 브라우저에서 워드, 엑셀, 파워포인트 프로그램을 무료로 쓸 수 있다. 단, 웹용 오피스는 설치용 마이크로소프트 365와 같은 기능이 아니며, 자세한 차이점은 여길 참고하면 된다. 제약이 있기는 하지만 외부에서 마이크로소프트 오피스 파일을 편집하거나 다른 사람과 파일을 교환할 때 유용하다. 이렇게 만든 오피스 파일은 클라우드 원드라이브에 저장된다. 오프라인에서 문서 작업을 하려면 파일을 내려받아야 한다. 웹용 오피스에서는 리브레 오피스 형식의 파일을 가져오고 내보낼 수도 있다.

또 다른 대안으로는 워드 프로세서인 텍스트메이커, 프레젠테이션 프로그램인 프레젠테이션, 스프레드시트 프로그램인 플랜메이커가 포함된 소프트메이커 오피스(Softmaker Office)의 무료 버전 소프트메이커 프리오피스(Softmaker FreeOffice)가 있다. 
 

김프는 사진 편집은 물론, 여러 레이어와 마스크가 있는 복잡한 그래픽 작업까지 지원한다. ⓒ IDG 

이미지 편집과 그래픽

리눅스에는 이미 간단한 이미지 뷰어가 설치돼 있다. 그놈(Gnome) 데스크톱이 설치된 우분투에는 그놈 이미지 뷰어, 리눅스 민트에는 X 뷰어가 있다. 두 프로그램을 이용하면 슬라이드쇼를 표시하고 이미지를 회전할 수 있다. 간단한 편집 기능이 필요하다면 우분투에서는 샷웰(Shotwell)을, 리눅스 민트에서는 픽스(Pix)를 쓰면 된다. 기능은 비슷하다. 적목 제거, 색상 및 밝기 조정, 이미지 자르기 등을 지원한다.

이 중에서 디지캠(Digikam)은 사진 관리 앱이다. 앨범, 태그 또는 키워드에 따라 이미지를 정렬하고 분류할 수 있다. 톤 보정, 적목 제거, 프레임 추가 등 다양한 사진 편집 툴을 쓸 수 있다. RAW 파일을 보거나, 화이트 밸런스 같은 보정을 할 수 있다. 물론 로 테라피(Raw Therapee)나 다크테이블(Darktable)과 맞먹을 정도는 아니다. 더 많은 기능이 필요하면 전문가용 앱을 찾아야 한다.

김프(Gimp)는 주로 윈도우에서 포토샵을 쓰던 사용자를 대상으로 한 이미지 툴이다. 이 프로그램은 페인팅 도구, 레이어, 마스크, 자동 향상, 다양한 필터를 제공한다. 사진을 수정하고, 이미지를 자르거나 변형할 수 있다. 포토샵과 마찬가지로 대부분 기능은 즉시 액세스할 수 없고, 특정 효과는 마스크와 레이어를 적절히 조합해야만 쓸 수 있다. 하지만 널리 보급돼 사용되고 있으므로 궁금한 점은 구글링으로 대부분 해결할 수 있다. 김프 홈페이지의 문서(www.gimp.org/docs)가 좋은 출발점이다.

크리타(Krita)는 김프와 비슷하지만 더 쓰기 쉽다. 이 소프트웨어는 원래 페인팅 프로그램으로 만들어졌고, 드로잉 태블릿에서도 쓸 수 있다. 기본적으로 이미지를 선명하게 하거나 노이즈를 줄이는 등 사진에 대한 다양한 자동 보정과 최적화 도구를 지원한다. 크리타는 이미지를 예술적으로 바꾸는 다양한 효과도 제공한다. '이미지(Image)' 메뉴의 명령으로 크기 조정, 회전, 자르기를 빠르고 쉽게 할 수 있다.

잉크스케이프(Inkscape)는 어도비 일러스트레이터와 비슷하다. 벡터 그래픽 프로그램으로, 만화, 클립아트, 로고, 전단지, 브로셔, 다이어그램과 같은 예술적, 기술적 일러스트레이션 작업에 적합하다. 해상도와 관계없이 화면 및 인쇄용 요소를 선명하고 손실 없이 만들 수 있다. 잉크스케이프에서 사용하는 파일 형식은 SVG(Scalable Vector Graphics)다.
 

VLC 미디어 플레이어는 윈도우 버전과 거의 비슷하며 똑같이 작동한다. 일반적인 미디어 형식을 안정적으로 재생한다. ⓒ IDG 

오디오와 비디오용 소프트웨어

리눅스는 멀티미디어 기능에 있어서 아쉬운 것이 없다. 운영체제에 기본적으로 포함된 프로그램이 대부분 동영상 형식을 문제없이 재생하고, 필요한 경우 추가 코덱을 설치하면 된다.

대표적인 동영상 플레이어인 VLC는 윈도우에서 자주 사용되는데, 리눅스에서도 사용할 수 있다. 모든 일반적인 코덱이 포함돼 있어 거의 모든 동영상 형식을 재생한다. 복사 방지된 DVD를 재생하려면 추가 라이브러리가 필요하지만, 이는 다른 모든 미디어 플레이어와 마찬가지다. 복사 방지 DVD 재생 라이브러리를 설치하려면 일단 터미널을 연다.
 

sudo apt install libdvd-pkg


다음 명령으로 설치를 완료한다.
 

sudo dpkg-reconfigure libdvd-pkg


VLC 대신 쓸 수 있는 동영상 플레이어를 찾는다면 Sm플레이어(Smplayer)가 있다. 때에 따라 실행 속도가 조금 더 빠르고 별도 테마를 통해 인터페이스를 사용자 지정할 수 있는 것이 특징이다. Sm플레이어에는 중요한 오디오, 비디오 코덱도 모두 포함돼 있다. 오픈샷(Openshot)은 동영상을 편집하고, 효과를 추가하고, 여러 형식으로 저장하는 소프트웨어다. 인터페이스가 직관적이어서 초보자도 쉽게 이해하고 사용할 수 있다.
 

리브레 오피스 구성 폴더의 구조는 윈도우와 리눅스에서 같다. 따라서 파일을 리눅스 시스템으로 간단히 복사할 수 있다. ⓒ IDG 

윈도우의 앱 설정을 그대로 리눅스로 옮기는 방법

윈도우에서 리눅스로 전환해 필요한 소프트웨어를 사용할 때 리브레 오피스와 같이 복잡한 구성의 프로그램은 윈도우에서 데이터를 복사하는 방식으로 설정하는 시간을 줄일 수 있다. 인터페이스를 일관되게 사용할 수 있을 뿐만 아니라 자동 텍스트 모듈, 자체 사전 또는 매크로도 그대로 쓸 수 있다. 단, 이렇게 설정을 그대로 옮기려면 두 프로그램의 버전이 일치해야 한다.

윈도우용 리브레 오피스의 구성을 리눅스로 옮기기
구체적인 방법은 다음과 같다.
 

  • 1단계 : 윈도우에서 리브레 오피스를 종료하고 윈도우 탐색기의 주소 표시줄에 %appdata%를 입력한 후 엔터 키를 눌러 확인한다. 그런 다음 하위 폴더 "LibreOffice\4"로 변경하고 "user" 폴더를 ZIP 파일로 압축한다. 이 파일을 리눅스 홈 디렉터리에 복사한다.
  • 2단계 : 아직 리눅스에서 오피스 패키지를 실행한 적이 없다면, 리브레 오피스를 시작하고 프로그램을 다시 닫는다. 파일 관리자에서 홈 디렉터리로 이동해 ZIP 파일의 압축을 풀고 Ctrl-C를 사용하여 "사용자" 폴더를 복사한다.
  • 3단계: 파일 관리자에서 Ctrl-H를 사용해 숨겨진 파일을 표시하고 ".config/libreoffice/4" 폴더로 변경한다. 디렉터리 이름을 "user"로 바꾸고 윈도우에서 복사한 폴더를 Ctrl-V로 붙여 넣는다. 이제 리브레 오피스를 시작하면 윈도우에서 설정한 그대로 사용할 수 있다.


파이어폭스의 구성 옮기기
파이어폭스에서 데이터를 옮기려면 리눅스용 파이어폭스 버전이 윈도우 버전과 같거나 최신 버전이어야 한다. 버전 번호는 도움말 > 파이어폭스 정보에서 햄버거 메뉴를 클릭해 확인할 수 있다. 구체적인 방법은 다음과 같다.
 

  • 파이어폭스의 윈도우에서 햄버거 메뉴(대시 3개)를 클릭하고 도움말 > 추가 문제 해결 정보를 선택한다. "일반 정보" 아래에서 "프로필 폴더" 다음에 있는 "폴더 열기"를 클릭합니다. 파이어폭스를 종료한다. 표시된 폴더를 ZIP 파일로 압축한 다음 리눅스 홈 디렉터리에 복사해 압축을 푼다.
  • 리눅스에서 파이어폭스를 시작하고 윈도우와 마찬가지로 프로필 폴더를 만든다. 파이어폭스를 종료한다. 윈도우 백업의 프로필 폴더에 있는 모든 파일을 리눅스 프로필 폴더로 복사하고 기존 파일을 모두 덮어쓴다.


썬더버드의 구성 옮기기
여기서도 두 프로그램 버전이 같거나 리눅스 버전이 최신 버전이어야 한다. 파이어폭스와 마찬가지로 프로필 폴더를 정하고 썬더버드를 닫은 다음 폴더를 ZIP 파일로 압축한다. 리눅스에서 이 파일의 압축을 풀고 썬더버드 프로필 폴더를 확인한다. 이제 썬더버드를 닫고 윈도우 프로필의 파일을 이 폴더에 복사한다.

 

https://www.itworld.co.kr/news/335815

반응형

+ Recent posts